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`)
This commit is contained in:
Haileyesus
2026-02-06 12:45:05 +03:00
parent 527e8d040c
commit 856033a9f6
3 changed files with 101 additions and 47 deletions

View File

@@ -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
]);

View File

@@ -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(() => {

View File

@@ -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<boolean>(() => (
trackMobile ? getIsMobile(mobileBreakpoint) : false
));
const [isPWA, setIsPWA] = useState<boolean>(() => (
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 };
}