Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reduce motion #101

Merged
merged 4 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/renderer/src/atoms/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export * from "./dom"
export * from "./route"
export * from "./sidebar"
export * from "./ui"
export * from "./user"
75 changes: 75 additions & 0 deletions src/renderer/src/atoms/ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useRefValue } from "@renderer/hooks"
import { createAtomHooks } from "@renderer/lib/jotai"
import { getStorageNS } from "@renderer/lib/ns"
import { useAtomValue } from "jotai"
import { atomWithStorage, selectAtom } from "jotai/utils"
import { useMemo } from "react"

const createDefaultSettings = () => ({
// Sidebar
entryColWidth: 340,
opaqueSidebar: false,
sidebarShowUnreadCount: true,

// Global UI
uiTextSize: 16,
// System
showDockBadge: true,
// Misc
modalOverlay: true,
modalDraggable: true,
modalOpaque: true,
reduceMotion: false,

// Content
readerFontFamily: "SN Pro",
readerRenderInlineStyle: false,
codeHighlightTheme: "github-dark",
})
const atom = atomWithStorage(getStorageNS("ui"), createDefaultSettings())
const [, , useUISettingValue, , getUISettings, setUISettings] =
createAtomHooks(atom)

export const initializeDefaultUISettings = () => {
const currentSettings = getUISettings()
const defaultSettings = createDefaultSettings()
if (typeof currentSettings !== "object") setUISettings(defaultSettings)
const newSettings = { ...defaultSettings, ...currentSettings }
setUISettings(newSettings)
}

export { getUISettings, useUISettingValue }
export const useUISettingKey = <
T extends keyof ReturnType<typeof getUISettings>,
>(
key: T,
) => useAtomValue(useMemo(() => selectAtom(atom, (s) => s[key]), [key]))

export const useUISettingSelector = <
T extends keyof ReturnType<typeof getUISettings>,
S extends ReturnType<typeof getUISettings>,
R = S[T],
>(
selector: (s: S) => R,
): R => {
const stableSelector = useRefValue(selector)

return useAtomValue(
// @ts-expect-error

Check warning on line 58 in src/renderer/src/atoms/ui.ts

View workflow job for this annotation

GitHub Actions / Lint and Typecheck (18.x)

Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer
useMemo(() => selectAtom(atom, stableSelector.current), [stableSelector]),
)
}

export const setUISetting = <K extends keyof ReturnType<typeof getUISettings>>(
key: K,
value: ReturnType<typeof getUISettings>[K],
) => {
setUISettings({
...getUISettings(),
[key]: value,
})
}

export const clearUISettings = () => {
setUISettings(createDefaultSettings())
}
34 changes: 34 additions & 0 deletions src/renderer/src/components/common/Motion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useReduceMotion } from "@renderer/hooks/biz/useReduceMotion"
import type { MotionProps } from "framer-motion"
import { m as M } from "framer-motion"
import { createElement, forwardRef } from "react"

const cacheMap = new Map<string, any>()
export const m: typeof M = new Proxy(M, {
get(target, p: string) {
const Component = target[p]

if (cacheMap.has(p)) {
return cacheMap.get(p)
}
const MotionComponent = forwardRef((props: MotionProps, ref) => {
const shouldReduceMotion = useReduceMotion()
const nextProps = { ...props }
if (shouldReduceMotion) {
if (props.exit) {
delete nextProps.exit
}

if (props.initial) {
nextProps.initial = true
}
}

return createElement(Component, { ...nextProps, ref })
})

cacheMap.set(p, MotionComponent)

return MotionComponent
},
})
4 changes: 2 additions & 2 deletions src/renderer/src/components/ui/background/vibrancy.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useUISettingKey } from "@renderer/atoms"
import { useDark } from "@renderer/hooks"
import { cn } from "@renderer/lib/utils"
import { useUIStore } from "@renderer/store"
import { useMediaQuery } from "usehooks-ts"

