diff --git a/apps/desktop/layer/renderer/src/constants/hotkeys.ts b/apps/desktop/layer/renderer/src/constants/hotkeys.ts index bf62b4f95..4365ba12f 100644 --- a/apps/desktop/layer/renderer/src/constants/hotkeys.ts +++ b/apps/desktop/layer/renderer/src/constants/hotkeys.ts @@ -8,4 +8,5 @@ export enum HotkeyScope { Timeline = "timeline", EntryRender = "entry-render", SubscriptionList = "subscription-list", + SubLayer = "sub-layer", } 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 5bc5ec81a..61920945a 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 @@ -39,7 +39,7 @@ import { EnvironmentIndicator } from "~/modules/app/EnvironmentIndicator" import { NetworkStatusIndicator } from "~/modules/app/NetworkStatusIndicator" import { LoginModalContent } from "~/modules/auth/LoginModalContent" import { COMMAND_ID } from "~/modules/command/commands/id" -import { useCommandBinding } from "~/modules/command/hooks/use-register-hotkey" +import { useCommandBinding } from "~/modules/command/hooks/use-command-binding" import { DebugRegistry } from "~/modules/debug/registry" import { CmdF } from "~/modules/panel/cmdf" import { SearchCmdK } from "~/modules/panel/cmdk" 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 f100dcdec..ba98e326d 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 @@ -6,10 +6,13 @@ import { ELECTRON_BUILD } from "@follow/shared/constants" import { springScrollTo } from "@follow/utils/scroller" import { cn, getOS } from "@follow/utils/utils" import { useEffect, useRef, useState } from "react" +import { useHotkeys } from "react-hotkeys-hook" import { useTranslation } from "react-i18next" import { NavigationType, Outlet, useLocation, useNavigate, useNavigationType } from "react-router" import { FABContainer, FABPortable } from "~/components/ui/fab" +import { HotkeyScope } from "~/constants" +import { useConditionalHotkeyScope } from "~/hooks/common" import { useSubViewTitleValue } from "./hooks" @@ -53,6 +56,16 @@ export function SubviewLayout() { // electron window has pt-[calc(var(--fo-window-padding-top)_-10px)] const isElectronWindows = ELECTRON_BUILD && getOS() === "Windows" + useConditionalHotkeyScope(HotkeyScope.SubLayer, true) + + const backHandler = () => { + if (prevLocation.pathname === location.pathname) { + navigate({ pathname: "" }) + } else { + navigate(prevLocation) + } + } + useHotkeys("esc", backHandler) return (
{ - if (prevLocation.pathname === location.pathname) { - navigate({ pathname: "" }) - } else { - navigate(prevLocation) - } - }} + onClick={backHandler} className="no-drag-region hover:text-accent inline-flex items-center gap-1 duration-200" > diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/id.ts b/apps/desktop/layer/renderer/src/modules/command/commands/id.ts index 629e08af5..db7ee28eb 100644 --- a/apps/desktop/layer/renderer/src/modules/command/commands/id.ts +++ b/apps/desktop/layer/renderer/src/modules/command/commands/id.ts @@ -48,12 +48,12 @@ export const COMMAND_ID = { toggleTimelineColumn: "layout:toggle-timeline-column", focusToTimeline: "layout:focus-to-timeline", focusToSubscription: "layout:focus-to-subscription", + focusToEntryRender: "layout:focus-to-entry-render", }, timeline: { switchToNext: "timeline:switch-to-next", switchToPrevious: "timeline:switch-to-previous", refetch: "timeline:refetch", - enter: "timeline:enter", }, entryRender: { scrollDown: "entry-render:scroll-down", diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx index 9c7aadd09..219631e65 100644 --- a/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx +++ b/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx @@ -10,6 +10,7 @@ declare module "@follow/utils/event-bus" { interface EventBusMap { "layout:focus-to-timeline": never "layout:focus-to-subscription": never + "layout:focus-to-entry-render": never } } @@ -36,6 +37,13 @@ export const useRegisterLayoutCommands = () => { EventBus.dispatch(COMMAND_ID.layout.focusToSubscription) }, }, + { + id: COMMAND_ID.layout.focusToEntryRender, + label: "Enter Selected Entry", + run: () => { + EventBus.dispatch(COMMAND_ID.layout.focusToEntryRender) + }, + }, ]) } @@ -53,7 +61,12 @@ export type FocusToTimelineCommand = Command<{ id: typeof COMMAND_ID.layout.focusToTimeline fn: () => void }> +export type FocusToEntryRenderCommand = Command<{ + id: typeof COMMAND_ID.layout.focusToEntryRender + fn: () => void +}> export type LayoutCommand = | ToggleTimelineColumnCommand | FocusToTimelineCommand | FocusToSubscriptionCommand + | FocusToEntryRenderCommand diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx index 3f6d9e809..2f3ee4fd1 100644 --- a/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx +++ b/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx @@ -36,13 +36,6 @@ export const useRegisterTimelineCommand = () => { EventBus.dispatch("timeline:refetch") }, }, - { - id: COMMAND_ID.timeline.enter, - label: "Enter Selected Entry", - run: () => { - EventBus.dispatch("timeline:enter") - }, - }, ]) } @@ -61,13 +54,7 @@ export type RefetchTimelineCommand = Command<{ fn: () => void }> -export type EnterTimelineCommand = Command<{ - id: typeof COMMAND_ID.timeline.enter - fn: () => void -}> - export type TimelineCommand = | SwitchToNextTimelineCommand | SwitchToPreviousTimelineCommand | RefetchTimelineCommand - | EnterTimelineCommand diff --git a/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-binding.ts b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-binding.ts new file mode 100644 index 000000000..d1631750a --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-binding.ts @@ -0,0 +1,19 @@ +import type { BindingCommandId } from "./use-command-shortcut" +import { useCommandShortcut } from "./use-command-shortcut" +import type { RegisterHotkeyOptions } from "./use-register-hotkey" +import { useCommandHotkey } from "./use-register-hotkey" + +export const useCommandBinding = ({ + commandId, + when = true, + args, +}: Omit, "shortcut">) => { + const commandShortcut = useCommandShortcut(commandId) + + return useCommandHotkey({ + shortcut: commandShortcut, + commandId, + when, + args, + }) +} diff --git a/apps/desktop/layer/renderer/src/modules/command/hooks/use-register-hotkey.ts b/apps/desktop/layer/renderer/src/modules/command/hooks/use-register-hotkey.ts index 89a19a151..261b2627e 100644 --- a/apps/desktop/layer/renderer/src/modules/command/hooks/use-register-hotkey.ts +++ b/apps/desktop/layer/renderer/src/modules/command/hooks/use-register-hotkey.ts @@ -4,13 +4,11 @@ import { tinykeys } from "tinykeys" import type { FollowCommand, FollowCommandId } from "../types" import { getCommand } from "./use-command" -import type { BindingCommandId } from "./use-command-shortcut" -import { useCommandShortcut } from "./use-command-shortcut" export interface HotkeyOptions { forceInputElement?: true } -interface RegisterHotkeyOptions { +export interface RegisterHotkeyOptions { shortcut: string commandId: T args?: Parameters["run"]> @@ -80,18 +78,3 @@ export const useCommandHotkey = ({ return tinykeys(document.documentElement, keyMap) }, [shortcut, commandId, when, argsRef, options?.forceInputElement]) } - -export const useCommandBinding = ({ - commandId, - when = true, - args, -}: Omit, "shortcut">) => { - const commandShortcut = useCommandShortcut(commandId) - - return useCommandHotkey({ - shortcut: commandShortcut, - commandId, - when, - args, - }) -} 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 3f1cc5528..cc64dc81d 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/EntryColumnShortcutHandler.tsx @@ -13,7 +13,8 @@ import { useConditionalHotkeyScope } from "~/hooks/common" import { useHotkeyScope } from "~/providers/hotkey-provider" import { COMMAND_ID } from "../command/commands/id" -import { useCommandBinding, useCommandHotkey } from "../command/hooks/use-register-hotkey" +import { useCommandBinding } from "../command/hooks/use-command-binding" +import { useCommandHotkey } from "../command/hooks/use-register-hotkey" export const EntryColumnShortcutHandler: FC<{ refetch: () => void @@ -43,7 +44,7 @@ export const EntryColumnShortcutHandler: FC<{ }) useCommandHotkey({ - commandId: COMMAND_ID.timeline.enter, + commandId: COMMAND_ID.layout.focusToEntryRender, shortcut: "Enter", when, }) diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/actions/header-actions.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/actions/header-actions.tsx index a76305d51..0255e0ef1 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/actions/header-actions.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/actions/header-actions.tsx @@ -5,7 +5,7 @@ import { CommandActionButton } from "~/components/ui/button/CommandActionButton" import { useHasModal } from "~/components/ui/modal/stacked/hooks" import { useSortedEntryActions } from "~/hooks/biz/useEntryActions" import { COMMAND_ID } from "~/modules/command/commands/id" -import { useCommandBinding } from "~/modules/command/hooks/use-register-hotkey" +import { useCommandBinding } from "~/modules/command/hooks/use-command-binding" import { useEntry } from "~/store/entry/hooks" export const EntryHeaderActions = ({ diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/index.electron.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/index.electron.tsx index 6155a9183..fb1188cd1 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/index.electron.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/index.electron.tsx @@ -34,7 +34,8 @@ import { useFeedById } from "~/store/feed" import { useInboxById } from "~/store/inbox" import { COMMAND_ID } from "../command/commands/id" -import { useCommandBinding, useCommandHotkey } from "../command/hooks/use-register-hotkey" +import { useCommandBinding } from "../command/hooks/use-command-binding" +import { useCommandHotkey } from "../command/hooks/use-register-hotkey" import { EntryContentHTMLRenderer } from "../renderer/html" import { AISummary } from "./AISummary" import { EntryTimelineSidebar } from "./components/EntryTimelineSidebar" @@ -235,6 +236,7 @@ const EntryScrollArea: Component<{ } return ( { + EventBus.subscribe(COMMAND_ID.layout.focusToEntryRender, () => { const $scroller = scrollerRef.current if ($scroller) { springScrollTo(0, $scroller) diff --git a/apps/desktop/layer/renderer/src/modules/timeline-column/FeedList.electron.tsx b/apps/desktop/layer/renderer/src/modules/timeline-column/FeedList.electron.tsx index fafcd4545..f725975c4 100644 --- a/apps/desktop/layer/renderer/src/modules/timeline-column/FeedList.electron.tsx +++ b/apps/desktop/layer/renderer/src/modules/timeline-column/FeedList.electron.tsx @@ -26,7 +26,8 @@ import { } from "~/store/subscription" import { COMMAND_ID } from "../command/commands/id" -import { useCommandBinding, useCommandHotkey } from "../command/hooks/use-register-hotkey" +import { useCommandBinding } from "../command/hooks/use-command-binding" +import { useCommandHotkey } from "../command/hooks/use-register-hotkey" import { useIsPreviewFeed } from "../entry-column/hooks/useIsPreviewFeed" import { resetSelectedFeedIds, diff --git a/apps/desktop/layer/renderer/src/modules/timeline-column/index.tsx b/apps/desktop/layer/renderer/src/modules/timeline-column/index.tsx index e56f7d533..79cd26853 100644 --- a/apps/desktop/layer/renderer/src/modules/timeline-column/index.tsx +++ b/apps/desktop/layer/renderer/src/modules/timeline-column/index.tsx @@ -27,7 +27,7 @@ import { useHotkeyScope } from "~/providers/hotkey-provider" import { WindowUnderBlur } from "../../components/ui/background" import { COMMAND_ID } from "../command/commands/id" -import { useCommandBinding } from "../command/hooks/use-register-hotkey" +import { useCommandBinding } from "../command/hooks/use-command-binding" import { getSelectedFeedIds, resetSelectedFeedIds, setSelectedFeedIds } from "./atom" import { useShouldFreeUpSpace } from "./hook" import { TimelineColumnHeader } from "./TimelineColumnHeader" diff --git a/apps/desktop/layer/renderer/src/providers/global-hotkeys-provider.tsx b/apps/desktop/layer/renderer/src/providers/global-hotkeys-provider.tsx index d8e7971d3..5afe02854 100644 --- a/apps/desktop/layer/renderer/src/providers/global-hotkeys-provider.tsx +++ b/apps/desktop/layer/renderer/src/providers/global-hotkeys-provider.tsx @@ -1,10 +1,13 @@ import { highlightElement } from "@follow/components/common/Focusable/utils.js" import { nextFrame } from "@follow/utils/dom" +import { EventBus } from "@follow/utils/event-bus" +import { useEffect } from "react" +import { tinykeys } from "tinykeys" import { useEventListener } from "usehooks-ts" import { HotkeyScope } from "~/constants/hotkeys" import { COMMAND_ID } from "~/modules/command/commands/id" -import { useCommandBinding } from "~/modules/command/hooks/use-register-hotkey" +import { useCommandBinding } from "~/modules/command/hooks/use-command-binding" import { useHotkeyScope } from "./hotkey-provider" @@ -31,5 +34,36 @@ export const GlobalHotkeysProvider = () => { } }) + // Re force to sidebar focusable + useEventListener("focusin", (e) => { + if ( + activeScopes.length === 1 && + activeScopes[0] === HotkeyScope.Home && + e.target === document.body + ) { + EventBus.dispatch(COMMAND_ID.layout.focusToTimeline) + } + }) + // Re force to sidebar focusable + useEventListener("focusout", () => { + const { activeElement } = document + if ( + activeElement === document.body && + activeScopes.length === 1 && + activeScopes[0] === HotkeyScope.Home + ) { + EventBus.dispatch(COMMAND_ID.layout.focusToTimeline) + } + }) + + // Show current focused element + useEffect(() => { + return tinykeys(window, { + "$mod+Period": () => { + highlightElement(document.activeElement as HTMLElement) + }, + }) + }, []) + return null } diff --git a/packages/internal/components/src/ui/scroll-area/ScrollArea.tsx b/packages/internal/components/src/ui/scroll-area/ScrollArea.tsx index 2c1964647..682d4ef36 100644 --- a/packages/internal/components/src/ui/scroll-area/ScrollArea.tsx +++ b/packages/internal/components/src/ui/scroll-area/ScrollArea.tsx @@ -75,9 +75,11 @@ const Viewport = ({ ref: forwardedRef, className, mask = false, + focusable = true, ...rest }: React.ComponentPropsWithoutRef & { mask?: boolean + focusable?: boolean } & { ref?: React.Ref | null> }) => { const ref = React.useRef(null) const [shouldAddMask, setShouldAddMask] = React.useState(false) @@ -105,7 +107,7 @@ const Viewport = ({ ) @@ -143,6 +145,8 @@ export const ScrollArea = ({ onScroll, orientation = "vertical", asChild = false, + + focusable = true, }: React.PropsWithChildren & { rootClassName?: string viewportClassName?: string @@ -152,6 +156,7 @@ export const ScrollArea = ({ onScroll?: (e: React.UIEvent) => void orientation?: "vertical" | "horizontal" asChild?: boolean + focusable?: boolean } & { ref?: React.Ref }) => { const [viewportRef, setViewportRef] = React.useState(null) React.useImperativeHandle(ref, () => viewportRef as HTMLDivElement) @@ -166,6 +171,7 @@ export const ScrollArea = ({ mask={mask} asChild={asChild} onScroll={onScroll} + focusable={focusable} > {children}