From fd1209afdc10e0bb02d7c75a39c986a1d7d885c9 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:11:10 +0800 Subject: [PATCH] feat: merge actions for toggling state --- .../ui/dropdown-menu/dropdown-menu.tsx | 9 +- .../src/hooks/biz/useEntryActions.tsx | 26 +---- .../src/modules/command/commands/entry.tsx | 108 ++++++++---------- .../src/modules/command/commands/id.ts | 3 - .../src/modules/command/commands/types.ts | 16 --- .../hooks/use-register-command.test-d.ts | 14 --- .../command/registry/command.test-d.ts | 10 -- apps/renderer/src/modules/command/types.ts | 4 +- .../src/modules/customize-toolbar/constant.ts | 3 +- .../src/modules/customize-toolbar/dnd.tsx | 4 +- .../entry-column/Items/social-media-item.tsx | 5 +- .../entry-content/actions/more-actions.tsx | 24 +++- .../modules/entry-content/header.mobile.tsx | 5 +- locales/app/en.json | 6 +- .../src/ui/button/action-button.tsx | 6 +- 15 files changed, 98 insertions(+), 145 deletions(-) diff --git a/apps/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx b/apps/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx index d463cf38cc..0ce5bfd026 100644 --- a/apps/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx +++ b/apps/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx @@ -95,9 +95,10 @@ const DropdownMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean - icon?: React.ReactNode + icon?: React.ReactNode | ((props?: { isActive?: boolean }) => React.ReactNode) + active?: boolean } ->(({ className, inset, icon, ...props }, ref) => ( +>(({ className, inset, icon, active, ...props }, ref) => ( {!!icon && ( - {icon} + + {typeof icon === "function" ? icon({ isActive: active }) : icon} + )} {props.children} {/* Justify Fill */} diff --git a/apps/renderer/src/hooks/biz/useEntryActions.tsx b/apps/renderer/src/hooks/biz/useEntryActions.tsx index e85810aac1..8025669af9 100644 --- a/apps/renderer/src/hooks/biz/useEntryActions.tsx +++ b/apps/renderer/src/hooks/biz/useEntryActions.tsx @@ -128,16 +128,10 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee hide: isInbox || feed?.ownerUserId === whoami()?.id, shortcut: shortcuts.entry.tip.key, }, - { - id: COMMAND_ID.entry.unstar, - onClick: runCmdFn(COMMAND_ID.entry.unstar, [{ entryId }]), - hide: !entry?.collections, - shortcut: shortcuts.entry.toggleStarred.key, - }, { id: COMMAND_ID.entry.star, onClick: runCmdFn(COMMAND_ID.entry.star, [{ entryId, view }]), - hide: !!entry?.collections, + active: !!entry?.collections, shortcut: shortcuts.entry.toggleStarred.key, }, { @@ -159,13 +153,8 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee { id: COMMAND_ID.entry.viewSourceContent, onClick: runCmdFn(COMMAND_ID.entry.viewSourceContent, [{ entryId }]), - hide: isMobile() || isShowSourceContent || !entry?.entries.url, - }, - { - id: COMMAND_ID.entry.viewEntryContent, - onClick: runCmdFn(COMMAND_ID.entry.viewEntryContent, []), - hide: !isShowSourceContent, - active: true, + hide: isMobile() || !entry?.entries.url, + active: isShowSourceContent, }, { id: COMMAND_ID.entry.toggleAISummary, @@ -198,13 +187,8 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee { id: COMMAND_ID.entry.read, onClick: runCmdFn(COMMAND_ID.entry.read, [{ entryId }]), - hide: !hasEntry || entry.read || !!entry.collections || !!inList, - shortcut: shortcuts.entry.toggleRead.key, - }, - { - id: COMMAND_ID.entry.unread, - onClick: runCmdFn(COMMAND_ID.entry.unread, [{ entryId }]), - hide: !hasEntry || !entry.read || !!entry.collections || !!inList, + hide: !hasEntry || !!entry.collections || !!inList, + active: !!entry?.read, shortcut: shortcuts.entry.toggleRead.key, }, { diff --git a/apps/renderer/src/modules/command/commands/entry.tsx b/apps/renderer/src/modules/command/commands/entry.tsx index 5499844de4..60d95a1d17 100644 --- a/apps/renderer/src/modules/command/commands/entry.tsx +++ b/apps/renderer/src/modules/command/commands/entry.tsx @@ -1,13 +1,17 @@ import { FeedViewType, UserRole } from "@follow/constants" import { IN_ELECTRON } from "@follow/shared/constants" -import { getOS } from "@follow/utils/utils" +import { cn, getOS } from "@follow/utils/utils" import { useMutation } from "@tanstack/react-query" import { useTranslation } from "react-i18next" import { toast } from "sonner" import { toggleShowAISummary } from "~/atoms/ai-summary" import { toggleShowAITranslation } from "~/atoms/ai-translation" -import { setShowSourceContent, useSourceContentModal } from "~/atoms/source-content" +import { + getShowSourceContent, + toggleShowSourceContent, + useSourceContentModal, +} from "~/atoms/source-content" import { useUserRole } from "~/atoms/user" import { navigateEntry } from "~/hooks/biz/useNavigateEntry" import { getRouteParams } from "~/hooks/biz/useRouteParams" @@ -112,7 +116,13 @@ export const useRegisterEntryCommands = () => { { id: COMMAND_ID.entry.star, label: t("entry_actions.star"), - icon: , + icon: (props) => ( + + ), run: ({ entryId, view }) => { const entry = useEntryStore.getState().flatMapEntries[entryId] if (!entry) { @@ -128,20 +138,11 @@ export const useRegisterEntryCommands = () => { // width: 252, // }) // } - collect.mutate({ entryId, view }) - }, - }, - { - id: COMMAND_ID.entry.unstar, - label: t("entry_actions.unstar"), - icon: , - run: ({ entryId }) => { - const entry = useEntryStore.getState().flatMapEntries[entryId] - if (!entry) { - toast.error("Failed to unstar: entry is not available", { duration: 3000 }) - return + if (entry.collections) { + uncollect.mutate(entry.entries.id) + } else { + collect.mutate({ entryId, view }) } - uncollect.mutate(entry.entries.id) }, }, { @@ -211,37 +212,31 @@ export const useRegisterEntryCommands = () => { label: t("entry_actions.view_source_content"), icon: , run: ({ entryId }) => { - const entry = useEntryStore.getState().flatMapEntries[entryId] - if (!entry || !entry.entries.url) { - toast.error("Failed to view source content: url is not available", { duration: 3000 }) - return - } - const routeParams = getRouteParams() - const viewPreviewInModal = [ - FeedViewType.SocialMedia, - FeedViewType.Videos, - FeedViewType.Pictures, - ].includes(routeParams.view) - if (viewPreviewInModal) { - showSourceContentModal({ - title: entry.entries.title ?? undefined, - src: entry.entries.url, - }) - return - } - const layoutEntryId = routeParams.entryId - if (layoutEntryId !== entry.entries.id) { - navigateEntry({ entryId: entry.entries.id }) + if (!getShowSourceContent()) { + const entry = useEntryStore.getState().flatMapEntries[entryId] + if (!entry || !entry.entries.url) { + toast.error("Failed to view source content: url is not available", { duration: 3000 }) + return + } + const routeParams = getRouteParams() + const viewPreviewInModal = [ + FeedViewType.SocialMedia, + FeedViewType.Videos, + FeedViewType.Pictures, + ].includes(routeParams.view) + if (viewPreviewInModal) { + showSourceContentModal({ + title: entry.entries.title ?? undefined, + src: entry.entries.url, + }) + return + } + const layoutEntryId = routeParams.entryId + if (layoutEntryId !== entry.entries.id) { + navigateEntry({ entryId: entry.entries.id }) + } } - setShowSourceContent(true) - }, - }, - { - id: COMMAND_ID.entry.viewEntryContent, - label: t("entry_actions.view_source_content"), - icon: , - run: () => { - setShowSourceContent(false) + toggleShowSourceContent() }, }, { @@ -274,27 +269,20 @@ export const useRegisterEntryCommands = () => { { id: COMMAND_ID.entry.read, label: t("entry_actions.mark_as_read"), - icon: , + icon: (props) => ( + + ), run: ({ entryId }) => { const entry = useEntryStore.getState().flatMapEntries[entryId] if (!entry) { toast.error("Failed to mark as unread: feed is not available", { duration: 3000 }) return } - read.mutate({ entryId, feedId: entry.feedId }) - }, - }, - { - id: COMMAND_ID.entry.unread, - label: t("entry_actions.mark_as_unread"), - icon: , - run: ({ entryId }) => { - const entry = useEntryStore.getState().flatMapEntries[entryId] - if (!entry) { - toast.error("Failed to mark as unread: feed is not available", { duration: 3000 }) - return + if (entry.read) { + unread.mutate({ entryId, feedId: entry.feedId }) + } else { + read.mutate({ entryId, feedId: entry.feedId }) } - unread.mutate({ entryId, feedId: entry.feedId }) }, }, ]) diff --git a/apps/renderer/src/modules/command/commands/id.ts b/apps/renderer/src/modules/command/commands/id.ts index bf044ea928..ce56110512 100644 --- a/apps/renderer/src/modules/command/commands/id.ts +++ b/apps/renderer/src/modules/command/commands/id.ts @@ -2,16 +2,13 @@ export const COMMAND_ID = { entry: { tip: "entry:tip", star: "entry:star", - unstar: "entry:unstar", delete: "entry:delete", copyLink: "entry:copy-link", copyTitle: "entry:copy-title", openInBrowser: "entry:open-in-browser", viewSourceContent: "entry:view-source-content", - viewEntryContent: "entry:view-entry-content", share: "entry:share", read: "entry:read", - unread: "entry:unread", toggleAISummary: "entry:toggle-ai-summary", toggleAITranslation: "entry:toggle-ai-translation", }, diff --git a/apps/renderer/src/modules/command/commands/types.ts b/apps/renderer/src/modules/command/commands/types.ts index f6a4f97bf2..dabd5a6fb0 100644 --- a/apps/renderer/src/modules/command/commands/types.ts +++ b/apps/renderer/src/modules/command/commands/types.ts @@ -14,10 +14,6 @@ export type StarCommand = Command<{ id: typeof COMMAND_ID.entry.star fn: (data: { entryId: string; view?: FeedViewType }) => void }> -export type UnStarCommand = Command<{ - id: typeof COMMAND_ID.entry.unstar - fn: (data: { entryId: string }) => void -}> export type DeleteCommand = Command<{ id: typeof COMMAND_ID.entry.delete @@ -43,10 +39,6 @@ export type ViewSourceContentCommand = Command<{ id: typeof COMMAND_ID.entry.viewSourceContent fn: (data: { entryId: string }) => void }> -export type ViewEntryContentCommand = Command<{ - id: typeof COMMAND_ID.entry.viewEntryContent - fn: () => void -}> export type ShareCommand = Command<{ id: typeof COMMAND_ID.entry.share @@ -58,11 +50,6 @@ export type ReadCommand = Command<{ fn: ({ entryId }) => void }> -export type UnReadCommand = Command<{ - id: typeof COMMAND_ID.entry.unread - fn: ({ entryId }) => void -}> - export type ToggleAISummaryCommand = Command<{ id: typeof COMMAND_ID.entry.toggleAISummary fn: () => void @@ -76,16 +63,13 @@ export type ToggleAITranslationCommand = Command<{ export type EntryCommand = | TipCommand | StarCommand - | UnStarCommand | DeleteCommand | CopyLinkCommand | CopyTitleCommand | OpenInBrowserCommand | ViewSourceContentCommand - | ViewEntryContentCommand | ShareCommand | ReadCommand - | UnReadCommand | ToggleAISummaryCommand | ToggleAITranslationCommand diff --git a/apps/renderer/src/modules/command/hooks/use-register-command.test-d.ts b/apps/renderer/src/modules/command/hooks/use-register-command.test-d.ts index c4264195cc..1cffcd196e 100644 --- a/apps/renderer/src/modules/command/hooks/use-register-command.test-d.ts +++ b/apps/renderer/src/modules/command/hooks/use-register-command.test-d.ts @@ -33,13 +33,6 @@ test("useRegisterFollowCommand types", () => { expectTypeOf(entryId).toEqualTypeOf() }, }, - { - id: COMMAND_ID.entry.viewEntryContent, - label: "", - run: (...args) => { - expectTypeOf(args).toEqualTypeOf<[]>() - }, - }, ]), ) @@ -71,13 +64,6 @@ test("useRegisterFollowCommand types", () => { expectTypeOf(entryId).toEqualTypeOf() }, }, - { - id: COMMAND_ID.entry.viewEntryContent, - label: "", - run: (...args) => { - expectTypeOf(args).toEqualTypeOf<[]>() - }, - }, ]), ) }) diff --git a/apps/renderer/src/modules/command/registry/command.test-d.ts b/apps/renderer/src/modules/command/registry/command.test-d.ts index ec00d544c7..5588538668 100644 --- a/apps/renderer/src/modules/command/registry/command.test-d.ts +++ b/apps/renderer/src/modules/command/registry/command.test-d.ts @@ -45,16 +45,6 @@ test("defineFollowCommand types", () => { }) test("defineFollowCommand with keyBinding types", () => { - assertType( - defineFollowCommand({ - id: COMMAND_ID.entry.viewEntryContent, - label: "", - when: true, - keyBinding: "", - run: () => {}, - }), - ) - assertType( defineFollowCommand({ id: COMMAND_ID.entry.viewSourceContent, diff --git a/apps/renderer/src/modules/command/types.ts b/apps/renderer/src/modules/command/types.ts index 31199f7e28..4ea4f57334 100644 --- a/apps/renderer/src/modules/command/types.ts +++ b/apps/renderer/src/modules/command/types.ts @@ -58,7 +58,7 @@ export interface Command< title: string subTitle?: string } - readonly icon?: ReactNode + readonly icon?: ReactNode | ((props?: { isActive?: boolean }) => ReactNode) readonly category: CommandCategory readonly run: T["fn"] @@ -82,7 +82,7 @@ export interface CommandOptions< | (() => string) | { title: string; subTitle?: string } | (() => { title: string; subTitle?: string }) - icon?: ReactNode + icon?: ReactNode | ((props?: { isActive?: boolean }) => ReactNode) category?: CommandCategory run: T["fn"] diff --git a/apps/renderer/src/modules/customize-toolbar/constant.ts b/apps/renderer/src/modules/customize-toolbar/constant.ts index 698ba47edd..d92c4f85a8 100644 --- a/apps/renderer/src/modules/customize-toolbar/constant.ts +++ b/apps/renderer/src/modules/customize-toolbar/constant.ts @@ -9,7 +9,7 @@ export interface ToolbarActionOrder { export const DEFAULT_ACTION_ORDER: ToolbarActionOrder = { main: Object.values(COMMAND_ID.entry) - .filter((id) => !([COMMAND_ID.entry.read, COMMAND_ID.entry.unread] as string[]).includes(id)) + .filter((id) => !([COMMAND_ID.entry.read] as string[]).includes(id)) .filter( (id) => !([COMMAND_ID.entry.copyLink, COMMAND_ID.entry.openInBrowser] as string[]).includes(id), @@ -22,7 +22,6 @@ export const DEFAULT_ACTION_ORDER: ToolbarActionOrder = { COMMAND_ID.entry.copyLink, COMMAND_ID.entry.openInBrowser, COMMAND_ID.entry.read, - COMMAND_ID.entry.unread, ] as string[] ).includes(id), ), diff --git a/apps/renderer/src/modules/customize-toolbar/dnd.tsx b/apps/renderer/src/modules/customize-toolbar/dnd.tsx index d8c0e846d9..369051e0ca 100644 --- a/apps/renderer/src/modules/customize-toolbar/dnd.tsx +++ b/apps/renderer/src/modules/customize-toolbar/dnd.tsx @@ -46,7 +46,9 @@ export const SortableActionButton = ({ id }: { id: UniqueIdentifier }) => { return (
-
{cmd.icon}
+
+ {typeof cmd.icon === "function" ? cmd.icon({ isActive: false }) : cmd.icon} +
{cmd.label.title}
diff --git a/apps/renderer/src/modules/entry-column/Items/social-media-item.tsx b/apps/renderer/src/modules/entry-column/Items/social-media-item.tsx index 8513749d30..03c680e540 100644 --- a/apps/renderer/src/modules/entry-column/Items/social-media-item.tsx +++ b/apps/renderer/src/modules/entry-column/Items/social-media-item.tsx @@ -135,10 +135,7 @@ const ActionBar = ({ entryId }: { entryId: string }) => {
{entryActions .filter( - (item) => - item.id !== COMMAND_ID.entry.read && - item.id !== COMMAND_ID.entry.unread && - item.id !== COMMAND_ID.entry.openInBrowser, + (item) => item.id !== COMMAND_ID.entry.read && item.id !== COMMAND_ID.entry.openInBrowser, ) .map((item) => ( diff --git a/apps/renderer/src/modules/entry-content/actions/more-actions.tsx b/apps/renderer/src/modules/entry-content/actions/more-actions.tsx index ddff0a5028..ea3688859a 100644 --- a/apps/renderer/src/modules/entry-content/actions/more-actions.tsx +++ b/apps/renderer/src/modules/entry-content/actions/more-actions.tsx @@ -37,11 +37,21 @@ export const MoreActions = ({ entryId, view }: { entryId: string; view?: FeedVie {availableActions.map((config) => ( - + ))} {extraAction.map((config) => ( - + ))} @@ -51,14 +61,22 @@ export const MoreActions = ({ entryId, view }: { entryId: string; view?: FeedVie const CommandDropdownMenuItem = ({ commandId, onClick, + active, }: { commandId: FollowCommandId onClick: () => void + active?: boolean }) => { const command = useCommand(commandId) if (!command) return null return ( - + {command.label.title} ) diff --git a/apps/renderer/src/modules/entry-content/header.mobile.tsx b/apps/renderer/src/modules/entry-content/header.mobile.tsx index fb180c2424..1c376dabfc 100644 --- a/apps/renderer/src/modules/entry-content/header.mobile.tsx +++ b/apps/renderer/src/modules/entry-content/header.mobile.tsx @@ -187,6 +187,7 @@ const HeaderRightActions = ({ { setCtxOpen(false) item.onClick?.() @@ -207,9 +208,11 @@ const HeaderRightActions = ({ const CommandMotionButton = ({ commandId, onClick, + active, }: { commandId: FollowCommandId onClick: () => void + active?: boolean }) => { const command = useCommand(commandId) if (!command) return null @@ -219,7 +222,7 @@ const CommandMotionButton = ({ layout={false} className="flex w-full items-center gap-2 px-4 py-2" > - {command.icon} + {typeof command.icon === "function" ? command.icon({ isActive: active }) : command.icon} {command.label.title} ) diff --git a/locales/app/en.json b/locales/app/en.json index e41ce2a2e2..537890d369 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -114,7 +114,7 @@ "entry_actions.failed_to_save_to_outline": "Failed to save to Outline.", "entry_actions.failed_to_save_to_readeck": "Failed to save to Readeck.", "entry_actions.failed_to_save_to_readwise": "Failed to save to Readwise.", - "entry_actions.mark_as_read": "Mark as Read", + "entry_actions.mark_as_read": "Mark as Read / UnRead", "entry_actions.mark_as_unread": "Mark as Unread", "entry_actions.open_in_browser": "Open In {{which}}", "entry_actions.recent_reader": "Recent reader:", @@ -131,14 +131,14 @@ "entry_actions.saved_to_readeck": "Saved To Readeck.", "entry_actions.saved_to_readwise": "Saved To Readwise.", "entry_actions.share": "Share", - "entry_actions.star": "Star", + "entry_actions.star": "Star / UnStar", "entry_actions.starred": "Starred.", "entry_actions.tip": "Tip", "entry_actions.toggle_ai_summary": "Toggle AI Summary", "entry_actions.toggle_ai_translation": "Toggle AI Translation", "entry_actions.unstar": "Unstar", "entry_actions.unstarred": "Unstarred.", - "entry_actions.view_source_content": "View Source Content", + "entry_actions.view_source_content": "Toggle View Source Content", "entry_column.filtered_content_tip": "You have filtered content hidden.", "entry_column.filtered_content_tip_2": "In addition to the entries shown above, there is also filtered content.", "entry_column.refreshing": "Refreshing new entries...", diff --git a/packages/components/src/ui/button/action-button.tsx b/packages/components/src/ui/button/action-button.tsx index be2947c248..91644dd1c8 100644 --- a/packages/components/src/ui/button/action-button.tsx +++ b/packages/components/src/ui/button/action-button.tsx @@ -10,7 +10,7 @@ import { KbdCombined } from "../kbd/Kbd" import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "../tooltip" export interface ActionButtonProps { - icon?: React.ReactNode | React.FC + icon?: React.ReactNode | ((props: { isActive?: boolean; className: string }) => React.ReactNode) tooltip?: React.ReactNode tooltipSide?: "top" | "bottom" active?: boolean @@ -74,7 +74,7 @@ export const ActionButton = React.forwardRef< onFocusCapture={stopPropagation} className={cn( "no-drag-region pointer-events-auto inline-flex items-center justify-center", - active && "bg-zinc-500/15 hover:bg-zinc-500/20", + active && typeof icon !== "function" && "bg-zinc-500/15 hover:bg-zinc-500/20", "rounded-md duration-200 hover:bg-theme-button-hover data-[state=open]:bg-theme-button-hover", "disabled:cursor-not-allowed disabled:opacity-50", clickableDisabled && "cursor-not-allowed opacity-50", @@ -103,6 +103,8 @@ export const ActionButton = React.forwardRef< ) : typeof icon === "function" ? ( React.createElement(icon, { className: "size-4 grayscale text-current", + + isActive: active, }) ) : ( icon