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 }; +}