From 9e08708ea4e3eebabc7c12c43cc505bca3597650 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Wed, 14 Oct 2026 06:44:41 -0400 Subject: [PATCH] refactor(renderer): relocate shortcut-registration store into stores Move the transient global-shortcut registration Zustand store out of the hooks layer into the stores layer, matching the documented state-ownership rule for session client state. - Add stores/useShortcutRegistrationStore.ts and mirror its ShortcutRegistrationStore type in stores/types.ts - Export it from the stores barrel - Trim hooks/useShortcutRegistration.ts to the effect hook, importing the store from the stores layer - Repoint the seven consumers (GlobalEffects, SystemSettings, and the test setup/helpers) to the barrel No behavior change: registration, revert-on-failure, and the surfaced error are identical. --- src/renderer/__helpers__/test-utils.tsx | 9 +++- src/renderer/__helpers__/visual.setup.ts | 2 +- src/renderer/__helpers__/vitest.setup.ts | 2 +- .../components/GlobalEffects.test.tsx | 3 +- src/renderer/components/GlobalEffects.tsx | 8 ++-- .../components/settings/SystemSettings.tsx | 7 +++- .../hooks/useShortcutRegistration.test.ts | 4 +- src/renderer/hooks/useShortcutRegistration.ts | 42 +------------------ src/renderer/stores/index.ts | 1 + src/renderer/stores/types.ts | 27 ++++++++++++ .../stores/useShortcutRegistrationStore.ts | 27 ++++++++++++ 11 files changed, 78 insertions(+), 54 deletions(-) create mode 100644 src/renderer/stores/useShortcutRegistrationStore.ts diff --git a/src/renderer/__helpers__/test-utils.tsx b/src/renderer/__helpers__/test-utils.tsx index 0455dbbb1..53bf781fd 100644 --- a/src/renderer/__helpers__/test-utils.tsx +++ b/src/renderer/__helpers__/test-utils.tsx @@ -4,8 +4,13 @@ import { type InitialEntry, MemoryRouter } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { useShortcutRegistrationStore } from '../hooks/useShortcutRegistration'; -import { type FiltersStore, useAccountsStore, useFiltersStore, useSettingsStore } from '../stores'; +import { + type FiltersStore, + useAccountsStore, + useFiltersStore, + useSettingsStore, + useShortcutRegistrationStore, +} from '../stores'; import type { Account, SettingsState } from '../types'; diff --git a/src/renderer/__helpers__/visual.setup.ts b/src/renderer/__helpers__/visual.setup.ts index 8a9c09476..988cfcbe7 100644 --- a/src/renderer/__helpers__/visual.setup.ts +++ b/src/renderer/__helpers__/visual.setup.ts @@ -162,7 +162,7 @@ beforeEach(async () => { const { resetHookMocks } = await import('./hook-mocks'); resetHookMocks(); - const { useShortcutRegistrationStore } = await import('../hooks/useShortcutRegistration'); + const { useShortcutRegistrationStore } = await import('../stores'); useShortcutRegistrationStore.getState().reset(); }); diff --git a/src/renderer/__helpers__/vitest.setup.ts b/src/renderer/__helpers__/vitest.setup.ts index 8245e34a2..b9e46155b 100644 --- a/src/renderer/__helpers__/vitest.setup.ts +++ b/src/renderer/__helpers__/vitest.setup.ts @@ -153,7 +153,7 @@ beforeEach(async () => { const { resetHookMocks } = await import('./hook-mocks'); resetHookMocks(); - const { useShortcutRegistrationStore } = await import('../hooks/useShortcutRegistration'); + const { useShortcutRegistrationStore } = await import('../stores'); useShortcutRegistrationStore.getState().reset(); }); diff --git a/src/renderer/components/GlobalEffects.test.tsx b/src/renderer/components/GlobalEffects.test.tsx index 08be84dfc..9f83546f7 100644 --- a/src/renderer/components/GlobalEffects.test.tsx +++ b/src/renderer/components/GlobalEffects.test.tsx @@ -3,8 +3,7 @@ import { act } from '@testing-library/react'; import { renderWithProviders } from '../__helpers__/test-utils'; import { mockGitHubCloudAccount } from '../__mocks__/account-mocks'; -import { useShortcutRegistrationStore } from '../hooks/useShortcutRegistration'; -import { useAccountsStore, useSettingsStore } from '../stores'; +import { useAccountsStore, useSettingsStore, useShortcutRegistrationStore } from '../stores'; import { Theme } from '../types'; diff --git a/src/renderer/components/GlobalEffects.tsx b/src/renderer/components/GlobalEffects.tsx index 2114906d7..f44699a31 100644 --- a/src/renderer/components/GlobalEffects.tsx +++ b/src/renderer/components/GlobalEffects.tsx @@ -6,11 +6,13 @@ import { useNotificationsHost } from '../hooks/notifications/useNotificationsHos import { useAccounts } from '../hooks/useAccounts'; import { useAppearance } from '../hooks/useAppearance'; import { useOnlineStatus } from '../hooks/useOnlineStatus'; +import { useShortcutRegistration } from '../hooks/useShortcutRegistration'; import { - useShortcutRegistration, + useAccountsStore, + useFiltersStore, + useSettingsStore, useShortcutRegistrationStore, -} from '../hooks/useShortcutRegistration'; -import { useAccountsStore, useFiltersStore, useSettingsStore } from '../stores'; +} from '../stores'; import { setTrayIconColorAndTitle } from '../utils/system/tray'; diff --git a/src/renderer/components/settings/SystemSettings.tsx b/src/renderer/components/settings/SystemSettings.tsx index 0a29cba44..2702bbf9c 100644 --- a/src/renderer/components/settings/SystemSettings.tsx +++ b/src/renderer/components/settings/SystemSettings.tsx @@ -5,8 +5,11 @@ import { Banner, Button, ButtonGroup, IconButton, Stack, Text } from '@primer/re import { APPLICATION } from '../../../shared/constants'; -import { useShortcutRegistrationStore } from '../../hooks/useShortcutRegistration'; -import { DEFAULT_SETTINGS_STATE, useSettingsStore } from '../../stores'; +import { + DEFAULT_SETTINGS_STATE, + useSettingsStore, + useShortcutRegistrationStore, +} from '../../stores'; import { Checkbox } from '../fields/Checkbox'; import { RadioGroup } from '../fields/RadioGroup'; diff --git a/src/renderer/hooks/useShortcutRegistration.test.ts b/src/renderer/hooks/useShortcutRegistration.test.ts index 790a9bc07..6ea11b7d1 100644 --- a/src/renderer/hooks/useShortcutRegistration.test.ts +++ b/src/renderer/hooks/useShortcutRegistration.test.ts @@ -1,10 +1,10 @@ import { renderHook, waitFor } from '@testing-library/react'; -import { DEFAULT_SETTINGS_STATE, useSettingsStore } from '../stores'; +import { DEFAULT_SETTINGS_STATE, useSettingsStore, useShortcutRegistrationStore } from '../stores'; import type { KeyboardAcceleratorShortcut } from '../types'; -import { useShortcutRegistration, useShortcutRegistrationStore } from './useShortcutRegistration'; +import { useShortcutRegistration } from './useShortcutRegistration'; describe('renderer/hooks/useShortcutRegistration.ts', () => { it('registers the configured shortcut and records it as last applied', async () => { diff --git a/src/renderer/hooks/useShortcutRegistration.ts b/src/renderer/hooks/useShortcutRegistration.ts index d6665b415..8c00504c5 100644 --- a/src/renderer/hooks/useShortcutRegistration.ts +++ b/src/renderer/hooks/useShortcutRegistration.ts @@ -1,49 +1,9 @@ import { useEffect } from 'react'; -import { create } from 'zustand'; - -import { DEFAULT_SETTINGS_STATE, useSettingsStore } from '../stores'; - -import type { KeyboardAcceleratorShortcut } from '../types'; +import { useSettingsStore, useShortcutRegistrationStore } from '../stores'; import { applyKeyboardShortcut } from '../utils/system/comms'; -interface ShortcutRegistrationStore { - /** - * User-facing error when the configured global shortcut could not be - * registered (e.g. already in use by another application). - */ - shortcutRegistrationError: string | null; - - /** - * The last accelerator that registered successfully; used to revert the - * setting when a newly chosen accelerator fails to register. - */ - lastAppliedShortcut: KeyboardAcceleratorShortcut; - - setShortcutRegistrationError: (error: string | null) => void; - setLastAppliedShortcut: (accelerator: KeyboardAcceleratorShortcut) => void; - clearShortcutRegistrationError: () => void; - reset: () => void; -} - -/** - * Transient (non-persisted) state for global keyboard shortcut registration. - */ -export const useShortcutRegistrationStore = create()((set) => ({ - shortcutRegistrationError: null, - lastAppliedShortcut: DEFAULT_SETTINGS_STATE.openGitifyShortcut, - - setShortcutRegistrationError: (error) => set({ shortcutRegistrationError: error }), - setLastAppliedShortcut: (accelerator) => set({ lastAppliedShortcut: accelerator }), - clearShortcutRegistrationError: () => set({ shortcutRegistrationError: null }), - reset: () => - set({ - shortcutRegistrationError: null, - lastAppliedShortcut: DEFAULT_SETTINGS_STATE.openGitifyShortcut, - }), -})); - /** * Registers the global open/close shortcut whenever the setting changes, * reverting to the last working accelerator when registration fails. diff --git a/src/renderer/stores/index.ts b/src/renderer/stores/index.ts index 7fcb033af..210a0e78e 100644 --- a/src/renderer/stores/index.ts +++ b/src/renderer/stores/index.ts @@ -8,3 +8,4 @@ export { getNotificationFailureKey, } from './useNotificationActionFailuresStore'; export { default as useSettingsStore } from './useSettingsStore'; +export { default as useShortcutRegistrationStore } from './useShortcutRegistrationStore'; diff --git a/src/renderer/stores/types.ts b/src/renderer/stores/types.ts index 22ac2508e..c7ca4575f 100644 --- a/src/renderer/stores/types.ts +++ b/src/renderer/stores/types.ts @@ -5,6 +5,7 @@ import type { Forge, GitifyError, Hostname, + KeyboardAcceleratorShortcut, ReasonFilterValue, ReviewRequestTypeFilterValue, SearchToken, @@ -273,3 +274,29 @@ export interface NotificationActionFailuresActions { */ export type NotificationActionFailuresStore = NotificationActionFailuresState & NotificationActionFailuresActions; + +// ============================================================================ +// Shortcut Registration Store Types +// ============================================================================ + +/** + * Transient (non-persisted) state for global keyboard shortcut registration. + */ +export interface ShortcutRegistrationStore { + /** + * User-facing error when the configured global shortcut could not be + * registered (e.g. already in use by another application). + */ + shortcutRegistrationError: string | null; + + /** + * The last accelerator that registered successfully; used to revert the + * setting when a newly chosen accelerator fails to register. + */ + lastAppliedShortcut: KeyboardAcceleratorShortcut; + + setShortcutRegistrationError: (error: string | null) => void; + setLastAppliedShortcut: (accelerator: KeyboardAcceleratorShortcut) => void; + clearShortcutRegistrationError: () => void; + reset: () => void; +} diff --git a/src/renderer/stores/useShortcutRegistrationStore.ts b/src/renderer/stores/useShortcutRegistrationStore.ts new file mode 100644 index 000000000..c9545a576 --- /dev/null +++ b/src/renderer/stores/useShortcutRegistrationStore.ts @@ -0,0 +1,27 @@ +import { create } from 'zustand'; + +import type { ShortcutRegistrationStore } from './types'; + +import { DEFAULT_SETTINGS_STATE } from './defaults'; + +/** + * Transient (non-persisted) state for global keyboard shortcut registration. + * + * Lives in the stores layer alongside the other client state; the + * `useShortcutRegistration` effect hook reads it from the stores barrel. + */ +const useShortcutRegistrationStore = create()((set) => ({ + shortcutRegistrationError: null, + lastAppliedShortcut: DEFAULT_SETTINGS_STATE.openGitifyShortcut, + + setShortcutRegistrationError: (error) => set({ shortcutRegistrationError: error }), + setLastAppliedShortcut: (accelerator) => set({ lastAppliedShortcut: accelerator }), + clearShortcutRegistrationError: () => set({ shortcutRegistrationError: null }), + reset: () => + set({ + shortcutRegistrationError: null, + lastAppliedShortcut: DEFAULT_SETTINGS_STATE.openGitifyShortcut, + }), +})); + +export default useShortcutRegistrationStore;