import { useCallback, useMemo } from "react"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query"; import { queryClient as appQueryClient } from "@/data/query-client"; import type { AppLanguage } from "@/i18n/locales"; import { DEFAULT_DESKTOP_SETTINGS, loadDesktopSettings, migrateLegacyDesktopSettings, useDesktopSettings, } from "@/desktop/settings/desktop-settings"; import { isElectronRuntime } from "@/desktop/host"; import { APP_SETTINGS_KEY, APP_SETTINGS_QUERY_KEY, DEFAULT_APP_SETTINGS, DEFAULT_CLIENT_SETTINGS, DEFAULT_CODE_FONT_SIZE, DEFAULT_CONTENT_FONT_SIZE, DEFAULT_TERMINAL_SCROLLBACK_LINES, DEFAULT_THEME_PREFERENCE, DEFAULT_UI_BASE_FONT_SIZE, MAX_CODE_FONT_SIZE, MAX_CONTENT_FONT_SIZE, MAX_TERMINAL_SCROLLBACK_LINES, MAX_UI_BASE_FONT_SIZE, MIN_CODE_FONT_SIZE, MIN_CONTENT_FONT_SIZE, MIN_TERMINAL_SCROLLBACK_LINES, MIN_UI_BASE_FONT_SIZE, loadAppSettingsFromStorage as loadAppSettingsFromStoragePure, loadSettingsFromStorage as loadSettingsFromStoragePure, normalizeAppSettings, parseClampedFontSize, parseTerminalScrollbackLines, sanitizeFontFamily, saveAppSettings as saveAppSettingsPure, type AppSettings, type DesktopSettingsBridge, type KeyValueStorage, type ReleaseChannel, type SendBehavior, type ServiceUrlBehavior, type Settings, type SidebarWorkspaceTrailing, type SettingsDeps, type WorkspaceTitleSource, } from "./storage"; export { APP_SETTINGS_KEY, DEFAULT_APP_SETTINGS, DEFAULT_CLIENT_SETTINGS, DEFAULT_CODE_FONT_SIZE, DEFAULT_CONTENT_FONT_SIZE, DEFAULT_TERMINAL_SCROLLBACK_LINES, DEFAULT_THEME_PREFERENCE, DEFAULT_UI_BASE_FONT_SIZE, MAX_CODE_FONT_SIZE, MAX_CONTENT_FONT_SIZE, MAX_TERMINAL_SCROLLBACK_LINES, MAX_UI_BASE_FONT_SIZE, MIN_CODE_FONT_SIZE, MIN_CONTENT_FONT_SIZE, MIN_TERMINAL_SCROLLBACK_LINES, MIN_UI_BASE_FONT_SIZE, parseClampedFontSize, parseTerminalScrollbackLines, sanitizeFontFamily, }; export type { AppSettings, AppLanguage, DesktopSettingsBridge, KeyValueStorage, ReleaseChannel, SendBehavior, ServiceUrlBehavior, Settings, SettingsDeps, SidebarWorkspaceTrailing, WorkspaceTitleSource, }; /** * Split a `Settings` patch into the part the app owns. The two halves persist to different * places (AsyncStorage vs the Electron settings bridge), and the app's half is exactly the * key set of `DEFAULT_CLIENT_SETTINGS` — reading the keys off it means a new app setting * flows through here without anyone remembering to widen a hand-written list. */ function pickDefinedAppSettings(updates: Partial): Partial { const appUpdates: Partial = {}; for (const key of Object.keys(DEFAULT_CLIENT_SETTINGS) as (keyof AppSettings)[]) { const value = updates[key]; if (value !== undefined) { Object.assign(appUpdates, { [key]: value }); } } return appUpdates; } const productionDeps: SettingsDeps = { storage: AsyncStorage, desktop: { isElectron: isElectronRuntime, loadDesktopSettings, migrateLegacyDesktopSettings, }, }; export interface UseAppSettingsReturn { settings: AppSettings; isLoading: boolean; error: unknown; updateSettings: (updates: Partial) => Promise; resetSettings: () => Promise; } export interface UseSettingsReturn { settings: Settings; isLoading: boolean; error: unknown; updateSettings: (updates: Partial) => Promise; resetSettings: () => Promise; } type SettingsSelector = (settings: Settings) => TSelected; export function useAppSettings(): UseAppSettingsReturn { const queryClient = useQueryClient(); const { data, isPending, error } = useQuery({ queryKey: APP_SETTINGS_QUERY_KEY, queryFn: () => loadAppSettingsFromStorage(), staleTime: Infinity, gcTime: Infinity, }); const updateSettings = useCallback( async (updates: Partial) => { try { await saveAppSettings({ queryClient, updates }); } catch (err) { console.error("[AppSettings] Failed to save settings:", err); throw err; } }, [queryClient], ); const resetSettings = useCallback(async () => { try { const next = { ...DEFAULT_CLIENT_SETTINGS }; queryClient.setQueryData(APP_SETTINGS_QUERY_KEY, next); await AsyncStorage.setItem(APP_SETTINGS_KEY, JSON.stringify(next)); } catch (err) { console.error("[AppSettings] Failed to reset settings:", err); throw err; } }, [queryClient]); const settings = useMemo(() => normalizeAppSettings(data), [data]); return { settings, isLoading: isPending, error: error ?? null, updateSettings, resetSettings, }; } export function useSettings(): UseSettingsReturn; export function useSettings(selector: SettingsSelector): TSelected; export function useSettings( selector?: SettingsSelector, ): UseSettingsReturn | TSelected { const appSettings = useAppSettings(); const desktopSettings = useDesktopSettings(); const updateSettings = useCallback( async (updates: Partial) => { const appUpdates = pickDefinedAppSettings(updates); const promises: Promise[] = []; if (Object.keys(appUpdates).length > 0) { promises.push(appSettings.updateSettings(appUpdates)); } if (isElectronRuntime()) { const desktopUpdates: Parameters[0] = {}; if (updates.manageBuiltInDaemon !== undefined) { desktopUpdates.daemon = { manageBuiltInDaemon: updates.manageBuiltInDaemon, }; } if (updates.releaseChannel !== undefined) { desktopUpdates.releaseChannel = updates.releaseChannel; } if (Object.keys(desktopUpdates).length > 0) { promises.push(desktopSettings.updateSettings(desktopUpdates)); } } await Promise.all(promises); }, [appSettings, desktopSettings], ); const resetSettings = useCallback(async () => { const resets: Promise[] = [appSettings.resetSettings()]; if (isElectronRuntime()) { resets.push(desktopSettings.updateSettings(DEFAULT_DESKTOP_SETTINGS)); } await Promise.all(resets); }, [appSettings, desktopSettings]); const settings = { ...DEFAULT_APP_SETTINGS, ...appSettings.settings, manageBuiltInDaemon: desktopSettings.settings.daemon.manageBuiltInDaemon, releaseChannel: desktopSettings.settings.releaseChannel, }; if (selector) { return selector(settings); } return { settings, isLoading: appSettings.isLoading || desktopSettings.isLoading, error: appSettings.error ?? desktopSettings.error, updateSettings, resetSettings, }; } export async function persistAppSettings(updates: Partial): Promise { await saveAppSettings({ queryClient: appQueryClient, updates }); } export async function saveAppSettings(input: { queryClient: QueryClient; updates: Partial; deps?: SettingsDeps; }): Promise { await saveAppSettingsPure({ queryClient: input.queryClient, updates: input.updates, deps: input.deps ?? productionDeps, }); } export async function loadAppSettingsFromStorage(deps?: SettingsDeps): Promise { return loadAppSettingsFromStoragePure(deps ?? productionDeps); } export async function loadSettingsFromStorage(deps?: SettingsDeps): Promise { return loadSettingsFromStoragePure(deps ?? productionDeps); }