From e374dd64735d89c01465ff4987f254b2129db4b8 Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 27 May 2025 23:01:37 +0800 Subject: [PATCH] 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 --- .../modules/app-layout/feed-column/desktop.tsx | 9 ++++++--- .../modules/app-layout/subview/index.electron.tsx | 6 +++--- .../entry-column/EntryColumnShortcutHandler.tsx | 6 ++---- .../entry-column/components/mark-all-button.tsx | 15 +++++++++++---- .../entry-column/layouts/EntryItemWrapper.tsx | 6 +++--- .../modules/settings/helper/EnhancedIndicator.tsx | 2 ++ .../layer/renderer/src/modules/settings/utils.ts | 1 + .../src/modules/subscription-column/FeedItem.tsx | 14 +++++++------- .../components/src/common/Focusable/hooks.ts | 8 +++++++- .../internal/components/src/ui/checkbox/index.tsx | 1 - 10 files changed, 42 insertions(+), 26 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/app-layout/feed-column/desktop.tsx b/apps/desktop/layer/renderer/src/modules/app-layout/feed-column/desktop.tsx index 8b28b5be5..0500bf42f 100644 --- a/apps/desktop/layer/renderer/src/modules/app-layout/feed-column/desktop.tsx +++ b/apps/desktop/layer/renderer/src/modules/app-layout/feed-column/desktop.tsx @@ -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) diff --git a/apps/desktop/layer/renderer/src/modules/app-layout/subview/index.electron.tsx b/apps/desktop/layer/renderer/src/modules/app-layout/subview/index.electron.tsx index a6f5ae4a0..619d1a1d9 100644 --- a/apps/desktop/layer/renderer/src/modules/app-layout/subview/index.electron.tsx +++ b/apps/desktop/layer/renderer/src/modules/app-layout/subview/index.electron.tsx @@ -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 (
diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx index 43f362836..1efdb3204 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx @@ -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, diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/components/mark-all-button.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/components/mark-all-button.tsx index cbfa4bc50..ade048a87 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/components/mark-all-button.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/components/mark-all-button.tsx @@ -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(() => { diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryItemWrapper.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryItemWrapper.tsx index d90288586..77057359a 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryItemWrapper.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryItemWrapper.tsx @@ -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") diff --git a/apps/desktop/layer/renderer/src/modules/settings/helper/EnhancedIndicator.tsx b/apps/desktop/layer/renderer/src/modules/settings/helper/EnhancedIndicator.tsx index 129a20002..c316f6fc6 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/helper/EnhancedIndicator.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/helper/EnhancedIndicator.tsx @@ -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 ( diff --git a/apps/desktop/layer/renderer/src/modules/settings/utils.ts b/apps/desktop/layer/renderer/src/modules/settings/utils.ts index 4b79314e1..2aafb53e4 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/utils.ts +++ b/apps/desktop/layer/renderer/src/modules/settings/utils.ts @@ -21,6 +21,7 @@ export interface SettingPageConfig { ctx: SettingPageContext, serverConfigs?: ServerConfigs | null, ) => [boolean, DisableWhy] + viewportClassName?: string } export const defineSettingPageData = (config: SettingPageConfig) => ({ ...config, diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/FeedItem.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/FeedItem.tsx index f7eb6d7d6..b566bb80e 100644 --- a/apps/desktop/layer/renderer/src/modules/subscription-column/FeedItem.tsx +++ b/apps/desktop/layer/renderer/src/modules/subscription-column/FeedItem.tsx @@ -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 = ({ 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 = ({ 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) diff --git a/packages/internal/components/src/common/Focusable/hooks.ts b/packages/internal/components/src/common/Focusable/hooks.ts index 4fbdc541c..1e3163506 100644 --- a/packages/internal/components/src/common/Focusable/hooks.ts +++ b/packages/internal/components/src/common/Focusable/hooks.ts @@ -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) => boolean) => { +export const useGlobalFocusableScopeSelector = ( + selector: (scope: EnhanceSet) => boolean, +) => { const ctx = use(GlobalFocusableContext) return useAtomValue(useMemo(() => selectAtom(ctx, selector), [ctx, selector])) diff --git a/packages/internal/components/src/ui/checkbox/index.tsx b/packages/internal/components/src/ui/checkbox/index.tsx index 571651d33..cdec7eea1 100644 --- a/packages/internal/components/src/ui/checkbox/index.tsx +++ b/packages/internal/components/src/ui/checkbox/index.tsx @@ -57,7 +57,6 @@ function Checkbox({ className, onCheckedChange, ...props }: CheckboxProps) { opacity: 1, transition: { duration: 0.2, - delay: 0.2, }, }, unchecked: {