perf: use focusable selector hooks across components

- Replaced instances of `useGlobalFocusableScope` with `useGlobalFocusableScopeSelector` for improved focus state management.
- Updated various components including FeedColumn, Subview, EntryColumn, and FeedItem to utilize the new selector for better performance and clarity.
- Added a new `viewportClassName` property to the SettingPageConfig interface for enhanced configuration options.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-05-27 23:01:37 +08:00
parent f31d43da08
commit e374dd6473
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
10 changed files with 42 additions and 26 deletions

View File

@ -1,6 +1,6 @@
import type { DragEndEvent } from "@dnd-kit/core"
import { DndContext, PointerSensor, pointerWithin, useSensor, useSensors } from "@dnd-kit/core"
import { useGlobalFocusableScope } from "@follow/components/common/Focusable/hooks.js"
import { useGlobalFocusableScopeSelector } from "@follow/components/common/Focusable/hooks.js"
import { PanelSplitter } from "@follow/components/ui/divider/index.js"
import { Kbd } from "@follow/components/ui/kbd/Kbd.js"
import { RootPortal } from "@follow/components/ui/portal/index.jsx"
@ -238,11 +238,14 @@ const FeedResponsiveResizerContainer = ({
}
}, [feedColumnShow])
const activeScopes = useGlobalFocusableScope()
const when = useGlobalFocusableScopeSelector(
// eslint-disable-next-line @eslint-react/hooks-extra/no-unnecessary-use-callback
React.useCallback((activeScope) => !activeScope.or(...FloatingLayerScope), []),
)
useCommandBinding({
commandId: COMMAND_ID.layout.toggleSubscriptionColumn,
when: !activeScopes.or(...FloatingLayerScope),
when,
})
const [delayShowSplitter, setDelayShowSplitter] = useState(feedColumnShow)

View File

