// SPDX-License-Identifier: AGPL-3.0-or-later import styles from '@app/app/App.module.css'; import {router} from '@app/app/Router'; import * as AccessibilityCommands from '@app/features/accessibility/commands/AccessibilityCommands'; import {NekoSprite} from '@app/features/accessibility/components/NekoSprite'; import Accessibility from '@app/features/accessibility/state/Accessibility'; import ScreenReader from '@app/features/accessibility/state/ScreenReader'; import {DndContext} from '@app/features/app/components/layout/DndContext'; import GlobalOverlays from '@app/features/app/components/layout/GlobalOverlays'; import {NativeTitlebar} from '@app/features/app/components/layout/NativeTitlebar'; import {useDesktopAllowTransparency} from '@app/features/app/hooks/useDesktopAllowTransparency'; import {useDesktopElectronBridges} from '@app/features/app/hooks/useDesktopElectronBridges'; import {useDocumentClassToggle} from '@app/features/app/hooks/useDocumentClassToggle'; import {useInertBackground} from '@app/features/app/hooks/useInertBackground'; import {useIsRootDocumentFullscreen} from '@app/features/app/hooks/useIsRootDocumentFullscreen'; import {useKeybindManager} from '@app/features/app/hooks/useKeybindManager'; import {useNativePlatform} from '@app/features/app/hooks/useNativePlatform'; import {usePlatformClasses} from '@app/features/app/hooks/usePlatformClasses'; import {useServiceWorkerBadge} from '@app/features/app/hooks/useServiceWorkerBadge'; import {useTabKeyFocusGuard} from '@app/features/app/hooks/useTabKeyFocusGuard'; import {type LayoutVariant, LayoutVariantProvider} from '@app/features/app/state/LayoutVariantContext'; import RuntimeConfig from '@app/features/app/state/RuntimeConfig'; import RuntimeCrash from '@app/features/app/state/RuntimeCrash'; import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions'; import {showMyselfTypingHelper} from '@app/features/devtools/utils/ShowMyselfTypingHelper'; import GatewayConnection from '@app/features/gateway/transport/GatewayConnection'; import GatewaySessions from '@app/features/gateway/transport/GatewaySessionPool'; import {AppI18nProvider} from '@app/features/i18n/components/AppI18nProvider'; import MemberSidebar from '@app/features/member/state/MemberSidebar'; import {attachExternalLinkInterceptor} from '@app/features/messaging/utils/ExternalLinkUtils'; import {startDeepLinkHandling} from '@app/features/navigation/utils/DeepLinkUtils'; import {Outlet, RouterProvider} from '@app/features/platform/components/router/RouterReact'; import SessionManager from '@app/features/platform/state/AuthSession'; import {Logger} from '@app/features/platform/utils/AppLogger'; import {ensureAutostartDefaultEnabled} from '@app/features/platform/utils/Autostart'; import {startDesktopJumpListBridge} from '@app/features/platform/utils/DesktopJumpListBridge'; import {startDesktopLocaleBridge} from '@app/features/platform/utils/DesktopLocaleBridge'; import {PremiumCheckoutReturnWatcher} from '@app/features/premium/components/PremiumCheckoutReturnWatcher'; import {QUICK_SWITCHER_PORTAL_ID} from '@app/features/search/components/quick_switcher/QuickSwitcherConstants'; import {useCustomThemeStyle} from '@app/features/theme/hooks/useCustomThemeStyle'; import {useRemScaleTracking} from '@app/features/theme/hooks/useRemScaleTracking'; import {useThemeCssVariables} from '@app/features/theme/hooks/useThemeCssVariables'; import Theme from '@app/features/theme/state/Theme'; import ThemeLibrary from '@app/features/theme/state/ThemeLibrary'; import WindowsFont from '@app/features/theme/state/WindowsFont'; import {useThemeStudioBroadcast} from '@app/features/theme_studio/state/ThemeStudioBroadcast'; import ThemeStudioState from '@app/features/theme_studio/state/ThemeStudioState'; import {SVGMasks} from '@app/features/ui/components/SVGMasks'; import FocusRingScope from '@app/features/ui/focus_ring/FocusRingScope'; import {useTextInputContextMenu} from '@app/features/ui/hooks/useTextInputContextMenu'; import {getActivePortalHost, setActivePortalHost} from '@app/features/ui/overlay/PortalHostContext'; import MobileLayout from '@app/features/ui/state/MobileLayout'; import Modal from '@app/features/ui/state/Modal'; import Popout from '@app/features/ui/state/Popout'; import {getDesktopWindowBehaviorSettings} from '@app/features/ui/utils/DesktopWindowBehaviorUtils'; import {isDesktop} from '@app/features/ui/utils/NativeUtils'; import {UNFOCUSED_FULLY_INTERACTIVE_CLASS} from '@app/features/ui/utils/WindowFocusInteractionGuard'; import UserSettings from '@app/features/user/state/UserSettings'; import {IncomingCallManager} from '@app/features/voice/components/IncomingCallManager'; import {VoiceLiveKitRoot} from '@app/features/voice/components/VoiceLiveKitRoot'; import MediaEngine from '@app/features/voice/engine/MediaEngineFacade'; import {useElectronScreenSharePicker} from '@app/features/voice/hooks/useElectronScreenSharePicker'; import {startScreenSharePiPController} from '@app/features/voice/state/ScreenSharePiPController'; import {useNativeTitleBar} from '@app/features/window/hooks/useNativeTitleBar'; import {useStopFlashFrameOnFocus} from '@app/features/window/hooks/useStopFlashFrameOnFocus'; import {useWindowEventListeners} from '@app/features/window/hooks/useWindowEventListeners'; import {i18n} from '@lingui/core'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; import {IconContext} from '@phosphor-icons/react'; import {reaction} from 'mobx'; import {observer} from 'mobx-react-lite'; import React, {type ReactNode, useCallback, useEffect, useMemo, useRef, useState} from 'react'; const SKIP_TO_CONTENT_DESCRIPTOR = msg({ message: 'Skip to content', comment: 'Accessible skip-link label for keyboard users.', }); const logger = new Logger('App'); interface AppWrapperProps { children: ReactNode; } function getInstanceThemeColor(): string | null { const snapshot = RuntimeConfig.getSnapshotOrNull(); if (snapshot === null || !(DeveloperOptions.selfHostedModeOverride || snapshot.features.self_hosted)) { return null; } return snapshot.appPublic.branding.theme_color ?? null; } export const AppWrapper = observer(({children}: AppWrapperProps) => { const {i18n} = useLingui(); const reducedMotion = Accessibility.useReducedMotion; const stayInteractiveWhenUnfocused = Accessibility.stayInteractiveWhenUnfocused; const {platform, isNative} = useNativePlatform(); const useSystemTitleBar = useNativeTitleBar(); const messageDisplayCompact = UserSettings.getMessageDisplayCompact(); const isRootDocumentFullscreen = useIsRootDocumentFullscreen(); const [layoutVariant, setLayoutVariant] = useState('app'); const layoutVariantContextValue = useMemo( () => ({variant: layoutVariant, setVariant: setLayoutVariant}), [layoutVariant], ); const topPopoutRequiresBackdrop = Popout.requiresBackdrop(); const hasBlockingModal = Modal.hasModalOpen(); const room = MediaEngine.room; const ringsContainerRef = useRef(null); const overlayScopeRef = useRef(null); useElectronScreenSharePicker(); useTextInputContextMenu(); useThemeStudioBroadcast( useCallback((message) => { switch (message.type) { case 'customThemeCss': if ((Accessibility.customThemeCss ?? null) !== message.value) { AccessibilityCommands.update({customThemeCss: message.value}); } return; case 'themeLibrary': void ThemeLibrary.reload(); return; case 'studio:closed-popout': ThemeStudioState.clearPoppedOut(); return; default: return; } }, []), ); const handleSkipLinkFocus = useTabKeyFocusGuard(); useInertBackground(ringsContainerRef, hasBlockingModal || topPopoutRequiresBackdrop); useEffect(() => { showMyselfTypingHelper.start(); return () => showMyselfTypingHelper.stop(); }, []); useEffect(() => { let active = true; void SessionManager.initialize() .then(() => { if (active) { GatewaySessions.startRestoredSession(); } }) .catch((error: unknown) => { logger.error('Failed to initialize the restored gateway session', error); }); return () => { active = false; GatewaySessions.dispose(); }; }, []); useEffect( () => reaction( () => GatewayConnection.sessionId, (sessionId) => MemberSidebar.synchronizeGatewaySession(sessionId), {fireImmediately: true}, ), [], ); useEffect(() => { const clearForeignPortalHost = (): void => { const activePortalHost = getActivePortalHost(); if (!activePortalHost) return; if (activePortalHost.ownerDocument === document) return; setActivePortalHost(null); }; document.addEventListener('pointerdown', clearForeignPortalHost, true); document.addEventListener('focusin', clearForeignPortalHost, true); document.addEventListener('keydown', clearForeignPortalHost, true); return () => { document.removeEventListener('pointerdown', clearForeignPortalHost, true); document.removeEventListener('focusin', clearForeignPortalHost, true); document.removeEventListener('keydown', clearForeignPortalHost, true); }; }, []); useEffect(() => startScreenSharePiPController(), []); useServiceWorkerBadge(); useKeybindManager(i18n); useDesktopElectronBridges(); useDocumentClassToggle('reduced-motion', reducedMotion); useDocumentClassToggle('mobile-layout', MobileLayout.platformMobileDetected || MobileLayout.enabled); useDocumentClassToggle(UNFOCUSED_FULLY_INTERACTIVE_CLASS, stayInteractiveWhenUnfocused); useDocumentClassToggle('windows-fluxer-sans', WindowsFont.useFluxerSans); useDesktopAllowTransparency(isNative); useWindowEventListeners(); useRemScaleTracking(); usePlatformClasses(platform, isNative); useThemeCssVariables({ effectiveTheme: Theme.effectiveTheme, saturationFactor: Accessibility.saturationFactor, alwaysUnderlineLinks: Accessibility.alwaysUnderlineLinks, dimStrikethroughText: Accessibility.dimStrikethroughText, enableTextSelection: Accessibility.textSelectionEnabled, fontSize: Accessibility.fontSize, messageGutter: Accessibility.messageGutter, messageGroupSpacing: Accessibility.getMessageGroupSpacingValue(messageDisplayCompact), hdrDisplayMode: Accessibility.hdrDisplayMode, instanceThemeColor: getInstanceThemeColor(), }); useCustomThemeStyle({ enabledThemeCss: ThemeLibrary.activeThemeCss, customThemeCss: Accessibility.customThemeCss, themeLibraryAssets: ThemeLibrary.assets, themeLibraryLocalFiles: ThemeLibrary.localFiles, themeLibraryRevision: ThemeLibrary.revision, }); return (
{i18n._(SKIP_TO_CONTENT_DESCRIPTOR)} {isNative && !useSystemTitleBar && !isRootDocumentFullscreen && ( )} {children}
); }); export const App = observer((): React.ReactElement => { const fatalError = RuntimeCrash.fatalError; if (fatalError) { throw fatalError; } useEffect(() => { if (!isDesktop()) return; void ensureAutostartDefaultEnabled(); void getDesktopWindowBehaviorSettings(); }, []); useEffect(() => { void startDeepLinkHandling(); }, []); useEffect(() => { if (!isDesktop()) return; startDesktopLocaleBridge(); startDesktopJumpListBridge(); ScreenReader.startDesktopBridge(); }, []); useStopFlashFrameOnFocus(); useEffect(() => { const detach = attachExternalLinkInterceptor(); return () => detach?.(); }, []); return ( ); });