mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
287 lines
13 KiB
TypeScript
287 lines
13 KiB
TypeScript
// 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<LayoutVariant>('app');
|
|
const layoutVariantContextValue = useMemo(
|
|
() => ({variant: layoutVariant, setVariant: setLayoutVariant}),
|
|
[layoutVariant],
|
|
);
|
|
const topPopoutRequiresBackdrop = Popout.requiresBackdrop();
|
|
const hasBlockingModal = Modal.hasModalOpen();
|
|
const room = MediaEngine.room;
|
|
const ringsContainerRef = useRef<HTMLDivElement>(null);
|
|
const overlayScopeRef = useRef<HTMLDivElement>(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 (
|
|
<LayoutVariantProvider value={layoutVariantContextValue} data-flx="app.app.app-wrapper.layout-variant-provider">
|
|
<SVGMasks data-flx="app.app.app-wrapper.svg-masks" />
|
|
<VoiceLiveKitRoot room={room} data-flx="app.app.app-wrapper.voice-live-kit-root">
|
|
<div ref={ringsContainerRef} className={styles.appContainer} data-flx="app.app.app-wrapper.app-container">
|
|
<FocusRingScope containerRef={ringsContainerRef} data-flx="app.app.app-wrapper.focus-ring-scope">
|
|
<a
|
|
href="#main-content"
|
|
className={styles.skipLink}
|
|
onFocus={handleSkipLinkFocus}
|
|
data-flx="app.app.app-wrapper.skip-link"
|
|
>
|
|
{i18n._(SKIP_TO_CONTENT_DESCRIPTOR)}
|
|
</a>
|
|
{isNative && !useSystemTitleBar && !isRootDocumentFullscreen && (
|
|
<NativeTitlebar platform={platform} data-flx="app.app.app-wrapper.native-titlebar" />
|
|
)}
|
|
{children}
|
|
</FocusRingScope>
|
|
</div>
|
|
<div ref={overlayScopeRef} className={styles.overlayScope} data-flx="app.app.app-wrapper.overlay-scope">
|
|
<NekoSprite data-flx="app.app.app-wrapper.neko-sprite" />
|
|
<div
|
|
id={QUICK_SWITCHER_PORTAL_ID}
|
|
className={styles.quickSwitcherPortal}
|
|
data-overlay-pass-through="true"
|
|
aria-hidden="true"
|
|
data-flx="app.app.app-wrapper.quick-switcher-portal"
|
|
/>
|
|
<GlobalOverlays data-flx="app.app.app-wrapper.global-overlays" />
|
|
<IncomingCallManager data-flx="app.app.app-wrapper.incoming-call-manager" />
|
|
</div>
|
|
</VoiceLiveKitRoot>
|
|
</LayoutVariantProvider>
|
|
);
|
|
});
|
|
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 (
|
|
<AppI18nProvider i18n={i18n}>
|
|
<IconContext.Provider value={{color: 'currentColor', weight: 'fill'}}>
|
|
<PremiumCheckoutReturnWatcher data-flx="app.app.premium-checkout-return-watcher" />
|
|
<DndContext data-flx="app.app.dnd-context">
|
|
<RouterProvider router={router}>
|
|
<AppWrapper data-flx="app.app.app-wrapper">
|
|
<Outlet />
|
|
</AppWrapper>
|
|
</RouterProvider>
|
|
</DndContext>
|
|
</IconContext.Provider>
|
|
</AppI18nProvider>
|
|
);
|
|
});
|