@ -1,5 +1,5 @@
import { getReadonlyRoute } from "@follow/components/atoms/route.js"
import { useGlobalFocusableScope } from "@follow/components/common/Focusable/hooks.js"
import { useGlobalFocusableHasScope } from "@follow/components/common/Focusable/hooks.js"
import { MotionButtonBase } from "@follow/components/ui/button/index.js"
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
import { Routes } from "@follow/constants"
@ -71,9 +71,9 @@ function SubviewLayoutInner() {
navigate(-1)
}
}
const activeScope = useGlobalFocusableScope()
useHotkeys("Escape", backHandler, {
enabled: activeScope.has(HotkeyScope.SubLayer),
enabled: useGlobalFocusableHasScope(HotkeyScope.SubLayer),
})
return (
<div className="relative flex size-full">

View File

@ -1,6 +1,6 @@
import {
useFocusActions,
useGlobalFocusableScope,
useGlobalFocusableScopeSelector,
} from "@follow/components/common/Focusable/hooks.js"
import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js"
import { useRefValue } from "@follow/hooks"
@ -24,9 +24,7 @@ export const EntryColumnShortcutHandler: FC<{
}> = memo(({ data, refetch, handleScrollTo }) => {
const dataRef = useRefValue(data!)
const activeScope = useGlobalFocusableScope()
const when = FocusablePresets.isTimeline(activeScope)
const when = useGlobalFocusableScopeSelector(FocusablePresets.isTimeline)
useCommandBinding({
commandId: COMMAND_ID.timeline.switchToNext,

View File

@ -1,11 +1,11 @@
import { useGlobalFocusableScope } from "@follow/components/common/Focusable/hooks.js"
import { useGlobalFocusableScopeSelector } from "@follow/components/common/Focusable/hooks.js"
import { ActionButton, Button } from "@follow/components/ui/button/index.js"
import { Kbd, KbdCombined } from "@follow/components/ui/kbd/Kbd.js"
import { useCountdown } from "@follow/hooks"
import { EventBus } from "@follow/utils/event-bus"
import { cn } from "@follow/utils/utils"
import type { FC, ReactNode } from "react"
import { useEffect, useState } from "react"
import { useCallback, useEffect, useState } from "react"
import { useHotkeys } from "react-hotkeys-hook"
import { Trans, useTranslation } from "react-i18next"
import { toast } from "sonner"
@ -33,10 +33,17 @@ export const MarkAllReadButton = ({
const { t } = useTranslation()
const { t: commonT } = useTranslation("common")
const activeScope = useGlobalFocusableScope()
// const activeScope = useGlobalFocusableScope()
const when = useGlobalFocusableScopeSelector(
// eslint-disable-next-line @eslint-react/hooks-extra/no-unnecessary-use-callback
useCallback(
(activeScope) => activeScope.or(HotkeyScope.Timeline, HotkeyScope.SubscriptionList),
[],
),
)
useCommandBinding({
commandId: COMMAND_ID.subscription.markAllAsRead,
when: activeScope.or(HotkeyScope.Timeline, HotkeyScope.SubscriptionList),
when,
})
useEffect(() => {

View File

@ -1,4 +1,4 @@
import { useGlobalFocusableScope } from "@follow/components/common/Focusable/hooks.js"
import { useGlobalFocusableScopeSelector } from "@follow/components/common/Focusable/hooks.js"
import { useMobile } from "@follow/components/hooks/useMobile.js"
import type { FeedViewType } from "@follow/constants"
import { views } from "@follow/constants"
@ -51,8 +51,8 @@ export const EntryItemWrapper: FC<
({ entryId }) => entryId === entry.entries.id,
[entry.entries.id],
)
const scope = useGlobalFocusableScope()
useContextMenuActionShortCutTrigger(actionConfigs, isActive && FocusablePresets.isTimeline(scope))
const when = useGlobalFocusableScopeSelector(FocusablePresets.isTimeline)
useContextMenuActionShortCutTrigger(actionConfigs, isActive && when)
const asRead = useEntryIsRead(entry)
const hoverMarkUnread = useGeneralSettingKey("hoverMarkUnread")

View File

@ -8,6 +8,8 @@ import { IconTransition } from "~/components/ux/transition/icon"
export const EnhancedSettingsIndicator = () => {
const enhancedSettings = useGeneralSettingKey("enhancedSettings")
const { t } = useTranslation("settings")
if (!enhancedSettings) return null
return (
<Tooltip>
<TooltipTrigger>

View File

@ -21,6 +21,7 @@ export interface SettingPageConfig {
ctx: SettingPageContext,
serverConfigs?: ServerConfigs | null,
) => [boolean, DisableWhy]
viewportClassName?: string
}
export const defineSettingPageData = (config: SettingPageConfig) => ({
...config,

View File

@ -1,4 +1,4 @@
import { useGlobalFocusableScope } from "@follow/components/common/Focusable/hooks.js"
import { useGlobalFocusableScopeSelector } from "@follow/components/common/Focusable/hooks.js"
import { useMobile } from "@follow/components/hooks/useMobile.js"
import { OouiUserAnonymous } from "@follow/components/icons/OouiUserAnonymous.jsx"
import { Button } from "@follow/components/ui/button/index.js"
@ -120,9 +120,9 @@ const FeedItemImpl = ({ view, feedId, className, isPreview }: FeedItemProps) =>
view,
})
const scope = useGlobalFocusableScope()
const when = useGlobalFocusableScopeSelector(FocusablePresets.isSubscriptionList)
const whenTrigger = FocusablePresets.isSubscriptionList(scope) && isActive
const whenTrigger = when && isActive
useContextMenuActionShortCutTrigger(items, whenTrigger)
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
@ -260,8 +260,8 @@ const ListItemImpl: Component<ListItemProps> = ({
const isActive = useRouteParamsSelector((routerParams) => routerParams.listId === listId)
const items = useListActions({ listId, view })
const scope = useGlobalFocusableScope()
useContextMenuActionShortCutTrigger(items, FocusablePresets.isSubscriptionList(scope) && isActive)
const when = useGlobalFocusableScopeSelector(FocusablePresets.isSubscriptionList)
useContextMenuActionShortCutTrigger(items, when && isActive)
const listUnread = useUnreadByListId(listId)
@ -369,8 +369,8 @@ const InboxItemImpl: Component<InboxItemProps> = ({ view, inboxId, className, ic
const isActive = useRouteParamsSelector((routerParams) => routerParams.inboxId === inboxId)
const { items } = useInboxActions({ inboxId })
const scope = useGlobalFocusableScope()
useContextMenuActionShortCutTrigger(items, FocusablePresets.isSubscriptionList(scope) && isActive)
const when = useGlobalFocusableScopeSelector(FocusablePresets.isSubscriptionList)
useContextMenuActionShortCutTrigger(items, when && isActive)
const inboxUnread = useUnreadById(inboxId)

View File

@ -28,6 +28,10 @@ export const useFocusableContainerRef = () => {
return use(FocusableContainerRefContext)
}
/**
* Performance issue, use `useGlobalFocusableScopeSelector` instead
* @deprecated use `useGlobalFocusableScopeSelector` instead
*/
export const useGlobalFocusableScope = () => {
return useAtomValue(use(GlobalFocusableContext))
}
@ -35,7 +39,9 @@ export const useGlobalFocusableScope = () => {
export const useGlobalFocusableHasScope = (scope: string) => {
return useGlobalFocusableScopeSelector(useCallback((v) => v.has(scope), [scope]))
}
export const useGlobalFocusableScopeSelector = (selector: (scope: Set<string>) => boolean) => {
export const useGlobalFocusableScopeSelector = (
selector: (scope: EnhanceSet<string>) => boolean,
) => {
const ctx = use(GlobalFocusableContext)
return useAtomValue(useMemo(() => selectAtom(ctx, selector), [ctx, selector]))

View File

@ -57,7 +57,6 @@ function Checkbox({ className, onCheckedChange, ...props }: CheckboxProps) {
opacity: 1,
transition: {
duration: 0.2,
delay: 0.2,
},
},
unchecked: {