export const Vibrancy: Component<
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
> = ({ className, children, ...rest }) => {
const opaqueSidebar = useUIStore((s) => s.opaqueSidebar)
const opaqueSidebar = useUISettingKey("opaqueSidebar")
const canVibrancy =
window.electron &&
window.electron.process.platform === "darwin" &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { m } from "@renderer/components/common/Motion"
import { cn } from "@renderer/lib/utils"
import type { Variants } from "framer-motion"
import { AnimatePresence, m } from "framer-motion"
import { AnimatePresence } from "framer-motion"
import { useCallback, useRef, useState } from "react"

import { MotionButtonBase } from "../button"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @eslint-react/dom/no-dangerously-set-innerhtml */
import { useUISettingSelector } from "@renderer/atoms"
import { cn } from "@renderer/lib/utils"
import { useUIStore } from "@renderer/store"
import type { FC } from "react"
import { useLayoutEffect, useMemo, useRef, useState } from "react"
import type {
Expand Down Expand Up @@ -51,7 +51,7 @@ export const ShikiHighLighter: FC<ShikiProps> = (props) => {

const [loaded, setLoaded] = useState(false)

const codeTheme = useUIStore((s) => overrideTheme || s.codeHighlightTheme)
const codeTheme = useUISettingSelector((s) => overrideTheme || s.codeHighlightTheme)
useLayoutEffect(() => {
let isMounted = true
setLoaded(false)
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/components/ui/image/preview-image.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { m } from "@renderer/components/common/Motion"
import { stopPropagation } from "@renderer/lib/dom"
import { m } from "framer-motion"
import type { FC } from "react"
import { useState } from "react"
import { Mousewheel, Scrollbar, Virtual } from "swiper/modules"
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/src/components/ui/modal/stacked/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getUISettings } from "@renderer/atoms"
import { jotaiStore } from "@renderer/lib/jotai"
import { useUIStore } from "@renderer/store"
import { useCallback, useContext, useEffect, useId, useRef } from "react"
import { useLocation } from "react-router-dom"

Expand Down Expand Up @@ -31,9 +31,9 @@ export const useModalStack = (options?: ModalStackOptions) => {
} else {
// NOTE: The props of the Command Modal are immutable, so we'll just take the store value and inject it.
// There is no need to inject `overlay` props, this is rendered responsively based on ui changes.
const uiState = useUIStore.getState()
const uiSettings = getUISettings()
const modalConfig: Partial<ModalProps> = {
draggable: uiState.modalDraggable,
draggable: uiSettings.modalDraggable,
}
jotaiStore.set(modalStackAtom, (p) => {
const modalProps: ModalProps = {
Expand Down
8 changes: 5 additions & 3 deletions src/renderer/src/components/ui/modal/stacked/modal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as Dialog from "@radix-ui/react-dialog"
import { useUISettingKey } from "@renderer/atoms"
import { m } from "@renderer/components/common/Motion"
import { stopPropagation } from "@renderer/lib/dom"
import { cn } from "@renderer/lib/utils"
import { useUIStore } from "@renderer/store"
import { m, useAnimationControls, useDragControls } from "framer-motion"
import { useAnimationControls, useDragControls } from "framer-motion"
import { useSetAtom } from "jotai"
import type { PointerEventHandler, SyntheticEvent } from "react"
import {
Expand Down Expand Up @@ -60,7 +61,8 @@ export const ModalInternal: Component<{
// opaque: state.modalOpaque,
// })),
// )
const opaque = useUIStore((state) => state.modalOpaque)
// const opaque = useUIStore((state) => state.modalOpaque)
const opaque = useUISettingKey("modalOpaque")

const {
CustomModalComponent,
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/components/ui/modal/stacked/overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { m } from "framer-motion"
import { m } from "@renderer/components/common/Motion"

import { RootPortal } from "../../portal"

Expand Down
8 changes: 4 additions & 4 deletions src/renderer/src/components/ui/modal/stacked/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useUIStore } from "@renderer/store"
import { useUISettingKey } from "@renderer/atoms"
import { AnimatePresence } from "framer-motion"
import { useAtomValue } from "jotai"
import type { FC, PropsWithChildren } from "react"

import { modalStackAtom } from "./atom"
import { MODAL_STACK_Z_INDEX } from "./constants"
import { useDismissAllWhenRouterChange } from "./hooks"
// import { useDismissAllWhenRouterChange } from "./hooks"
import { ModalInternal } from "./modal"
import { ModalOverlay } from "./overlay"
Expand All @@ -20,9 +21,8 @@ const ModalStack = () => {
const stack = useAtomValue(modalStackAtom)

// Vite HMR issue
// useDismissAllWhenRouterChange()

const modalSettingOverlay = useUIStore((state) => state.modalOverlay)
useDismissAllWhenRouterChange()
const modalSettingOverlay = useUISettingKey("modalOverlay")

const forceOverlay = stack.some((item) => item.overlay)

Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/database/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createAtomHooks, jotaiStore } from "@renderer/lib/jotai"
import { buildStorageNS } from "@renderer/lib/ns"
import { getStorageNS } from "@renderer/lib/ns"
import { atomWithStorage } from "jotai/utils"

const SHOULD_USE_INDEXED_DB_KEY = buildStorageNS("shouldUseIndexedDB")
const SHOULD_USE_INDEXED_DB_KEY = getStorageNS("shouldUseIndexedDB")

export const [
__shouldUseIndexedDBAtom,
Expand Down
8 changes: 8 additions & 0 deletions src/renderer/src/hooks/biz/useReduceMotion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useUISettingKey } from "@renderer/atoms/ui"
import { useReducedMotion } from "framer-motion"

export const useReduceMotion = () => {
const appReduceMotion = useUISettingKey("reduceMotion")
const reduceMotion = useReducedMotion()
return appReduceMotion || reduceMotion
}
4 changes: 2 additions & 2 deletions src/renderer/src/lib/constants.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildStorageNS } from "./ns"
import { getStorageNS } from "./ns"

export const levels = {
view: "view",
Expand Down Expand Up @@ -79,7 +79,7 @@ export const APP_NAME = "Follow"
/// Feed
export const FEED_COLLECTION_LIST = "collections"
/// Local storage keys
export const QUERY_PERSIST_KEY = buildStorageNS("REACT_QUERY_OFFLINE_CACHE")
export const QUERY_PERSIST_KEY = getStorageNS("REACT_QUERY_OFFLINE_CACHE")

/// Route Keys
export const ROUTE_FEED_PENDING = "pending"
6 changes: 5 additions & 1 deletion src/renderer/src/lib/ns.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
const ns = "follow"
export const buildStorageNS = (key: string) => `${ns}:${key}`
export const getStorageNS = (key: string) => `${ns}:${key}`
/**
* @deprecated Use `getStorageNS` instead.
*/
export const buildStorageKey = getStorageNS
6 changes: 3 additions & 3 deletions src/renderer/src/modules/entry-column/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMainContainerElement } from "@renderer/atoms"
import { useUser } from "@renderer/atoms/user"
import { m } from "@renderer/components/common/Motion"
import { ActionButton, StyledButton } from "@renderer/components/ui/button"
import {
Popover,
Expand All @@ -16,7 +17,7 @@ import {
} from "@renderer/hooks/biz/useRouteParams"
import { apiClient } from "@renderer/lib/api-fetch"
import { ROUTE_FEED_PENDING, views } from "@renderer/lib/constants"
import { buildStorageNS } from "@renderer/lib/ns"
import { getStorageNS } from "@renderer/lib/ns"
import { shortcuts } from "@renderer/lib/shortcuts"
import { cn, getEntriesParams, getOS, isBizId } from "@renderer/lib/utils"
import { useEntries } from "@renderer/queries/entries"
Expand All @@ -32,7 +33,6 @@ import {
useEntryIdsByFeedIdOrView,
} from "@renderer/store/entry/hooks"
import type { HTMLMotionProps } from "framer-motion"
import { m } from "framer-motion"
import { useAtom, useAtomValue } from "jotai"
import { atomWithStorage } from "jotai/utils"
import { debounce } from "lodash-es"
Expand All @@ -56,7 +56,7 @@ import { LoadingCircle } from "../../components/ui/loading"
import { EntryItem } from "./item"

const unreadOnlyAtom = atomWithStorage<boolean>(
buildStorageNS("entry-unreadonly"),
getStorageNS("entry-unreadonly"),
true,
undefined,
{
Expand Down
11 changes: 5 additions & 6 deletions src/renderer/src/modules/entry-content/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useUISettingKey } from "@renderer/atoms"
import { m } from "@renderer/components/common/Motion"
import { Logo } from "@renderer/components/icons/logo"
import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height"
import { useBizQuery } from "@renderer/hooks"
Expand All @@ -9,8 +11,7 @@ import {
WrappedElementProvider,
} from "@renderer/providers/wrapped-element-provider"
import { Queries } from "@renderer/queries"
import { useEntry, useFeedHeaderTitle, useUIStore } from "@renderer/store"
import { m } from "framer-motion"
import { useEntry, useFeedHeaderTitle } from "@renderer/store"
import { useEffect, useState } from "react"

import { LoadingCircle } from "../../components/ui/loading"
Expand Down Expand Up @@ -43,9 +44,7 @@ function EntryContentRender({ entryId }: { entryId: string }) {

const entry = useEntry(entryId)
const [content, setContent] = useState<JSX.Element>()
const readerRenderInlineStyle = useUIStore(
(state) => state.readerRenderInlineStyle,
)
const readerRenderInlineStyle = useUISettingKey("readerRenderInlineStyle")
useEffect(() => {
// Fallback data, if local data is broken should fallback to cached query data.
const processContent = entry?.entries.content ?? data?.entries.content
Expand Down Expand Up @@ -85,7 +84,7 @@ function EntryContentRender({ entryId }: { entryId: string }) {
},
)

const readerFontFamily = useUIStore((state) => state.readerFontFamily)
const readerFontFamily = useUISettingKey("readerFontFamily")

if (!entry) return null

Expand Down
4 changes: 3 additions & 1 deletion src/renderer/src/modules/feed-column/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Logo } from "@renderer/components/icons/logo"
import { ActionButton } from "@renderer/components/ui/button"
import { ProfileButton } from "@renderer/components/user-button"
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
import { useReduceMotion } from "@renderer/hooks/biz/useReduceMotion"
import { APP_NAME, levels, views } from "@renderer/lib/constants"
import { stopPropagation } from "@renderer/lib/dom"
import { Routes } from "@renderer/lib/enum"
Expand Down Expand Up @@ -99,6 +100,7 @@ export function FeedColumn() {
const normalStyle =
!window.electron || window.electron.process.platform !== "darwin"

const reduceMotion = useReduceMotion()
return (
<Vibrancy
className="flex h-full flex-col gap-3 pt-2.5"
Expand Down Expand Up @@ -158,7 +160,7 @@ export function FeedColumn() {
))}
</div>
<div className="size-full overflow-hidden" ref={carouselRef}>
<m.div className="flex h-full" style={{ x: spring }}>
<m.div className="flex h-full" style={{ x: reduceMotion ? -active * 256 : spring }}>
{views.map((item, index) => (
<section
key={item.name}
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/modules/feed-column/list.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useUISettingKey } from "@renderer/atoms"
import { useBizQuery } from "@renderer/hooks"
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
import { useRouteFeedId } from "@renderer/hooks/biz/useRouteParams"
Expand All @@ -11,7 +12,6 @@ import type { SubscriptionPlainModel } from "@renderer/store"
import {
getFeedById,
useSubscriptionByView,
useUIStore,
useUnreadStore,
} from "@renderer/store"
import { useMemo, useState } from "react"
Expand Down Expand Up @@ -123,7 +123,7 @@ export function FeedList({

const feedId = useRouteFeedId()
const navigate = useNavigateEntry()
const showUnreadCount = useUIStore((state) => state.sidebarShowUnreadCount)
const showUnreadCount = useUISettingKey("sidebarShowUnreadCount")

return (
<div className={cn(className, "font-medium")}>
Expand Down
Loading
Loading