>
)
if (isMobile) {
diff --git a/apps/desktop/layer/renderer/src/modules/boost/boost-progress.tsx b/apps/desktop/layer/renderer/src/modules/boost/boost-progress.tsx
index 2092db5fe..e915b2d7c 100644
--- a/apps/desktop/layer/renderer/src/modules/boost/boost-progress.tsx
+++ b/apps/desktop/layer/renderer/src/modules/boost/boost-progress.tsx
@@ -24,7 +24,7 @@ export const BoostProgress = ({
const nextLevel = level + 1
return (
-
diff --git a/apps/desktop/layer/renderer/src/modules/command/command-button.test-d.ts b/apps/desktop/layer/renderer/src/modules/command/command-button.test-d.ts
index d2d611c93..3cc5d02fa 100644
--- a/apps/desktop/layer/renderer/src/modules/command/command-button.test-d.ts
+++ b/apps/desktop/layer/renderer/src/modules/command/command-button.test-d.ts
@@ -1,8 +1,8 @@
import { assertType, test } from "vitest"
import { CommandActionButton, CommandIdButton } from "./command-button"
+import type { OpenInBrowserCommand, TipCommand } from "./commands/entry"
import { COMMAND_ID } from "./commands/id"
-import type { OpenInBrowserCommand, TipCommand } from "./commands/types"
test("CommandActionButton types", () => {
const mockCommand = {} as OpenInBrowserCommand
diff --git a/apps/desktop/layer/renderer/src/modules/command/command-manager.ts b/apps/desktop/layer/renderer/src/modules/command/command-manager.ts
index 63eb4b674..0c5d34957 100644
--- a/apps/desktop/layer/renderer/src/modules/command/command-manager.ts
+++ b/apps/desktop/layer/renderer/src/modules/command/command-manager.ts
@@ -1,16 +1,22 @@
import { useRegisterEntryCommands } from "./commands/entry"
+import { useRegisterEntryRenderCommand } from "./commands/entry-render"
+import { useRegisterGlobalCommands } from "./commands/global"
import { useRegisterIntegrationCommands } from "./commands/integration"
+import { useRegisterLayoutCommands } from "./commands/layout"
import { useRegisterListCommands } from "./commands/list"
import { useRegisterSettingsCommands } from "./commands/settings"
+import { useRegisterSubscriptionCommands } from "./commands/subscription"
+import { useRegisterTimelineCommand } from "./commands/timeline"
-export function useRegisterFollowCommands() {
+export const FollowCommandManager = () => {
useRegisterSettingsCommands()
useRegisterListCommands()
useRegisterEntryCommands()
useRegisterIntegrationCommands()
-}
-
-export const FollowCommandManager = () => {
- useRegisterFollowCommands()
+ useRegisterGlobalCommands()
+ useRegisterLayoutCommands()
+ useRegisterTimelineCommand()
+ useRegisterEntryRenderCommand()
+ useRegisterSubscriptionCommands()
return null
}
diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/entry-render.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/entry-render.tsx
new file mode 100644
index 000000000..29682f14f
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/command/commands/entry-render.tsx
@@ -0,0 +1,81 @@
+import { EventBus } from "@follow/utils/event-bus"
+
+import { useRegisterCommandEffect } from "../hooks/use-register-command"
+import type { Command } from "../types"
+import { COMMAND_ID } from "./id"
+
+declare module "@follow/utils/event-bus" {
+ interface EventBusMap {
+ "entry-render:scroll-down": never
+ "entry-render:scroll-up": never
+ "entry-render:next-entry": never
+ "entry-render:previous-entry": never
+ }
+}
+const LABEL_PREFIX = "Entry Render"
+
+const category = "follow:entry-render"
+export const useRegisterEntryRenderCommand = () => {
+ useRegisterCommandEffect([
+ {
+ id: COMMAND_ID.entryRender.scrollDown,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.entryRender.scrollDown)
+ },
+ category,
+ label: `${LABEL_PREFIX}: Scroll down`,
+ },
+ {
+ id: COMMAND_ID.entryRender.scrollUp,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.entryRender.scrollUp)
+ },
+ category,
+ label: `${LABEL_PREFIX}: Scroll up`,
+ },
+ {
+ id: COMMAND_ID.entryRender.nextEntry,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.timeline.switchToNext)
+ EventBus.dispatch(COMMAND_ID.entryRender.nextEntry)
+ },
+ category,
+ label: `${LABEL_PREFIX}: Next entry`,
+ },
+ {
+ id: COMMAND_ID.entryRender.previousEntry,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.timeline.switchToPrevious)
+ EventBus.dispatch(COMMAND_ID.entryRender.previousEntry)
+ },
+ category,
+ label: `${LABEL_PREFIX}: Previous entry`,
+ },
+ ])
+}
+
+type EntryScrollDownCommand = Command<{
+ id: typeof COMMAND_ID.entryRender.scrollDown
+ fn: () => void
+}>
+
+type EntryScrollUpCommand = Command<{
+ id: typeof COMMAND_ID.entryRender.scrollUp
+ fn: () => void
+}>
+
+type EntryNextEntryCommand = Command<{
+ id: typeof COMMAND_ID.entryRender.nextEntry
+ fn: () => void
+}>
+
+type EntryPreviousEntryCommand = Command<{
+ id: typeof COMMAND_ID.entryRender.previousEntry
+ fn: () => void
+}>
+
+export type EntryRenderCommand =
+ | EntryScrollDownCommand
+ | EntryScrollUpCommand
+ | EntryNextEntryCommand
+ | EntryPreviousEntryCommand
diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/entry.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/entry.tsx
index ad6654795..f0eb7d157 100644
--- a/apps/desktop/layer/renderer/src/modules/command/commands/entry.tsx
+++ b/apps/desktop/layer/renderer/src/modules/command/commands/entry.tsx
@@ -28,6 +28,7 @@ import { useTipModal } from "~/modules/wallet/hooks"
import { entryActions, useEntryStore } from "~/store/entry"
import { useRegisterFollowCommand } from "../hooks/use-register-command"
+import type { Command } from "../types"
import { COMMAND_ID } from "./id"
const useCollect = () => {
@@ -421,3 +422,107 @@ export const useRegisterEntryCommands = () => {
},
)
}
+
+export type TipCommand = Command<{
+ id: typeof COMMAND_ID.entry.tip
+ fn: (data: { userId?: string | null; feedId?: string; entryId?: string }) => void
+}>
+
+export type StarCommand = Command<{
+ id: typeof COMMAND_ID.entry.star
+ fn: (data: { entryId: string; view?: FeedViewType }) => void
+}>
+
+export type DeleteCommand = Command<{
+ id: typeof COMMAND_ID.entry.delete
+ fn: (data: { entryId: string }) => void
+}>
+
+export type CopyLinkCommand = Command<{
+ id: typeof COMMAND_ID.entry.copyLink
+ fn: (data: { entryId: string }) => void
+}>
+
+export type ExportAsPDFCommand = Command<{
+ id: typeof COMMAND_ID.entry.exportAsPDF
+ fn: (data: { entryId: string }) => void
+}>
+
+export type CopyTitleCommand = Command<{
+ id: typeof COMMAND_ID.entry.copyTitle
+ fn: (data: { entryId: string }) => void
+}>
+
+export type OpenInBrowserCommand = Command<{
+ id: typeof COMMAND_ID.entry.openInBrowser
+ fn: (data: { entryId: string }) => void
+}>
+
+export type ViewSourceContentCommand = Command<{
+ id: typeof COMMAND_ID.entry.viewSourceContent
+ fn: (data: { entryId: string; siteUrl?: string | null | undefined }) => void
+}>
+
+export type ShareCommand = Command<{
+ id: typeof COMMAND_ID.entry.share
+ fn: (data: { entryId: string }) => void
+}>
+
+export type ReadCommand = Command<{
+ id: typeof COMMAND_ID.entry.read
+ fn: (data: { entryId: string }) => void
+}>
+
+export type ReadAboveCommand = Command<{
+ id: typeof COMMAND_ID.entry.readAbove
+ fn: (data: { publishedAt: string }) => void
+}>
+
+export type ReadBelowCommand = Command<{
+ id: typeof COMMAND_ID.entry.readBelow
+ fn: (data: { publishedAt: string }) => void
+}>
+
+export type ToggleAISummaryCommand = Command<{
+ id: typeof COMMAND_ID.entry.toggleAISummary
+ fn: () => void
+}>
+
+export type ToggleAITranslationCommand = Command<{
+ id: typeof COMMAND_ID.entry.toggleAITranslation
+ fn: () => void
+}>
+
+export type ImageGalleryCommand = Command<{
+ id: typeof COMMAND_ID.entry.imageGallery
+ fn: (data: { entryId: string }) => void
+}>
+
+export type TTSCommand = Command<{
+ id: typeof COMMAND_ID.entry.tts
+ fn: (data: { entryId: string; entryContent: string }) => void
+}>
+
+export type ReadabilityCommand = Command<{
+ id: typeof COMMAND_ID.entry.readability
+ fn: (data: { entryId: string; entryUrl: string }) => void
+}>
+
+export type EntryCommand =
+ | TipCommand
+ | StarCommand
+ | DeleteCommand
+ | CopyLinkCommand
+ | ExportAsPDFCommand
+ | CopyTitleCommand
+ | OpenInBrowserCommand
+ | ViewSourceContentCommand
+ | ShareCommand
+ | ReadCommand
+ | ReadAboveCommand
+ | ReadBelowCommand
+ | ToggleAISummaryCommand
+ | ToggleAITranslationCommand
+ | ImageGalleryCommand
+ | TTSCommand
+ | ReadabilityCommand
diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/global.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/global.tsx
new file mode 100644
index 000000000..60fd1a3d2
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/command/commands/global.tsx
@@ -0,0 +1,26 @@
+import { useShortcutsModal } from "~/modules/modal/shortcuts"
+
+import { useRegisterCommandEffect } from "../hooks/use-register-command"
+import type { Command } from "../types"
+import { COMMAND_ID } from "./id"
+
+export const useRegisterGlobalCommands = () => {
+ const showShortcuts = useShortcutsModal()
+
+ useRegisterCommandEffect([
+ {
+ id: COMMAND_ID.global.showShortcuts,
+ label: "Show shortcuts",
+ run: () => {
+ showShortcuts()
+ },
+ },
+ ])
+}
+
+export type ShowShortcutsCommand = Command<{
+ id: typeof COMMAND_ID.global.showShortcuts
+ fn: () => void
+}>
+
+export type GlobalCommand = ShowShortcutsCommand
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 5790d9a24..629e08af5 100644
--- a/apps/desktop/layer/renderer/src/modules/command/commands/id.ts
+++ b/apps/desktop/layer/renderer/src/modules/command/commands/id.ts
@@ -41,4 +41,39 @@ export const COMMAND_ID = {
changeThemeToLight: "follow:change-color-mode-to-light",
customizeToolbar: "follow:customize-toolbar",
},
+ global: {
+ showShortcuts: "global:show-shortcuts",
+ },
+ layout: {
+ toggleTimelineColumn: "layout:toggle-timeline-column",
+ focusToTimeline: "layout:focus-to-timeline",
+ focusToSubscription: "layout:focus-to-subscription",
+ },
+ timeline: {
+ switchToNext: "timeline:switch-to-next",
+ switchToPrevious: "timeline:switch-to-previous",
+ refetch: "timeline:refetch",
+ enter: "timeline:enter",
+ },
+ entryRender: {
+ scrollDown: "entry-render:scroll-down",
+ scrollUp: "entry-render:scroll-up",
+ nextEntry: "entry-render:next-entry",
+ previousEntry: "entry-render:previous-entry",
+ },
+ subscription: {
+ switchTabToNext: "subscription:switch-tab-to-next",
+ switchTabToPrevious: "subscription:switch-tab-to-previous",
+ switchTabToArticle: "subscription:switch-tab-to-article",
+ switchTabToSocial: "subscription:switch-tab-to-social",
+ switchTabToPicture: "subscription:switch-tab-to-picture",
+ switchTabToVideo: "subscription:switch-tab-to-video",
+ switchTabToAudio: "subscription:switch-tab-to-audio",
+ switchTabToNotification: "subscription:switch-tab-to-notification",
+
+ nextSubscription: "subscription:next",
+ previousSubscription: "subscription:previous",
+
+ toggleFolderCollapse: "subscription:toggle-folder-collapse",
+ },
} as const
diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/integration.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/integration.tsx
index 508b97e4b..7895ad87e 100644
--- a/apps/desktop/layer/renderer/src/modules/command/commands/integration.tsx
+++ b/apps/desktop/layer/renderer/src/modules/command/commands/integration.tsx
@@ -28,6 +28,7 @@ import { useEntryStore } from "~/store/entry"
import { useRegisterCommandEffect } from "../hooks/use-register-command"
import { defineFollowCommand } from "../registry/command"
+import type { Command } from "../types"
import { COMMAND_ID } from "./id"
export const useRegisterIntegrationCommands = () => {
@@ -558,3 +559,47 @@ const buildMemoRequestBody = (entry: FlatEntryModel, selectedText: string) => {
source_url: entry.entries.url,
}
}
+
+export type SaveToEagleCommand = Command<{
+ id: typeof COMMAND_ID.integration.saveToEagle
+ fn: (payload: { entryId: string }) => void
+}>
+
+export type SaveToReadwiseCommand = Command<{
+ id: typeof COMMAND_ID.integration.saveToReadwise
+ fn: (payload: { entryId: string }) => void
+}>
+
+export type SaveToInstapaperCommand = Command<{
+ id: typeof COMMAND_ID.integration.saveToInstapaper
+ fn: (payload: { entryId: string }) => void
+}>
+
+export type SaveToObsidianCommand = Command<{
+ id: typeof COMMAND_ID.integration.saveToObsidian
+ fn: (payload: { entryId: string }) => void
+}>
+
+export type SaveToOutlineCommand = Command<{
+ id: typeof COMMAND_ID.integration.saveToOutline
+ fn: (payload: { entryId: string }) => void
+}>
+
+export type SaveToReadeckCommand = Command<{
+ id: typeof COMMAND_ID.integration.saveToReadeck
+ fn: (payload: { entryId: string }) => void
+}>
+
+export type SaveToCuboxCommand = Command<{
+ id: typeof COMMAND_ID.integration.saveToCubox
+ fn: (payload: { entryId: string }) => void
+}>
+
+export type IntegrationCommand =
+ | SaveToEagleCommand
+ | SaveToReadwiseCommand
+ | SaveToInstapaperCommand
+ | SaveToObsidianCommand
+ | SaveToOutlineCommand
+ | SaveToReadeckCommand
+ | SaveToCuboxCommand
diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx
new file mode 100644
index 000000000..9c7aadd09
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx
@@ -0,0 +1,59 @@
+import { EventBus } from "@follow/utils/event-bus"
+
+import { setTimelineColumnShow } from "~/atoms/sidebar"
+
+import { useRegisterCommandEffect } from "../hooks/use-register-command"
+import type { Command } from "../types"
+import { COMMAND_ID } from "./id"
+
+declare module "@follow/utils/event-bus" {
+ interface EventBusMap {
+ "layout:focus-to-timeline": never
+ "layout:focus-to-subscription": never
+ }
+}
+
+export const useRegisterLayoutCommands = () => {
+ useRegisterCommandEffect([
+ {
+ id: COMMAND_ID.layout.toggleTimelineColumn,
+ label: "Toggle timeline column",
+ run: () => {
+ setTimelineColumnShow((show) => !show)
+ },
+ },
+ {
+ id: COMMAND_ID.layout.focusToTimeline,
+ label: "Focus to timeline",
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.layout.focusToTimeline)
+ },
+ },
+ {
+ id: COMMAND_ID.layout.focusToSubscription,
+ label: "Focus to subscription",
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.layout.focusToSubscription)
+ },
+ },
+ ])
+}
+
+export type FocusToSubscriptionCommand = Command<{
+ id: typeof COMMAND_ID.layout.focusToSubscription
+ fn: () => void
+}>
+
+export type ToggleTimelineColumnCommand = Command<{
+ id: typeof COMMAND_ID.layout.toggleTimelineColumn
+ fn: () => void
+}>
+
+export type FocusToTimelineCommand = Command<{
+ id: typeof COMMAND_ID.layout.focusToTimeline
+ fn: () => void
+}>
+export type LayoutCommand =
+ | ToggleTimelineColumnCommand
+ | FocusToTimelineCommand
+ | FocusToSubscriptionCommand
diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/settings.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/settings.tsx
index 2dc753a12..6231b0992 100644
--- a/apps/desktop/layer/renderer/src/modules/command/commands/settings.tsx
+++ b/apps/desktop/layer/renderer/src/modules/command/commands/settings.tsx
@@ -5,6 +5,7 @@ import { useSetTheme } from "~/hooks/common"
import { useShowCustomizeToolbarModal } from "~/modules/customize-toolbar/modal"
import { useRegisterCommandEffect } from "../hooks/use-register-command"
+import type { Command } from "../types"
import { COMMAND_ID } from "./id"
export const useRegisterSettingsCommands = () => {
@@ -66,3 +67,10 @@ const useRegisterThemeCommands = () => {
},
])
}
+
+export type CustomizeToolbarCommand = Command<{
+ id: typeof COMMAND_ID.settings.customizeToolbar
+ fn: () => void
+}>
+
+export type SettingsCommand = CustomizeToolbarCommand
diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/subscription.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/subscription.tsx
new file mode 100644
index 000000000..ba93f3cdd
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/command/commands/subscription.tsx
@@ -0,0 +1,172 @@
+import { EventBus } from "@follow/utils/event-bus"
+
+import { useRegisterCommandEffect } from "../hooks/use-register-command"
+import type { Command } from "../types"
+import { COMMAND_ID } from "./id"
+
+declare module "@follow/utils/event-bus" {
+ interface EventBusMap {
+ "subscription:switch-tab-to-next": never
+ "subscription:switch-tab-to-previous": never
+ "subscription:switch-tab-to-article": never
+ "subscription:switch-tab-to-social": never
+ "subscription:switch-tab-to-picture": never
+ "subscription:switch-tab-to-video": never
+ "subscription:switch-tab-to-audio": never
+ "subscription:switch-tab-to-notification": never
+
+ "subscription:next": never
+ "subscription:previous": never
+ "subscription:toggle-folder-collapse": never
+ }
+}
+const LABEL_PREFIX = "Subscription"
+export const useRegisterSubscriptionCommands = () => {
+ useRegisterCommandEffect([
+ {
+ id: COMMAND_ID.subscription.switchTabToNext,
+ label: `${LABEL_PREFIX}: Switch to next tab`,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.subscription.switchTabToNext)
+ },
+ },
+ {
+ id: COMMAND_ID.subscription.switchTabToPrevious,
+ label: `${LABEL_PREFIX}: Switch to previous tab`,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.subscription.switchTabToPrevious)
+ },
+ },
+ {
+ id: COMMAND_ID.subscription.switchTabToArticle,
+ label: `${LABEL_PREFIX}: Switch to article tab`,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.subscription.switchTabToArticle)
+ },
+ },
+ {
+ id: COMMAND_ID.subscription.switchTabToSocial,
+ label: `${LABEL_PREFIX}: Switch to social tab`,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.subscription.switchTabToSocial)
+ },
+ },
+ {
+ id: COMMAND_ID.subscription.switchTabToPicture,
+ label: `${LABEL_PREFIX}: Switch to picture tab`,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.subscription.switchTabToPicture)
+ },
+ },
+ {
+ id: COMMAND_ID.subscription.switchTabToVideo,
+ label: `${LABEL_PREFIX}: Switch to video tab`,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.subscription.switchTabToVideo)
+ },
+ },
+ {
+ id: COMMAND_ID.subscription.switchTabToAudio,
+ label: `${LABEL_PREFIX}: Switch to audio tab`,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.subscription.switchTabToAudio)
+ },
+ },
+ {
+ id: COMMAND_ID.subscription.switchTabToNotification,
+ label: `${LABEL_PREFIX}: Switch to notification tab`,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.subscription.switchTabToNotification)
+ },
+ },
+ {
+ id: COMMAND_ID.subscription.nextSubscription,
+ label: `${LABEL_PREFIX}: Next Subscription`,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.subscription.nextSubscription)
+ },
+ },
+ {
+ id: COMMAND_ID.subscription.previousSubscription,
+ label: `${LABEL_PREFIX}: Previous Subscription`,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.subscription.previousSubscription)
+ },
+ },
+ {
+ id: COMMAND_ID.subscription.toggleFolderCollapse,
+ label: `${LABEL_PREFIX}: Toggle Folder Collapse`,
+ run: () => {
+ EventBus.dispatch(COMMAND_ID.subscription.toggleFolderCollapse)
+ },
+ },
+ ])
+}
+
+type SwitchTabToNextCommand = Command<{
+ id: typeof COMMAND_ID.subscription.switchTabToNext
+ fn: () => void
+}>
+
+type SwitchTabToPreviousCommand = Command<{
+ id: typeof COMMAND_ID.subscription.switchTabToPrevious
+ fn: () => void
+}>
+
+type SwitchTabToArticleCommand = Command<{
+ id: typeof COMMAND_ID.subscription.switchTabToArticle
+ fn: () => void
+}>
+
+type SwitchTabToSocialCommand = Command<{
+ id: typeof COMMAND_ID.subscription.switchTabToSocial
+ fn: () => void
+}>
+
+type SwitchTabToPictureCommand = Command<{
+ id: typeof COMMAND_ID.subscription.switchTabToPicture
+ fn: () => void
+}>
+
+type SwitchTabToVideoCommand = Command<{
+ id: typeof COMMAND_ID.subscription.switchTabToVideo
+ fn: () => void
+}>
+
+type SwitchTabToAudioCommand = Command<{
+ id: typeof COMMAND_ID.subscription.switchTabToAudio
+ fn: () => void
+}>
+
+type SwitchTabToNotificationCommand = Command<{
+ id: typeof COMMAND_ID.subscription.switchTabToNotification
+ fn: () => void
+}>
+
+type NextSubscriptionCommand = Command<{
+ id: typeof COMMAND_ID.subscription.nextSubscription
+ fn: () => void
+}>
+
+type PreviousSubscriptionCommand = Command<{
+ id: typeof COMMAND_ID.subscription.previousSubscription
+ fn: () => void
+}>
+
+type ToggleFolderCollapseCommand = Command<{
+ id: typeof COMMAND_ID.subscription.toggleFolderCollapse
+ fn: () => void
+}>
+
+export type SubscriptionCommand =
+ | SwitchTabToNextCommand
+ | SwitchTabToPreviousCommand
+ | SwitchTabToArticleCommand
+ | SwitchTabToSocialCommand
+ | SwitchTabToPictureCommand
+ | SwitchTabToVideoCommand
+ | SwitchTabToAudioCommand
+ | SwitchTabToNotificationCommand
+ | NextSubscriptionCommand
+ | PreviousSubscriptionCommand
+ | ToggleFolderCollapseCommand
diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx
new file mode 100644
index 000000000..3f6d9e809
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx
@@ -0,0 +1,73 @@
+import { EventBus } from "@follow/utils/event-bus"
+
+import { useRegisterCommandEffect } from "../hooks/use-register-command"
+import type { Command } from "../types"
+import { COMMAND_ID } from "./id"
+
+declare module "@follow/utils/event-bus" {
+ interface EventBusMap {
+ "timeline:switch-to-next": never
+ "timeline:switch-to-previous": never
+ "timeline:refetch": never
+ "timeline:enter": never
+ }
+}
+export const useRegisterTimelineCommand = () => {
+ useRegisterCommandEffect([
+ {
+ id: COMMAND_ID.timeline.switchToNext,
+ label: "Switch to next timeline",
+
+ run: () => {
+ EventBus.dispatch("timeline:switch-to-next")
+ },
+ },
+ {
+ id: COMMAND_ID.timeline.switchToPrevious,
+ label: "Switch to previous timeline",
+ run: () => {
+ EventBus.dispatch("timeline:switch-to-previous")
+ },
+ },
+ {
+ id: COMMAND_ID.timeline.refetch,
+ label: "Refetch timeline",
+ run: () => {
+ EventBus.dispatch("timeline:refetch")
+ },
+ },
+ {
+ id: COMMAND_ID.timeline.enter,
+ label: "Enter Selected Entry",
+ run: () => {
+ EventBus.dispatch("timeline:enter")
+ },
+ },
+ ])
+}
+
+export type SwitchToNextTimelineCommand = Command<{
+ id: typeof COMMAND_ID.timeline.switchToNext
+ fn: () => void
+}>
+
+export type SwitchToPreviousTimelineCommand = Command<{
+ id: typeof COMMAND_ID.timeline.switchToPrevious
+ fn: () => void
+}>
+
+export type RefetchTimelineCommand = Command<{
+ id: typeof COMMAND_ID.timeline.refetch
+ 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/commands/types.ts b/apps/desktop/layer/renderer/src/modules/command/commands/types.ts
index 17bd050aa..1db475a15 100644
--- a/apps/desktop/layer/renderer/src/modules/command/commands/types.ts
+++ b/apps/desktop/layer/renderer/src/modules/command/commands/types.ts
@@ -1,167 +1,20 @@
// Entry commands
-import type { FeedViewType } from "@follow/constants"
+import type { EntryCommand } from "./entry"
+import type { EntryRenderCommand } from "./entry-render"
+import type { GlobalCommand } from "./global"
+import type { IntegrationCommand } from "./integration"
+import type { LayoutCommand } from "./layout"
+import type { SettingsCommand } from "./settings"
+import type { SubscriptionCommand } from "./subscription"
+import type { TimelineCommand } from "./timeline"
-import type { Command } from "../types"
-import type { COMMAND_ID } from "./id"
-
-export type TipCommand = Command<{
- id: typeof COMMAND_ID.entry.tip
- fn: (data: { userId?: string | null; feedId?: string; entryId?: string }) => void
-}>
-
-export type StarCommand = Command<{
- id: typeof COMMAND_ID.entry.star
- fn: (data: { entryId: string; view?: FeedViewType }) => void
-}>
-
-export type DeleteCommand = Command<{
- id: typeof COMMAND_ID.entry.delete
- fn: (data: { entryId: string }) => void
-}>
-
-export type CopyLinkCommand = Command<{
- id: typeof COMMAND_ID.entry.copyLink
- fn: (data: { entryId: string }) => void
-}>
-
-export type ExportAsPDFCommand = Command<{
- id: typeof COMMAND_ID.entry.exportAsPDF
- fn: (data: { entryId: string }) => void
-}>
-
-export type CopyTitleCommand = Command<{
- id: typeof COMMAND_ID.entry.copyTitle
- fn: (data: { entryId: string }) => void
-}>
-
-export type OpenInBrowserCommand = Command<{
- id: typeof COMMAND_ID.entry.openInBrowser
- fn: (data: { entryId: string }) => void
-}>
-
-export type ViewSourceContentCommand = Command<{
- id: typeof COMMAND_ID.entry.viewSourceContent
- fn: (data: { entryId: string; siteUrl?: string | null | undefined }) => void
-}>
-
-export type ShareCommand = Command<{
- id: typeof COMMAND_ID.entry.share
- fn: (data: { entryId: string }) => void
-}>
-
-export type ReadCommand = Command<{
- id: typeof COMMAND_ID.entry.read
- fn: (data: { entryId: string }) => void
-}>
-
-export type ReadAboveCommand = Command<{
- id: typeof COMMAND_ID.entry.readAbove
- fn: (data: { publishedAt: string }) => void
-}>
-
-export type ReadBelowCommand = Command<{
- id: typeof COMMAND_ID.entry.readBelow
- fn: (data: { publishedAt: string }) => void
-}>
-
-export type ToggleAISummaryCommand = Command<{
- id: typeof COMMAND_ID.entry.toggleAISummary
- fn: () => void
-}>
-
-export type ToggleAITranslationCommand = Command<{
- id: typeof COMMAND_ID.entry.toggleAITranslation
- fn: () => void
-}>
-
-export type ImageGalleryCommand = Command<{
- id: typeof COMMAND_ID.entry.imageGallery
- fn: (data: { entryId: string }) => void
-}>
-
-export type TTSCommand = Command<{
- id: typeof COMMAND_ID.entry.tts
- fn: (data: { entryId: string; entryContent: string }) => void
-}>
-
-export type ReadabilityCommand = Command<{
- id: typeof COMMAND_ID.entry.readability
- fn: (data: { entryId: string; entryUrl: string }) => void
-}>
-
-export type EntryCommand =
- | TipCommand
- | StarCommand
- | DeleteCommand
- | CopyLinkCommand
- | ExportAsPDFCommand
- | CopyTitleCommand
- | OpenInBrowserCommand
- | ViewSourceContentCommand
- | ShareCommand
- | ReadCommand
- | ReadAboveCommand
- | ReadBelowCommand
- | ToggleAISummaryCommand
- | ToggleAITranslationCommand
- | ImageGalleryCommand
- | TTSCommand
- | ReadabilityCommand
-
-// Settings commands
-
-export type CustomizeToolbarCommand = Command<{
- id: typeof COMMAND_ID.settings.customizeToolbar
- fn: () => void
-}>
-
-export type SettingsCommand = CustomizeToolbarCommand
-
-// Integration commands
-
-export type SaveToEagleCommand = Command<{
- id: typeof COMMAND_ID.integration.saveToEagle
- fn: (payload: { entryId: string }) => void
-}>
-
-export type SaveToReadwiseCommand = Command<{
- id: typeof COMMAND_ID.integration.saveToReadwise
- fn: (payload: { entryId: string }) => void
-}>
-
-export type SaveToInstapaperCommand = Command<{
- id: typeof COMMAND_ID.integration.saveToInstapaper
- fn: (payload: { entryId: string }) => void
-}>
-
-export type SaveToObsidianCommand = Command<{
- id: typeof COMMAND_ID.integration.saveToObsidian
- fn: (payload: { entryId: string }) => void
-}>
-
-export type SaveToOutlineCommand = Command<{
- id: typeof COMMAND_ID.integration.saveToOutline
- fn: (payload: { entryId: string }) => void
-}>
-
-export type SaveToReadeckCommand = Command<{
- id: typeof COMMAND_ID.integration.saveToReadeck
- fn: (payload: { entryId: string }) => void
-}>
-
-export type SaveToCuboxCommand = Command<{
- id: typeof COMMAND_ID.integration.saveToCubox
- fn: (payload: { entryId: string }) => void
-}>
-
-export type IntegrationCommand =
- | SaveToEagleCommand
- | SaveToReadwiseCommand
- | SaveToInstapaperCommand
- | SaveToObsidianCommand
- | SaveToOutlineCommand
- | SaveToReadeckCommand
- | SaveToCuboxCommand
-
-export type BasicCommand = EntryCommand | SettingsCommand | IntegrationCommand
+export type BasicCommand =
+ | EntryCommand
+ | SettingsCommand
+ | IntegrationCommand
+ | GlobalCommand
+ | LayoutCommand
+ | TimelineCommand
+ | EntryRenderCommand
+ | SubscriptionCommand
diff --git a/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-shortcut.ts b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-shortcut.ts
new file mode 100644
index 000000000..7921d8ac4
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-shortcut.ts
@@ -0,0 +1,44 @@
+import { shortcuts } from "~/constants/shortcuts"
+
+import { COMMAND_ID } from "../commands/id"
+
+const defaultCommandShortcuts = {
+ [COMMAND_ID.entry.read]: shortcuts.entry.toggleRead.key,
+ [COMMAND_ID.entry.openInBrowser]: shortcuts.entry.openInBrowser.key,
+ [COMMAND_ID.entry.star]: shortcuts.entry.toggleStarred.key,
+ [COMMAND_ID.entry.copyLink]: shortcuts.entry.copyLink.key,
+ [COMMAND_ID.entry.copyTitle]: shortcuts.entry.copyTitle.key,
+ [COMMAND_ID.entry.tts]: shortcuts.entry.tts.key,
+ [COMMAND_ID.entry.tip]: shortcuts.entry.tip.key,
+ [COMMAND_ID.entry.share]: shortcuts.entry.share.key,
+
+ [COMMAND_ID.entryRender.scrollUp]: shortcuts.entry.scrollUp.key,
+ [COMMAND_ID.entryRender.scrollDown]: shortcuts.entry.scrollDown.key,
+
+ [COMMAND_ID.timeline.switchToNext]: shortcuts.entries.next.key,
+ [COMMAND_ID.timeline.switchToPrevious]: shortcuts.entries.previous.key,
+ [COMMAND_ID.timeline.refetch]: shortcuts.entries.refetch.key,
+
+ [COMMAND_ID.layout.toggleTimelineColumn]: shortcuts.layout.toggleSidebar.key,
+
+ [COMMAND_ID.subscription.nextSubscription]: shortcuts.subscriptions.nextSubscription.key,
+ [COMMAND_ID.subscription.previousSubscription]: shortcuts.subscriptions.previousSubscription.key,
+ [COMMAND_ID.subscription.switchTabToNext]: shortcuts.subscriptions.switchNextView.key,
+ [COMMAND_ID.subscription.switchTabToPrevious]: shortcuts.subscriptions.switchPreviousView.key,
+
+ [COMMAND_ID.global.showShortcuts]: shortcuts.layout.showShortcuts.key,
+
+ [COMMAND_ID.entryRender.nextEntry]: shortcuts.entry.nextEntry.key,
+ [COMMAND_ID.entryRender.previousEntry]: shortcuts.entry.previousEntry.key,
+
+ [COMMAND_ID.subscription.toggleFolderCollapse]: shortcuts.subscriptions.toggleFolderCollapse.key,
+} as const
+
+export type BindingCommandId = keyof typeof defaultCommandShortcuts
+
+// eslint-disable-next-line @eslint-react/hooks-extra/no-unnecessary-use-prefix, @eslint-react/hooks-extra/ensure-custom-hooks-using-other-hooks
+export const useCommandShortcut = (commandId: BindingCommandId): string => {
+ const commandShortcut = defaultCommandShortcuts[commandId]
+
+ return commandShortcut
+}
diff --git a/apps/desktop/layer/renderer/src/modules/command/hooks/use-command.test-d.ts b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command.test-d.ts
index 04e6eef5a..450d1784b 100644
--- a/apps/desktop/layer/renderer/src/modules/command/hooks/use-command.test-d.ts
+++ b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command.test-d.ts
@@ -1,7 +1,7 @@
import { assertType, expectTypeOf, test } from "vitest"
+import type { TipCommand } from "../commands/entry"
import { COMMAND_ID } from "../commands/id"
-import type { TipCommand } from "../commands/types"
import { getCommand, useCommand, useRunCommandFn } from "./use-command"
test("getCommand types work properly", () => {
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 5c4f1ff50..304310d5c 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
@@ -1,14 +1,16 @@
-import { useHotkeys } from "react-hotkeys-hook"
+import { useEffect } from "react"
+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"
interface RegisterHotkeyOptions
{
shortcut: string
commandId: T
args?: Parameters["run"]>
when?: boolean
- // hotkeyOptions?: Options
}
export const useCommandHotkey = ({
@@ -17,27 +19,60 @@ export const useCommandHotkey = ({
when,
args,
}: RegisterHotkeyOptions) => {
- useHotkeys(
- shortcut,
- () => {
- const command = getCommand(commandId)
+ useEffect(() => {
+ if (!when) {
+ return
+ }
- if (!command) return
- if (Array.isArray(args)) {
- // It should be safe to spread the args here because we are checking if it is an array
- // @ts-expect-error - A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556)
- command.run(...args)
- return
+ if (!shortcut) {
+ return
+ }
+
+ // Handle comma-separated shortcuts
+ const shortcuts = shortcut.split(",").map((s) => s.trim())
+ const keyMap: Record void> = {}
+
+ // Create a handler for each shortcut
+ shortcuts.forEach((key) => {
+ keyMap[key] = (event) => {
+ event.preventDefault()
+ event.stopPropagation()
+
+ const command = getCommand(commandId)
+ if (!command) return
+
+ if (Array.isArray(args)) {
+ // It should be safe to spread the args here because we are checking if it is an array
+ // @ts-expect-error - A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556)
+ command.run(...args)
+ return
+ }
+
+ if (args === undefined) {
+ // @ts-expect-error
+ command.run()
+ return
+ }
+
+ console.error("Invalid args", typeof args, args)
}
- if (args === undefined) {
- // @ts-expect-error
- command.run()
- return
- }
- console.error("Invalid args", typeof args, args)
- },
- {
- enabled: when,
- },
- )
+ })
+
+ return tinykeys(document.documentElement, keyMap)
+ }, [shortcut, commandId, when, args])
+}
+
+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/registry/command.ts b/apps/desktop/layer/renderer/src/modules/command/registry/command.ts
index de30ab791..ffadc8d08 100644
--- a/apps/desktop/layer/renderer/src/modules/command/registry/command.ts
+++ b/apps/desktop/layer/renderer/src/modules/command/registry/command.ts
@@ -23,9 +23,6 @@ export function createCommand<
label = typeof label === "string" ? { title: label } : label
return label
},
- // when: !!(options.when ?? true),
- // keyBinding:
- // typeof options.keyBinding === "string" ? { binding: options.keyBinding } : options.keyBinding,
}
}
diff --git a/apps/desktop/layer/renderer/src/modules/command/types.ts b/apps/desktop/layer/renderer/src/modules/command/types.ts
index 8e84bec41..edb0121f5 100644
--- a/apps/desktop/layer/renderer/src/modules/command/types.ts
+++ b/apps/desktop/layer/renderer/src/modules/command/types.ts
@@ -8,6 +8,7 @@ export type CommandCategory =
| "follow:updates"
| "follow:help"
| "follow:general"
+ | "follow:entry-render"
export interface KeybindingOptions {
binding: string
@@ -17,36 +18,6 @@ export interface KeybindingOptions {
// skipRegister?: boolean
}
-export interface CommandKeybindingOptions<
- ID extends string,
- T extends (...args: any[]) => unknown = (...args: unknown[]) => unknown,
-> {
- /**
- * the command id.
- */
- commandId: ID
- /**
- * a set of predefined precondition strategies.
- *
- * note: this only used for keybinding and command menu.
- * command will always available when called directly.
- */
- when?: boolean
- /**
- * we use https://github.com/jamiebuilds/tinykeys so that we can use the same keybinding definition
- * for both mac and windows.
- *
- * Use `$mod` for `Cmd` on Mac and `Ctrl` on Windows and Linux.
- */
- keyBinding?: KeybindingOptions | string
- /**
- * additional arguments for the command.
- *
- * Only used when the command is called from a keybinding.
- */
- args?: Parameters
-}
-
export interface Command<
T extends { id: string; fn: (...args: any[]) => unknown } = {
id: string
@@ -61,9 +32,6 @@ export interface Command<
readonly icon?: ReactNode | ((props?: { isActive?: boolean }) => ReactNode)
readonly category: CommandCategory
readonly run: T["fn"]
-
- // readonly when: boolean
- // readonly keyBinding?: KeybindingOptions
}
export type SimpleCommand = Command<{ id: T; fn: () => void }>
@@ -87,7 +55,6 @@ export interface CommandOptions<
run: T["fn"]
when?: boolean
- keyBinding?: T["fn"] extends () => void ? KeybindingOptions | string : never
}
export type FollowCommandMap = {
diff --git a/apps/desktop/layer/renderer/src/modules/customize-toolbar/constant.ts b/apps/desktop/layer/renderer/src/modules/customize-toolbar/constant.ts
index c9826cd08..fce80b20d 100644
--- a/apps/desktop/layer/renderer/src/modules/customize-toolbar/constant.ts
+++ b/apps/desktop/layer/renderer/src/modules/customize-toolbar/constant.ts
@@ -7,24 +7,29 @@ export interface ToolbarActionOrder {
more: UniqueIdentifier[]
}
-const entryItemInMore = new Set([
- COMMAND_ID.entry.copyLink,
- COMMAND_ID.entry.openInBrowser,
- COMMAND_ID.entry.exportAsPDF,
- COMMAND_ID.entry.read,
- COMMAND_ID.entry.tts,
-])
-
-export const entryItemHideInHeader = new Set([
+export const ENTRY_ITEM_HIDE_IN_HEADER = new Set([
COMMAND_ID.entry.readAbove,
COMMAND_ID.entry.readBelow,
])
+const MAIN_ACTIONS = [
+ COMMAND_ID.entry.readability,
+ COMMAND_ID.entry.tts,
+ COMMAND_ID.entry.star,
+
+ COMMAND_ID.entry.toggleAISummary,
+ COMMAND_ID.entry.toggleAITranslation,
+
+ COMMAND_ID.entry.imageGallery,
+ COMMAND_ID.entry.share,
+]
+const MAIN_ACTIONS_SET = new Set(MAIN_ACTIONS)
+
export const DEFAULT_ACTION_ORDER: ToolbarActionOrder = {
- main: Object.values(COMMAND_ID.entry).filter((id) => !entryItemInMore.has(id)),
+ main: MAIN_ACTIONS,
more: [
...Object.values(COMMAND_ID.integration),
- ...Object.values(COMMAND_ID.entry).filter((id) => entryItemInMore.has(id)),
+ ...Object.values(COMMAND_ID.entry).filter((id) => !MAIN_ACTIONS_SET.has(id)),
COMMAND_ID.settings.customizeToolbar,
],
}
diff --git a/apps/desktop/layer/renderer/src/modules/customize-toolbar/hooks.ts b/apps/desktop/layer/renderer/src/modules/customize-toolbar/hooks.ts
index d799e6023..255e6f607 100644
--- a/apps/desktop/layer/renderer/src/modules/customize-toolbar/hooks.ts
+++ b/apps/desktop/layer/renderer/src/modules/customize-toolbar/hooks.ts
@@ -3,10 +3,11 @@ import { useMemo } from "react"
import { useUISettingSelector } from "~/atoms/settings/ui"
-import { DEFAULT_ACTION_ORDER, entryItemHideInHeader } from "./constant"
+import { DEFAULT_ACTION_ORDER, ENTRY_ITEM_HIDE_IN_HEADER } from "./constant"
export const useActionOrder = () => {
const actionOrderSetting = useUISettingSelector((s) => s.toolbarOrder)
+
return useMemo(() => {
const { main, more } = actionOrderSetting
const missingMainActions = DEFAULT_ACTION_ORDER.main.filter(
@@ -18,10 +19,10 @@ export const useActionOrder = () => {
return {
main: [...actionOrderSetting.main, ...missingMainActions].filter(
- (id) => !entryItemHideInHeader.has(id as string),
+ (id) => !ENTRY_ITEM_HIDE_IN_HEADER.has(id as string),
),
more: [...actionOrderSetting.more, ...missingMoreActions].filter(
- (id) => !entryItemHideInHeader.has(id as string),
+ (id) => !ENTRY_ITEM_HIDE_IN_HEADER.has(id as string),
),
}
}, [actionOrderSetting])
@@ -52,5 +53,6 @@ export const useToolbarOrderMap = () => {
)
return actionOrderMap
}, [actionOrder])
+
return actionOrderMap
}
diff --git a/apps/desktop/layer/renderer/src/modules/discover/DiscoverFeedCard.tsx b/apps/desktop/layer/renderer/src/modules/discover/DiscoverFeedCard.tsx
index e135ab80f..c61047cbd 100644
--- a/apps/desktop/layer/renderer/src/modules/discover/DiscoverFeedCard.tsx
+++ b/apps/desktop/layer/renderer/src/modules/discover/DiscoverFeedCard.tsx
@@ -114,7 +114,7 @@ export const DiscoverFeedCard: FC = memo(
{item.docs ? (
-
diff --git a/apps/desktop/layer/renderer/src/modules/discover/DiscoverInboxList.tsx b/apps/desktop/layer/renderer/src/modules/discover/DiscoverInboxList.tsx
index bba2ee6bc..f468ec1f5 100644
--- a/apps/desktop/layer/renderer/src/modules/discover/DiscoverInboxList.tsx
+++ b/apps/desktop/layer/renderer/src/modules/discover/DiscoverInboxList.tsx
@@ -63,7 +63,7 @@ export function DiscoverInboxList() {
{/* New Inbox */}
preCheck() &&
present({
diff --git a/apps/desktop/layer/renderer/src/modules/discover/FeedForm.tsx b/apps/desktop/layer/renderer/src/modules/discover/FeedForm.tsx
index d990247c6..c9c78cfea 100644
--- a/apps/desktop/layer/renderer/src/modules/discover/FeedForm.tsx
+++ b/apps/desktop/layer/renderer/src/modules/discover/FeedForm.tsx
@@ -71,6 +71,16 @@ export const FeedForm: Component<{
const isInModal = useIsInModal()
const placeholderRef = useRef(null)
+ useEffect(() => {
+ if (!feedQuery.isLoading) {
+ tracker.subscribeModalOpened({
+ feedId: id,
+ feedUrl: feedQuery.data?.feed.url || url,
+ isError: feedQuery.isError,
+ })
+ }
+ }, [feedQuery.isLoading])
+
return (
{
- if (analytics?.view !== undefined && !subscription && defaultValues?.view === undefined) {
+ if (
+ typeof analytics?.view === "number" &&
+ !subscription &&
+ typeof defaultValues?.view !== "number"
+ ) {
form.setValue("view", `${analytics.view}`)
}
}, [analytics, subscription, defaultValues?.view])
diff --git a/apps/desktop/layer/renderer/src/modules/discover/InboxTable.shared.tsx b/apps/desktop/layer/renderer/src/modules/discover/InboxTable.shared.tsx
index ddcdc1139..429611f08 100644
--- a/apps/desktop/layer/renderer/src/modules/discover/InboxTable.shared.tsx
+++ b/apps/desktop/layer/renderer/src/modules/discover/InboxTable.shared.tsx
@@ -97,7 +97,7 @@ const ConfirmDestroyModalContent = ({ id }: { id: string }) => {
{t("discover.inbox_destroy_warning")}
- mutationDestroy.mutate(id)}>
+ mutationDestroy.mutate(id)}>
{t("words.confirm")}
diff --git a/apps/desktop/layer/renderer/src/modules/discover/ListForm.tsx b/apps/desktop/layer/renderer/src/modules/discover/ListForm.tsx
index 83da82415..322373e97 100644
--- a/apps/desktop/layer/renderer/src/modules/discover/ListForm.tsx
+++ b/apps/desktop/layer/renderer/src/modules/discover/ListForm.tsx
@@ -66,6 +66,15 @@ export const ListForm: Component<{
const { t } = useTranslation()
+ useEffect(() => {
+ if (!feedQuery.isLoading) {
+ tracker.subscribeModalOpened({
+ listId: id,
+ isError: feedQuery.isError,
+ })
+ }
+ }, [feedQuery.isLoading])
+
return (
{
return Queries.discover.rsshubCategory({
category: "popular",
categories: category === "all" ? "popular" : `popular,${category}`,
- lang,
+ lang: LanguageMap[lang],
})
}
let firstLoad = true
export function Recommendations() {
const { t } = useTranslation()
- const lang = useGeneralSettingKey("language")
const { present } = useModalStack()
- const defaultLang = !lang || ["zh-CN", "zh-HK", "zh-TW"].includes(lang) ? "all" : "en"
const [category, setCategory] = useState
("all")
- const [selectedLang, setSelectedLang] = useState(defaultLang)
+ const lang = useUISettingKey("discoverLanguage")
- const rsshubPopular = useAuthQuery(fetchRsshubPopular(category, selectedLang), {
+ const rsshubPopular = useAuthQuery(fetchRsshubPopular(category, lang), {
meta: {
persist: true,
},
@@ -107,7 +111,7 @@ export function Recommendations() {
const handleLangChange = useCallback(
(value: string) => {
flushSync(() => {
- setSelectedLang(value as Language)
+ setUISetting("discoverLanguage", value as Language)
})
rsshubPopular.refetch()
},
@@ -152,7 +156,7 @@ export function Recommendations() {
{t("words.language")}
void
@@ -16,42 +22,43 @@ export const EntryColumnShortcutHandler: FC<{
}> = memo(({ data, refetch, handleScrollTo }) => {
const dataRef = useRefValue(data!)
- useHotkeys(
- shortcuts.entries.refetch.key,
- () => {
- refetch()
- },
- { scopes: HotKeyScopeMap.Home },
- )
- const currentEntryIdRef = useRefValue(useRouteEntryId())
+ const activeScope = useHotkeyScope()
+ const when =
+ activeScope.includes(HotkeyScope.Timeline) && !activeScope.includes(HotkeyScope.EntryRender)
+
+ useCommandBinding({
+ commandId: COMMAND_ID.timeline.switchToNext,
+ when,
+ })
+
+ useCommandBinding({
+ commandId: COMMAND_ID.timeline.switchToPrevious,
+ when,
+ })
+
+ useCommandBinding({
+ commandId: COMMAND_ID.timeline.refetch,
+ when,
+ })
+
+ useCommandHotkey({
+ commandId: COMMAND_ID.timeline.enter,
+ shortcut: "Enter",
+ when,
+ })
+
+ useCommandHotkey({
+ commandId: COMMAND_ID.layout.focusToSubscription,
+ shortcut: "Backspace, Escape",
+ when,
+ })
+
+ const currentEntryIdRef = useRefValue(useRouteEntryId())
const navigate = useNavigateEntry()
- const $mainContainer = useMainContainerElement()
- const [enabledArrowKey, setEnabledArrowKey] = useState(false)
-
- // Enable arrow key navigation shortcuts only when focus is on entryContent or entryList,
- // entryList shortcuts should not be triggered in the feed col
- useLayoutEffect(() => {
- if (!$mainContainer) return
- const handler = () => {
- const target = document.activeElement
- const isFocusIn = $mainContainer.contains(target) || $mainContainer === target
-
- setEnabledArrowKey(isFocusIn)
- }
-
- handler()
- // NOTE: focusin event will bubble to the document
- document.addEventListener("focusin", handler)
- return () => {
- document.removeEventListener("focusin", handler)
- }
- }, [$mainContainer])
-
- useHotkeys(
- shortcuts.entries.next.key,
- () => {
+ useEffect(() => {
+ return EventBus.subscribe(COMMAND_ID.timeline.switchToNext, () => {
const data = dataRef.current
const currentActiveEntryIndex = data.indexOf(currentEntryIdRef.current || "")
@@ -63,12 +70,11 @@ export const EntryColumnShortcutHandler: FC<{
navigate({
entryId: nextId,
})
- },
- { scopes: HotKeyScopeMap.Home, enabled: enabledArrowKey, preventDefault: true },
- )
- useHotkeys(
- shortcuts.entries.previous.key,
- () => {
+ })
+ }, [currentEntryIdRef, dataRef, handleScrollTo, navigate, when])
+
+ useEffect(() => {
+ return EventBus.subscribe(COMMAND_ID.timeline.switchToPrevious, () => {
const data = dataRef.current
const currentActiveEntryIndex = data.indexOf(currentEntryIdRef.current || "")
@@ -81,8 +87,27 @@ export const EntryColumnShortcutHandler: FC<{
navigate({
entryId: nextId,
})
- },
- { scopes: HotKeyScopeMap.Home, enabled: enabledArrowKey, preventDefault: true },
- )
+ })
+ }, [currentEntryIdRef, dataRef, handleScrollTo, navigate])
+
+ useEffect(() => {
+ return EventBus.subscribe(COMMAND_ID.timeline.refetch, () => {
+ refetch()
+ })
+ }, [refetch])
+
+ const $scrollArea = useScrollViewElement()
+ const { highlightBoundary } = useFocusActions()
+ useEffect(() => {
+ return EventBus.subscribe(COMMAND_ID.layout.focusToTimeline, () => {
+ $scrollArea?.focus()
+ nextFrame(highlightBoundary)
+ })
+ }, [$scrollArea, highlightBoundary])
+
+ const isFocusIn = useFocusable()
+
+ useConditionalHotkeyScope(HotkeyScope.Timeline, isFocusIn, true)
+
return null
})
diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx
index f31734ebd..a5eccaa0d 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx
@@ -1,3 +1,4 @@
+import { useMobile } from "@follow/components/hooks/useMobile.js"
import {
MasonryIntersectionContext,
MasonryItemsAspectRatioContext,
@@ -46,6 +47,7 @@ const gutter = 24
export const PictureMasonry: FC = (props) => {
const { data } = props
+ const isMobile = useMobile()
const cacheMap = useState(() => new Map())[0]
const [isInitDim, setIsInitDim] = useState(false)
const [isInitLayout, setIsInitLayout] = useState(false)
@@ -79,7 +81,7 @@ export const PictureMasonry: FC = (props) => {
},
)
- const finalColumn = customizeColumn !== -1 ? customizeColumn : currentColumn
+ const finalColumn = customizeColumn !== -1 && !isMobile ? customizeColumn : currentColumn
const finalItemWidth = useMemo(
() => (customizeColumn !== -1 ? calcItemWidth(finalColumn) : currentItemWidth),
[calcItemWidth, currentItemWidth, customizeColumn, finalColumn],
diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/Items/social-media-item.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/Items/social-media-item.tsx
index f8c7c21b3..180ccd998 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-column/Items/social-media-item.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-column/Items/social-media-item.tsx
@@ -2,6 +2,7 @@ import { PassviseFragment } from "@follow/components/common/Fragment.js"
import { useMobile } from "@follow/components/hooks/useMobile.js"
import { AutoResizeHeight } from "@follow/components/ui/auto-resize-height/index.js"
import { Skeleton } from "@follow/components/ui/skeleton/index.jsx"
+import { FeedViewType } from "@follow/constants"
import type { MediaModel } from "@follow/shared/hono"
import { getImageProxyUrl } from "@follow/utils/img-proxy"
import { LRUCache } from "@follow/utils/lru-cache"
@@ -10,9 +11,7 @@ import { atom } from "jotai"
import { useLayoutEffect, useMemo, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
-import { MenuItemText } from "~/atoms/context-menu"
import { useGeneralSettingKey } from "~/atoms/settings/general"
-import { CommandActionButton } from "~/components/ui/button/CommandActionButton"
import { RelativeTime } from "~/components/ui/datetime"
import { HTML } from "~/components/ui/markdown/HTML"
import { Media } from "~/components/ui/media"
@@ -22,7 +21,8 @@ import { useSortedEntryActions } from "~/hooks/biz/useEntryActions"
import { useRenderStyle } from "~/hooks/biz/useRenderStyle"
import { jotaiStore } from "~/lib/jotai"
import { parseSocialMedia } from "~/lib/parsers"
-import { COMMAND_ID } from "~/modules/command/commands/id"
+import { EntryHeaderActions } from "~/modules/entry-content/actions/header-actions"
+import { MoreActions } from "~/modules/entry-content/actions/more-actions"
import { FeedIcon } from "~/modules/feed/feed-icon"
import { FeedTitle } from "~/modules/feed/feed-title"
import { useEntry } from "~/store/entry/hooks"
@@ -73,7 +73,7 @@ export const SocialMediaItem: EntryListItemFC = ({ entryId, entryPreview, transl
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={cn(
- "relative flex px-5 py-4 lg:px-8",
+ "relative flex px-5 py-4 first:mt-6 lg:px-8",
"group",
!asRead &&
"before:bg-accent before:absolute before:left-1 before:top-8 before:block before:size-2 before:rounded-full md:before:-left-2 lg:before:left-2",
@@ -139,20 +139,8 @@ const ActionBar = ({ entryId }: { entryId: string }) => {
return (
- {entryActions
- .filter((item) => item instanceof MenuItemText)
- .filter(
- (item) =>
- item.id !== COMMAND_ID.entry.read && item.id !== COMMAND_ID.entry.openInBrowser,
- )
- .map((item) => (
-
- ))}
+
+
)
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 966995e47..4284f665a 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
@@ -8,7 +8,7 @@ import { useHotkeys } from "react-hotkeys-hook"
import { Trans, useTranslation } from "react-i18next"
import { toast } from "sonner"
-import { HotKeyScopeMap } from "~/constants"
+import { HotkeyScope } from "~/constants"
import { shortcuts } from "~/constants/shortcuts"
import { useI18n } from "~/hooks/common"
@@ -50,7 +50,7 @@ export const MarkAllReadButton = ({
label: (
{t("mark_all_read_button.undo")}
-
+
Meta+Z
@@ -61,7 +61,7 @@ export const MarkAllReadButton = ({
},
{
preventDefault: true,
- scopes: HotKeyScopeMap.Home,
+ scopes: HotkeyScope.Home,
},
)
@@ -100,7 +100,7 @@ const ConfirmMarkAllReadInfo = ({ undo }: { undo: () => any }) => {
const [countdown] = useCountdown({ countStart: 3 })
useHotkeys("ctrl+z,meta+z", undo, {
- scopes: HotKeyScopeMap.Home,
+ scopes: HotkeyScope.Home,
preventDefault: true,
})
@@ -137,7 +137,7 @@ export const FlatMarkAllReadButton: FC<
variant="ghost"
disabled={status === "done"}
buttonClassName={buttonClassName}
- className={cn(
+ textClassName={cn(
"center relative flex h-auto gap-1",
className,
diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/index.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/index.tsx
index c3d03232e..744a0acfb 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-column/index.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-column/index.tsx
@@ -1,3 +1,4 @@
+import { Focusable } from "@follow/components/common/Focusable/index.js"
import { useMobile } from "@follow/components/hooks/useMobile.js"
import { FeedViewType, views } from "@follow/constants"
import { useTitle } from "@follow/hooks"
@@ -116,7 +117,7 @@ function EntryColumnImpl() {
const ListComponent = views[view]!.gridMode ? EntryColumnGrid : EntryList
return (
-
)}
-
+
)
}
diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.electron.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.electron.tsx
index f227bc674..33749cf03 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.electron.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.electron.tsx
@@ -175,7 +175,7 @@ const PreviewHeaderInfoWrapper: Component = ({ children }) => {
{
const { feedId, listId } = getRouteParams()
if (!feedId) return
diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.mobile.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.mobile.tsx
index 6e09e42a1..372d128dd 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.mobile.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.mobile.tsx
@@ -163,7 +163,7 @@ const FollowSubscriptionButton = () => {
diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/templates/list-item-template.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/templates/list-item-template.tsx
index b77c63dd9..daffa41c8 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-column/templates/list-item-template.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-column/templates/list-item-template.tsx
@@ -156,7 +156,7 @@ export function ListItem({
>
{entry.entries.title ? (
diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/wrapper.electron.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/wrapper.electron.tsx
index f8e2002e0..6ba9d3e6a 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-column/wrapper.electron.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-column/wrapper.electron.tsx
@@ -1,6 +1,6 @@
import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js"
-import { views } from "@follow/constants"
-import { clsx } from "clsx"
+import { FeedViewType, views } from "@follow/constants"
+import { cn } from "@follow/utils/utils"
import { useIsZenMode } from "~/atoms/settings/ui"
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
@@ -14,17 +14,17 @@ export const EntryColumnWrapper = ({ ref, children, onScroll }: EntryColumnWrapp
const isZenMode = useIsZenMode()
return (
-
+
div]:grow flex"}
onScroll={onScroll}
>
{children}
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 dc08ac81a..a76305d51 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
@@ -3,10 +3,9 @@ import type { FeedViewType } from "@follow/constants"
import { MenuItemText } from "~/atoms/context-menu"
import { CommandActionButton } from "~/components/ui/button/CommandActionButton"
import { useHasModal } from "~/components/ui/modal/stacked/hooks"
-import { shortcuts } from "~/constants/shortcuts"
import { useSortedEntryActions } from "~/hooks/biz/useEntryActions"
import { COMMAND_ID } from "~/modules/command/commands/id"
-import { useCommandHotkey } from "~/modules/command/hooks/use-register-hotkey"
+import { useCommandBinding } from "~/modules/command/hooks/use-register-hotkey"
import { useEntry } from "~/store/entry/hooks"
export const EntryHeaderActions = ({
@@ -23,9 +22,8 @@ export const EntryHeaderActions = ({
const hasModal = useHasModal()
- useCommandHotkey({
+ useCommandBinding({
when: !!entry?.entries.url && !hasModal,
- shortcut: shortcuts.entry.openInBrowser.key,
commandId: COMMAND_ID.entry.openInBrowser,
args: [{ entryId }],
})
diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryReadHistory.electron.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryReadHistory.electron.tsx
index bffb08e2f..c72e1e515 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryReadHistory.electron.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryReadHistory.electron.tsx
@@ -1,19 +1,32 @@
-import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
-import {
- HoverCard,
- HoverCardContent,
- HoverCardPortal,
- HoverCardTrigger,
-} from "@radix-ui/react-hover-card"
-import clsx from "clsx"
+import { AvatarGroup } from "@follow/components/ui/avatar-group/index.js"
+import { FeedViewType } from "@follow/constants"
import { useWhoami } from "~/atoms/user"
+import { getRouteParams } from "~/hooks/biz/useRouteParams"
import { useAuthQuery } from "~/hooks/common"
import { useAppLayoutGridContainerWidth } from "~/providers/app-grid-layout-container-provider"
import { Queries } from "~/queries"
import { useEntryReadHistory } from "~/store/entry"
-import { EntryUser, EntryUserRow, getLimit } from "./EntryReadHistory.shared"
+import { EntryUser } from "./EntryReadHistory.shared"
+
+const getLimit = (width: number): number => {
+ const routeParams = getRouteParams()
+ // social media view has four extra buttons
+ if (
+ [FeedViewType.SocialMedia, FeedViewType.Pictures, FeedViewType.Videos].includes(
+ routeParams.view,
+ )
+ ) {
+ if (width > 1100) return 15
+ if (width > 950) return 10
+ if (width > 800) return 5
+ return 3
+ }
+ if (width > 900) return 15
+ if (width > 600) return 10
+ return 5
+}
export const EntryReadHistory: Component<{ entryId: string }> = ({ entryId }) => {
const me = useWhoami()
@@ -37,64 +50,27 @@ export const EntryReadHistory: Component<{ entryId: string }> = ({ entryId }) =>
className="animate-in fade-in @md:flex hidden items-center duration-200"
data-hide-in-print
>
- {entryHistory.userIds
- .filter((id) => id !== me?.id)
- .slice(0, LIMIT)
+
+ {entryHistory.userIds
+ .filter((id) => id !== me?.id)
+ .slice(0, LIMIT)
- .map((userId, i) => (
-
- ))}
+ .map((userId) => (
+
+ ))}
+
- {!!totalCount && totalCount > LIMIT && (
-
-
-
-
- +{Math.min(totalCount - LIMIT, 99)}
-
-
-
-
- {totalCount > LIMIT && (
-
-
-
-
- {entryHistory.userIds
- .filter((id) => id !== me?.id)
- .slice(LIMIT)
- .map((userId) => (
-
- ))}
-
-
-
-
- )}
-
- )}
+
+
+ +{Math.min(totalCount - LIMIT, 99)}
+
+
)
}
diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryReadHistory.shared.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryReadHistory.shared.tsx
index 8b226fe3f..b8f58855d 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryReadHistory.shared.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryReadHistory.shared.tsx
@@ -1,109 +1,46 @@
import { Avatar, AvatarFallback, AvatarImage } from "@follow/components/ui/avatar/index.jsx"
-import {
- Tooltip,
- TooltipContent,
- TooltipPortal,
- TooltipTrigger,
-} from "@follow/components/ui/tooltip/index.jsx"
-import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/index.js"
-import { FeedViewType } from "@follow/constants"
+import { TooltipContent, TooltipPortal } from "@follow/components/ui/tooltip/index.jsx"
import { getNameInitials } from "@follow/utils/cjk"
import { m } from "motion/react"
import { memo } from "react"
import { useTranslation } from "react-i18next"
-import { getRouteParams } from "~/hooks/biz/useRouteParams"
import { replaceImgUrlIfNeed } from "~/lib/img-proxy"
import { useUserById } from "~/store/user"
import { usePresentUserProfileModal } from "../../profile/hooks"
-export const getLimit = (width: number): number => {
- const routeParams = getRouteParams()
- // social media view has four extra buttons
- if (
- [FeedViewType.SocialMedia, FeedViewType.Pictures, FeedViewType.Videos].includes(
- routeParams.view,
- )
- ) {
- if (width > 1100) return 15
- if (width > 950) return 10
- if (width > 800) return 5
- return 3
- }
- if (width > 900) return 15
- if (width > 600) return 10
- return 5
-}
-
-export const EntryUserRow: Component<{ userId: string }> = memo(({ userId }) => {
- const user = useUserById(userId)
- const presentUserProfile = usePresentUserProfileModal("drawer")
- if (!user) return null
-
- return (
-
{
- presentUserProfile(userId)
- }}
- role="button"
- tabIndex={0}
- className="cursor-button hover:bg-theme-selection-hover hover:text-theme-selection-foreground relative flex min-w-0 max-w-[50ch] shrink-0 items-center gap-2 truncate rounded-md p-1 px-2"
- >
-
-
- {getNameInitials(user.name || "")}
-
-
- {user.name && (
-
- {user.name}
-
- )}
-
- )
-})
-
export const EntryUser: Component<{
userId: string
- i: number
-}> = memo(({ userId, i }) => {
+ ref?: React.Ref
+}> = memo(({ userId, ref }) => {
const user = useUserById(userId)
const { t } = useTranslation()
const presentUserProfile = usePresentUserProfileModal("drawer")
if (!user) return null
return (
-
-
+ {
+ presentUserProfile(userId)
}}
- asChild
>
- {
- presentUserProfile(userId)
- }}
- >
-
-
- {getNameInitials(user.name || "")}
-
-
-
+
+
+ {getNameInitials(user.name || "")}
+
+
{t("entry_actions.recent_reader")} {user.name}
-
+
)
})
diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/header.electron.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/header.electron.tsx
index 023e878a8..1f8449d09 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-content/header.electron.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-content/header.electron.tsx
@@ -1,4 +1,4 @@
-import { views } from "@follow/constants"
+import { FeedViewType, views } from "@follow/constants"
import { cn } from "@follow/utils/utils"
import { AnimatePresence, m } from "motion/react"
import { memo } from "react"
@@ -60,8 +60,8 @@ function EntryHeaderImpl({ view, entryId, className, compact }: EntryHeaderProps
{entryTitleMeta.title}
-
-
+
+
{entryTitleMeta.description}
@@ -69,10 +69,12 @@ function EntryHeaderImpl({ view, entryId, className, compact }: EntryHeaderProps
-
-
-
-
+ {view !== FeedViewType.SocialMedia && (
+
+
+
+
+ )}
)
diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/hooks.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/hooks.tsx
index 4087e8626..b20b6308c 100644
--- a/apps/desktop/layer/renderer/src/modules/entry-content/hooks.tsx
+++ b/apps/desktop/layer/renderer/src/modules/entry-content/hooks.tsx
@@ -1,29 +1,22 @@
import { tracker } from "@follow/tracker"
-import { EventBus } from "@follow/utils/event-bus"
-import { createElement, useCallback, useEffect } from "react"
+import { createElement, useCallback, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
+import {
+ useEntryIsInReadability,
+ useEntryIsInReadabilitySuccess,
+ useEntryReadabilityContent,
+} from "~/atoms/readability"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
+import { useAuthQuery } from "~/hooks/common/useBizQuery"
+import { Queries } from "~/queries"
+import { useEntryTranslation } from "~/store/ai/hook"
+import { useEntry } from "~/store/entry/hooks"
+import { useInboxById } from "~/store/inbox/hooks"
import { ImageGalleryContent } from "./components/ImageGalleryContent"
-declare module "@follow/utils/event-bus" {
- export interface CustomEvent {
- FOCUS_ENTRY_CONTAINER: never
- }
-}
-
-export const useFocusEntryContainerSubscriptions = (
- ref: React.RefObject