diff --git a/apps/renderer/src/modules/command/commands/entry.tsx b/apps/renderer/src/modules/command/commands/entry.tsx index d49e3495d2..b6a14465fe 100644 --- a/apps/renderer/src/modules/command/commands/entry.tsx +++ b/apps/renderer/src/modules/command/commands/entry.tsx @@ -13,8 +13,7 @@ import { tipcClient } from "~/lib/client" import { useTipModal } from "~/modules/wallet/hooks" import { entryActions, useEntryStore } from "~/store/entry" -import { useRegisterCommandEffect } from "../hooks/use-register-command-effect" -import { defineFollowCommand } from "../registry/command" +import { useRegisterFollowCommand } from "../hooks/use-register-command" import { COMMAND_ID } from "./id" const useCollect = () => { @@ -89,8 +88,8 @@ export const useRegisterEntryCommands = () => { const read = useRead() const unread = useUnread() - useRegisterCommandEffect([ - defineFollowCommand({ + useRegisterFollowCommand([ + { id: COMMAND_ID.entry.tip, label: t("entry_actions.tip"), icon: , @@ -105,8 +104,8 @@ export const useRegisterEntryCommands = () => { }), ) }, - }), - defineFollowCommand({ + }, + { id: COMMAND_ID.entry.star, label: t("entry_actions.star"), icon: , @@ -127,8 +126,8 @@ export const useRegisterEntryCommands = () => { // } collect.mutate({ entryId, view }) }, - }), - defineFollowCommand({ + }, + { id: COMMAND_ID.entry.unstar, label: t("entry_actions.unstar"), icon: , @@ -140,8 +139,8 @@ export const useRegisterEntryCommands = () => { } uncollect.mutate(entry.entries.id) }, - }), - defineFollowCommand({ + }, + { id: COMMAND_ID.entry.delete, label: t("entry_actions.delete"), icon: , @@ -153,8 +152,8 @@ export const useRegisterEntryCommands = () => { } deleteInboxEntry.mutate(entry.entries.id) }, - }), - defineFollowCommand({ + }, + { id: COMMAND_ID.entry.copyLink, label: t("entry_actions.copy_link"), icon: , @@ -170,8 +169,8 @@ export const useRegisterEntryCommands = () => { duration: 1000, }) }, - }), - defineFollowCommand({ + }, + { id: COMMAND_ID.entry.copyTitle, label: t("entry_actions.copy_title"), icon: , @@ -187,8 +186,8 @@ export const useRegisterEntryCommands = () => { duration: 1000, }) }, - }), - defineFollowCommand({ + }, + { id: COMMAND_ID.entry.openInBrowser, label: t("entry_actions.open_in_browser", { which: t(IN_ELECTRON ? "words.browser" : "words.newTab"), @@ -202,8 +201,8 @@ export const useRegisterEntryCommands = () => { } window.open(entry.entries.url, "_blank") }, - }), - defineFollowCommand({ + }, + { id: COMMAND_ID.entry.viewSourceContent, label: t("entry_actions.view_source_content"), icon: , @@ -232,16 +231,16 @@ export const useRegisterEntryCommands = () => { } setShowSourceContent(true) }, - }), - defineFollowCommand({ + }, + { id: COMMAND_ID.entry.viewEntryContent, label: t("entry_actions.view_source_content"), icon: , run: () => { setShowSourceContent(false) }, - }), - defineFollowCommand({ + }, + { id: COMMAND_ID.entry.share, label: t("entry_actions.share"), icon: @@ -267,8 +266,8 @@ export const useRegisterEntryCommands = () => { } return }, - }), - defineFollowCommand({ + }, + { id: COMMAND_ID.entry.read, label: t("entry_actions.mark_as_read"), icon: , @@ -280,8 +279,8 @@ export const useRegisterEntryCommands = () => { } read.mutate({ entryId, feedId: entry.feedId }) }, - }), - defineFollowCommand({ + }, + { id: COMMAND_ID.entry.unread, label: t("entry_actions.mark_as_unread"), icon: , @@ -293,6 +292,6 @@ export const useRegisterEntryCommands = () => { } unread.mutate({ entryId, feedId: entry.feedId }) }, - }), + }, ]) } diff --git a/apps/renderer/src/modules/command/commands/integration.tsx b/apps/renderer/src/modules/command/commands/integration.tsx index 87595f26c2..3a656164a0 100644 --- a/apps/renderer/src/modules/command/commands/integration.tsx +++ b/apps/renderer/src/modules/command/commands/integration.tsx @@ -21,7 +21,7 @@ import { parseHtml } from "~/lib/parse-html" import type { FlatEntryModel } from "~/store/entry" import { useEntryStore } from "~/store/entry" -import { useRegisterCommandEffect } from "../hooks/use-register-command-effect" +import { useRegisterCommandEffect } from "../hooks/use-register-command" import { defineFollowCommand } from "../registry/command" import { COMMAND_ID } from "./id" diff --git a/apps/renderer/src/modules/command/commands/list.tsx b/apps/renderer/src/modules/command/commands/list.tsx index 77e69e99a5..17d40c477a 100644 --- a/apps/renderer/src/modules/command/commands/list.tsx +++ b/apps/renderer/src/modules/command/commands/list.tsx @@ -9,7 +9,7 @@ import { getRouteParams } from "~/hooks/biz/useRouteParams" import { useDeleteSubscription } from "~/hooks/biz/useSubscriptionActions" import { ListForm } from "~/modules/discover/list-form" -import { useRegisterCommandEffect } from "../hooks/use-register-command-effect" +import { useRegisterCommandEffect } from "../hooks/use-register-command" import { COMMAND_ID } from "./id" export const useRegisterListCommands = () => { diff --git a/apps/renderer/src/modules/command/commands/theme.tsx b/apps/renderer/src/modules/command/commands/theme.tsx index 4fa2c8aa2b..8757b503cb 100644 --- a/apps/renderer/src/modules/command/commands/theme.tsx +++ b/apps/renderer/src/modules/command/commands/theme.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next" import { useSetTheme } from "~/hooks/common" -import { useRegisterCommandEffect } from "../hooks/use-register-command-effect" +import { useRegisterCommandEffect } from "../hooks/use-register-command" export const useRegisterThemeCommands = () => { const [t] = useTranslation("settings") diff --git a/apps/renderer/src/modules/command/hooks/use-register-command-effect.ts b/apps/renderer/src/modules/command/hooks/use-register-command-effect.ts deleted file mode 100644 index f7c5ff114b..0000000000 --- a/apps/renderer/src/modules/command/hooks/use-register-command-effect.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect } from "react" - -import { registerCommand } from "../registry/registry" -import type { CommandOptions } from "../types" - -export type RegisterOptions = { - deps?: unknown[] - enabled?: boolean - // forceMountSection?: boolean - // sectionMeta?: Record - // orderSection?: OrderSectionInstruction - // orderCommands?: OrderCommandsInstruction -} - -export const useRegisterCommandEffect = ( - options: CommandOptions | CommandOptions[], - registerOptions?: RegisterOptions, -) => { - // TODO memo command via useMemo - // See https://github.com/supabase/supabase/blob/master/packages/ui-patterns/CommandMenu/api/hooks/commandsHooks.ts - - useEffect(() => { - if (!Array.isArray(options)) { - return registerCommand(options) - } - - const unsubscribes = options.map((option) => registerCommand(option)) - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()) - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- we want to run this effect only once - }, registerOptions?.deps ?? []) -} 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 new file mode 100644 index 0000000000..c4264195cc --- /dev/null +++ b/apps/renderer/src/modules/command/hooks/use-register-command.test-d.ts @@ -0,0 +1,83 @@ +import { assertType, expectTypeOf, test } from "vitest" + +import { COMMAND_ID } from "../commands/id" +import { useRegisterFollowCommand } from "./use-register-command" + +test("useRegisterFollowCommand types", () => { + assertType( + useRegisterFollowCommand({ + id: COMMAND_ID.entry.openInBrowser, + label: "", + run: ({ entryId }) => { + expectTypeOf(entryId).toEqualTypeOf() + }, + }), + ) + + assertType( + useRegisterFollowCommand({ + id: "unknown id", + label: "", + run: (...args) => { + expectTypeOf(args).toEqualTypeOf<[]>() + }, + }), + ) + + assertType( + useRegisterFollowCommand([ + { + id: COMMAND_ID.entry.star, + label: "", + run: ({ entryId }) => { + expectTypeOf(entryId).toEqualTypeOf() + }, + }, + { + id: COMMAND_ID.entry.viewEntryContent, + label: "", + run: (...args) => { + expectTypeOf(args).toEqualTypeOf<[]>() + }, + }, + ]), + ) + + assertType( + useRegisterFollowCommand([ + { + id: "unknown id", + label: "", + run: (...args) => { + expectTypeOf(args).toEqualTypeOf<[]>() + }, + }, + ]), + ) + + assertType( + useRegisterFollowCommand([ + { + id: "unknown id", + label: "", + run: (...args) => { + expectTypeOf(args).toEqualTypeOf<[]>() + }, + }, + { + id: COMMAND_ID.entry.star, + label: "", + run: ({ entryId }) => { + expectTypeOf(entryId).toEqualTypeOf() + }, + }, + { + id: COMMAND_ID.entry.viewEntryContent, + label: "", + run: (...args) => { + expectTypeOf(args).toEqualTypeOf<[]>() + }, + }, + ]), + ) +}) diff --git a/apps/renderer/src/modules/command/hooks/use-register-command.ts b/apps/renderer/src/modules/command/hooks/use-register-command.ts new file mode 100644 index 0000000000..6eeb9f6ec4 --- /dev/null +++ b/apps/renderer/src/modules/command/hooks/use-register-command.ts @@ -0,0 +1,66 @@ +import { useEffect } from "react" +import { useTranslation } from "react-i18next" + +import { registerCommand } from "../registry/registry" +import type { CommandOptions, FollowCommandId, FollowCommandMap } from "../types" + +export type RegisterOptions = { + deps?: unknown[] + enabled?: boolean + // forceMountSection?: boolean + // sectionMeta?: Record + // orderSection?: OrderSectionInstruction + // orderCommands?: OrderCommandsInstruction +} + +export const useRegisterCommandEffect = ( + options: CommandOptions | CommandOptions[], + registerOptions?: RegisterOptions, +) => { + const { t } = useTranslation() + useEffect(() => { + if (!Array.isArray(options)) { + return registerCommand(options) + } + + const unsubscribes = options.map((option) => registerCommand(option)) + return () => { + unsubscribes.forEach((unsubscribe) => unsubscribe()) + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- we want to run this effect only once + }, [t, ...(registerOptions?.deps ?? [])]) +} + +/** + * Register a follow command. + */ +export function useRegisterFollowCommand( + options: CommandOptions<{ id: T; fn: FollowCommandMap[T]["run"] }>, + registerOptions?: RegisterOptions, +): void +/** + * Register a unknown command. + */ +export function useRegisterFollowCommand( + options: CommandOptions<{ id: T; fn: () => void }>, + registerOptions?: RegisterOptions, +): void +/** + * Register multiple follow commands or unknown commands. + */ +export function useRegisterFollowCommand( + options: [ + ...{ + [K in keyof T]: T[K] extends FollowCommandId + ? CommandOptions<{ id: T[K]; fn: FollowCommandMap[T[K]]["run"] }> + : CommandOptions<{ id: T[K]; fn: () => void }> + }, + ], + registerOptions?: RegisterOptions, +): void +export function useRegisterFollowCommand( + options: CommandOptions | CommandOptions[], + registerOptions?: RegisterOptions, +) { + return useRegisterCommandEffect(options as CommandOptions | CommandOptions[], registerOptions) +} 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 4627ba6385..ec00d544c7 100644 --- a/apps/renderer/src/modules/command/registry/command.test-d.ts +++ b/apps/renderer/src/modules/command/registry/command.test-d.ts @@ -1,7 +1,7 @@ import { assertType, expectTypeOf, test } from "vitest" import { COMMAND_ID } from "../commands/id" -import { defineCommandArgsArray, defineFollowCommand, defineFollowCommandArgs } from "./command" +import { defineFollowCommand } from "./command" test("defineFollowCommand types", () => { assertType( @@ -68,71 +68,3 @@ test("defineFollowCommand with keyBinding types", () => { }), ) }) - -test("defineCommandArgs with keyBinding types", () => { - assertType( - defineFollowCommandArgs({ - commandId: COMMAND_ID.entry.star, - args: [{ entryId: "1" }], - }), - ) - - assertType( - defineFollowCommandArgs({ - commandId: COMMAND_ID.entry.star, - // @ts-expect-error - invalid args - args: [], - }), - ) -}) - -test("defineCommandArgsArray with keyBinding types", () => { - assertType( - defineCommandArgsArray([ - { - commandId: COMMAND_ID.entry.star, - args: [{ entryId: "1" }], - }, - ]), - ) - - assertType( - defineCommandArgsArray([ - { - commandId: COMMAND_ID.entry.star, - // @ts-expect-error - invalid args - args: [], - }, - ]), - ) - - assertType( - defineCommandArgsArray([ - { - commandId: COMMAND_ID.entry.star, - // @ts-expect-error - invalid args - args: [], - }, - { - commandId: COMMAND_ID.entry.viewEntryContent, - args: [], - }, - ]), - ) - - assertType( - defineCommandArgsArray<{ test: boolean }>([ - { - commandId: COMMAND_ID.entry.star, - args: [{ entryId: "1" }], - test: true, - }, - { - commandId: COMMAND_ID.entry.viewEntryContent, - args: [], - // @ts-expect-error - invalid extra property - test: 1, - }, - ]), - ) -}) diff --git a/apps/renderer/src/modules/command/registry/command.ts b/apps/renderer/src/modules/command/registry/command.ts index ebea2b9868..de30ab791a 100644 --- a/apps/renderer/src/modules/command/registry/command.ts +++ b/apps/renderer/src/modules/command/registry/command.ts @@ -1,4 +1,10 @@ -import type { Command, CommandOptions, FollowCommand, FollowCommandId } from "../types" +import type { + Command, + CommandOptions, + FollowCommand, + FollowCommandId, + FollowCommandMap, +} from "../types" export function createCommand< T extends { id: string; fn: (...args: any[]) => unknown } = { @@ -32,33 +38,7 @@ export function createFollowCommand( } export function defineFollowCommand( - options: CommandOptions<{ id: T; fn: Extract["run"] }>, + options: CommandOptions<{ id: T; fn: FollowCommandMap[T]["run"] }>, ) { return options as CommandOptions } - -/** - * @deprecated - */ -export const defineFollowCommandArgs = (config: { - commandId: T - args: Parameters["run"]> -}) => config - -/** - * @deprecated - */ -export const defineCommandArgsArray = < - Ext extends Record, - T extends FollowCommandId[] = FollowCommandId[], ->( - config: [ - ...{ - [K in keyof T]: { - commandId: T[K] - args: Parameters["run"]> - // [key: string]: unknown - } & Ext - }, - ], -) => config