From 2f9d69910775ff8e4753abc2c463623733300806 Mon Sep 17 00:00:00 2001 From: Haileyesus Date: Fri, 6 Feb 2026 11:22:48 +0300 Subject: [PATCH] refactor: replace useLocalStorage with useUiPreferences for better state management in AppContent --- src/App.jsx | 26 +++-- src/hooks/useUiPreferences.ts | 176 ++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 15 deletions(-) create mode 100644 src/hooks/useUiPreferences.ts diff --git a/src/App.jsx b/src/App.jsx index f651c05..5a0fde0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -32,7 +32,7 @@ import { TaskMasterProvider } from './contexts/TaskMasterContext'; import { TasksSettingsProvider } from './contexts/TasksSettingsContext'; import { WebSocketProvider, useWebSocket } from './contexts/WebSocketContext'; import ProtectedRoute from './components/ProtectedRoute'; -import useLocalStorage from './hooks/useLocalStorage'; +import { useUiPreferences } from './hooks/useUiPreferences'; import { api, authenticatedFetch } from './utils/api'; import { I18nextProvider, useTranslation } from 'react-i18next'; import i18n from './i18n/config.js'; @@ -60,12 +60,8 @@ function AppContent() { const [showSettings, setShowSettings] = useState(false); const [settingsInitialTab, setSettingsInitialTab] = useState('agents'); const [showQuickSettings, setShowQuickSettings] = useState(false); - const [autoExpandTools, setAutoExpandTools] = useLocalStorage('autoExpandTools', false); - const [showRawParameters, setShowRawParameters] = useLocalStorage('showRawParameters', false); - const [showThinking, setShowThinking] = useLocalStorage('showThinking', true); - const [autoScrollToBottom, setAutoScrollToBottom] = useLocalStorage('autoScrollToBottom', true); - const [sendByCtrlEnter, setSendByCtrlEnter] = useLocalStorage('sendByCtrlEnter', false); - const [sidebarVisible, setSidebarVisible] = useLocalStorage('sidebarVisible', true); + const { preferences, setPreference } = useUiPreferences(); + const { autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter, sidebarVisible } = preferences; // Session Protection System: Track sessions with active conversations to prevent // automatic project updates from interrupting ongoing chats. When a user sends // a message, the session is marked as "active" and project updates are paused @@ -600,9 +596,9 @@ function AppContent() { onShowSettings={() => setShowSettings(true)} isPWA={isPWA} isMobile={isMobile} - onToggleSidebar={() => setSidebarVisible(false)} + onToggleSidebar={() => setPreference('sidebarVisible', false)} isCollapsed={!sidebarVisible} - onExpandSidebar={() => setSidebarVisible(true)} + onExpandSidebar={() => setPreference('sidebarVisible', true)} /> )} @@ -647,7 +643,7 @@ function AppContent() { onShowSettings={() => setShowSettings(true)} isPWA={isPWA} isMobile={isMobile} - onToggleSidebar={() => setSidebarVisible(false)} + onToggleSidebar={() => setPreference('sidebarVisible', false)} isCollapsed={false} /> @@ -700,15 +696,15 @@ function AppContent() { isOpen={showQuickSettings} onToggle={setShowQuickSettings} autoExpandTools={autoExpandTools} - onAutoExpandChange={setAutoExpandTools} + onAutoExpandChange={(value) => setPreference('autoExpandTools', value)} showRawParameters={showRawParameters} - onShowRawParametersChange={setShowRawParameters} + onShowRawParametersChange={(value) => setPreference('showRawParameters', value)} showThinking={showThinking} - onShowThinkingChange={setShowThinking} + onShowThinkingChange={(value) => setPreference('showThinking', value)} autoScrollToBottom={autoScrollToBottom} - onAutoScrollChange={setAutoScrollToBottom} + onAutoScrollChange={(value) => setPreference('autoScrollToBottom', value)} sendByCtrlEnter={sendByCtrlEnter} - onSendByCtrlEnterChange={setSendByCtrlEnter} + onSendByCtrlEnterChange={(value) => setPreference('sendByCtrlEnter', value)} isMobile={isMobile} /> )} diff --git a/src/hooks/useUiPreferences.ts b/src/hooks/useUiPreferences.ts new file mode 100644 index 0000000..00d592e --- /dev/null +++ b/src/hooks/useUiPreferences.ts @@ -0,0 +1,176 @@ +import { useEffect, useReducer } from 'react'; + +type UiPreferences = { + autoExpandTools: boolean; + showRawParameters: boolean; + showThinking: boolean; + autoScrollToBottom: boolean; + sendByCtrlEnter: boolean; + sidebarVisible: boolean; +}; + +type UiPreferenceKey = keyof UiPreferences; + +type SetPreferenceAction = { + type: 'set'; + key: UiPreferenceKey; + value: unknown; +}; + +type SetManyPreferencesAction = { + type: 'set_many'; + value?: Partial>; +}; + +type ResetPreferencesAction = { + type: 'reset'; + value?: Partial; +}; + +type UiPreferencesAction = + | SetPreferenceAction + | SetManyPreferencesAction + | ResetPreferencesAction; + +const DEFAULTS: UiPreferences = { + autoExpandTools: false, + showRawParameters: false, + showThinking: true, + autoScrollToBottom: true, + sendByCtrlEnter: false, + sidebarVisible: true, +}; + +const PREFERENCE_KEYS = Object.keys(DEFAULTS) as UiPreferenceKey[]; +const VALID_KEYS = new Set(PREFERENCE_KEYS); // prevents unknown keys from being written + +const parseBoolean = (value: unknown, fallback: boolean): boolean => { + if (typeof value === 'boolean') { + return value; + } + + if (typeof value === 'string') { + if (value === 'true') return true; + if (value === 'false') return false; + } + + return fallback; +}; + +const readLegacyPreference = (key: UiPreferenceKey, fallback: boolean): boolean => { + try { + const raw = localStorage.getItem(key); + if (raw === null) return fallback; + + // Supports values written by both JSON.stringify and plain strings. + const parsed = JSON.parse(raw); + return parseBoolean(parsed, fallback); + } catch { + return fallback; + } +}; + +const readInitialPreferences = (storageKey: string): UiPreferences => { + if (typeof window === 'undefined') { + return DEFAULTS; + } + + try { + const raw = localStorage.getItem(storageKey); + + if (raw) { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + const parsedRecord = parsed as Record; + + return PREFERENCE_KEYS.reduce((acc, key) => { + acc[key] = parseBoolean(parsedRecord[key], DEFAULTS[key]); + return acc; + }, { ...DEFAULTS }); + } + } + } catch { + // Fall back to legacy keys when unified key is missing or invalid. + } + + return PREFERENCE_KEYS.reduce((acc, key) => { + acc[key] = readLegacyPreference(key, DEFAULTS[key]); + return acc; + }, { ...DEFAULTS }); +}; + +function reducer(state: UiPreferences, action: UiPreferencesAction): UiPreferences { + switch (action.type) { + case 'set': { + const { key, value } = action; + if (!VALID_KEYS.has(key)) { + return state; + } + + const nextValue = parseBoolean(value, state[key]); + if (state[key] === nextValue) { + return state; + } + + return { ...state, [key]: nextValue }; + } + case 'set_many': { + const updates = action.value || {}; + let changed = false; + const nextState = { ...state }; + + for (const key of PREFERENCE_KEYS) { + if (!(key in updates)) continue; + + const value = updates[key]; + const nextValue = parseBoolean(value, state[key]); + if (nextState[key] !== nextValue) { + nextState[key] = nextValue; + changed = true; + } + } + + return changed ? nextState : state; + } + case 'reset': + return { ...DEFAULTS, ...(action.value || {}) }; + default: + return state; + } +} + +export function useUiPreferences(storageKey = 'uiPreferences') { + const [state, dispatch] = useReducer( + reducer, + storageKey, + readInitialPreferences + ); + + useEffect(() => { + if (typeof window === 'undefined') { + return; + } + + localStorage.setItem(storageKey, JSON.stringify(state)); + }, [state, storageKey]); + + const setPreference = (key: UiPreferenceKey, value: unknown) => { + dispatch({ type: 'set', key, value }); + }; + + const setPreferences = (value: Partial>) => { + dispatch({ type: 'set_many', value }); + }; + + const resetPreferences = (value?: Partial) => { + dispatch({ type: 'reset', value }); + }; + + return { + preferences: state, + setPreference, + setPreferences, + resetPreferences, + dispatch, + }; +}