From 856033a9f68813ccf7349da986fa951c2319cfcd Mon Sep 17 00:00:00 2001 From: Haileyesus Date: Fri, 6 Feb 2026 12:45:05 +0300 Subject: [PATCH] refactor: extract device detection into hook and localize PWA handling to Sidebar - Add new `useDeviceSettings` hook (`src/hooks/useDeviceSettings.ts`) to centralize device-related state: - exposes `isMobile` and `isPWA` - supports options: `mobileBreakpoint`, `trackMobile`, `trackPWA` - listens to window resize for mobile updates - listens to `display-mode: standalone` changes for PWA updates - includes `matchMedia.addListener/removeListener` fallback for older environments - Update `AppContent` (`src/App.jsx`) to consume `isMobile` from `useDeviceSettings({ trackPWA: false })`: - remove local `isMobile` state/effect - remove local `isPWA` state/effect - keep existing `isMobile` behavior for layout and mobile sidebar flow - stop passing `isPWA` into `Sidebar` props - Update `Sidebar` (`src/components/Sidebar.jsx`) to own PWA detection: - consume `isPWA` from `useDeviceSettings({ trackMobile: false })` - add effect to toggle `pwa-mode` class on `document.documentElement` and `document.body` - retain use of `isMobile` prop from `App` for sidebar/mobile rendering decisions Why: - removes duplicated device-detection logic from `AppContent` - makes device-state logic reusable and easier to maintain - keeps PWA-specific behavior where it is actually used (`Sidebar`) --- src/App.jsx | 48 +------------------ src/components/Sidebar.jsx | 12 ++++- src/hooks/useDeviceSettings.ts | 88 ++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 47 deletions(-) create mode 100644 src/hooks/useDeviceSettings.ts diff --git a/src/App.jsx b/src/App.jsx index 660f418..9b4cf96 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -32,6 +32,7 @@ import { TaskMasterProvider } from './contexts/TaskMasterContext'; import { TasksSettingsProvider } from './contexts/TasksSettingsContext'; import { WebSocketProvider, useWebSocket } from './contexts/WebSocketContext'; import ProtectedRoute from './components/ProtectedRoute'; +import { useDeviceSettings } from './hooks/useDeviceSettings'; import { api, authenticatedFetch } from './utils/api'; import { I18nextProvider, useTranslation } from 'react-i18next'; import i18n from './i18n/config.js'; @@ -51,7 +52,7 @@ function AppContent() { const [selectedProject, setSelectedProject] = useState(null); const [selectedSession, setSelectedSession] = useState(null); const [activeTab, setActiveTab] = useState('chat'); // 'chat' or 'files' - const [isMobile, setIsMobile] = useState(false); + const { isMobile } = useDeviceSettings({ trackPWA: false }); const [sidebarOpen, setSidebarOpen] = useState(false); const [isLoadingProjects, setIsLoadingProjects] = useState(true); const [loadingProgress, setLoadingProgress] = useState(null); // { phase, current, total, currentProject } @@ -77,49 +78,6 @@ function AppContent() { // Ref to track loading progress timeout for cleanup const loadingProgressTimeoutRef = useRef(null); - // Detect if running as PWA - const [isPWA, setIsPWA] = useState(false); - - useEffect(() => { - // Check if running in standalone mode (PWA) - const checkPWA = () => { - const isStandalone = window.matchMedia('(display-mode: standalone)').matches || - window.navigator.standalone || - document.referrer.includes('android-app://'); - setIsPWA(isStandalone); - document.addEventListener('touchstart', {}); - - // Add class to html and body for CSS targeting - if (isStandalone) { - document.documentElement.classList.add('pwa-mode'); - document.body.classList.add('pwa-mode'); - } else { - document.documentElement.classList.remove('pwa-mode'); - document.body.classList.remove('pwa-mode'); - } - }; - - checkPWA(); - - // Listen for changes - window.matchMedia('(display-mode: standalone)').addEventListener('change', checkPWA); - - return () => { - window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkPWA); - }; - }, []); - - useEffect(() => { - const checkMobile = () => { - setIsMobile(window.innerWidth < 768); - }; - - checkMobile(); - window.addEventListener('resize', checkMobile); - - return () => window.removeEventListener('resize', checkMobile); - }, []); - useEffect(() => { // Fetch projects on component mount fetchProjects(); @@ -584,7 +542,6 @@ function AppContent() { loadingProgress, onRefresh: handleSidebarRefresh, onShowSettings: handleShowSettings, - isPWA, isMobile }), [ projects, @@ -599,7 +556,6 @@ function AppContent() { loadingProgress, handleSidebarRefresh, handleShowSettings, - isPWA, isMobile ]); diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 9b05452..62e915a 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -14,6 +14,7 @@ import CodexLogo from './CodexLogo.jsx'; import TaskIndicator from './TaskIndicator'; import ProjectCreationWizard from './ProjectCreationWizard'; import VersionUpgradeModal from './modals/VersionUpgradeModal'; +import { useDeviceSettings } from '../hooks/useDeviceSettings'; import { useVersionCheck } from '../hooks/useVersionCheck'; import { useUiPreferences } from '../hooks/useUiPreferences'; import { api } from '../utils/api'; @@ -36,10 +37,10 @@ function Sidebar({ loadingProgress, onRefresh, onShowSettings, - isPWA, isMobile }) { const { t } = useTranslation(['sidebar', 'common']); + const { isPWA } = useDeviceSettings({ trackMobile: false }); const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui'); const { preferences, setPreference } = useUiPreferences(); const { sidebarVisible } = preferences; @@ -92,6 +93,15 @@ function Sidebar({ }; }; + useEffect(() => { + if (typeof document === 'undefined') { + return; + } + + document.documentElement.classList.toggle('pwa-mode', isPWA); + document.body.classList.toggle('pwa-mode', isPWA); + }, [isPWA]); + // Auto-update timestamps every minute useEffect(() => { const timer = setInterval(() => { diff --git a/src/hooks/useDeviceSettings.ts b/src/hooks/useDeviceSettings.ts new file mode 100644 index 0000000..ff7bbbf --- /dev/null +++ b/src/hooks/useDeviceSettings.ts @@ -0,0 +1,88 @@ +import { useEffect, useState } from 'react'; + +type UseDeviceSettingsOptions = { + mobileBreakpoint?: number; + trackMobile?: boolean; + trackPWA?: boolean; +}; + +const getIsMobile = (mobileBreakpoint: number): boolean => { + if (typeof window === 'undefined') { + return false; + } + + return window.innerWidth < mobileBreakpoint; +}; + +const getIsPWA = (): boolean => { + if (typeof window === 'undefined') { + return false; + } + + const navigatorWithStandalone = window.navigator as Navigator & { standalone?: boolean }; + + return ( + window.matchMedia('(display-mode: standalone)').matches || + Boolean(navigatorWithStandalone.standalone) || + document.referrer.includes('android-app://') + ); +}; + +export function useDeviceSettings(options: UseDeviceSettingsOptions = {}) { + const { + mobileBreakpoint = 768, + trackMobile = true, + trackPWA = true + } = options; + + const [isMobile, setIsMobile] = useState(() => ( + trackMobile ? getIsMobile(mobileBreakpoint) : false + )); + const [isPWA, setIsPWA] = useState(() => ( + trackPWA ? getIsPWA() : false + )); + + useEffect(() => { + if (!trackMobile || typeof window === 'undefined') { + return; + } + + const checkMobile = () => { + setIsMobile(getIsMobile(mobileBreakpoint)); + }; + + checkMobile(); + window.addEventListener('resize', checkMobile); + + return () => { + window.removeEventListener('resize', checkMobile); + }; + }, [mobileBreakpoint, trackMobile]); + + useEffect(() => { + if (!trackPWA || typeof window === 'undefined') { + return; + } + + const mediaQuery = window.matchMedia('(display-mode: standalone)'); + const checkPWA = () => { + setIsPWA(getIsPWA()); + }; + + checkPWA(); + + if (typeof mediaQuery.addEventListener === 'function') { + mediaQuery.addEventListener('change', checkPWA); + return () => { + mediaQuery.removeEventListener('change', checkPWA); + }; + } + + mediaQuery.addListener(checkPWA); + return () => { + mediaQuery.removeListener(checkPWA); + }; + }, [trackPWA]); + + return { isMobile, isPWA }; +}