| 1 | import { UiSettingsSyncProvider } from "./state/uiSettingsSync"; |
| 2 | import { PermissionUpdateNotice } from "./components/PermissionUpdateNotice"; |
| 3 | import * as Linking from "expo-linking"; |
| 4 | import * as SplashScreen from "expo-splash-screen"; |
| 5 | import { useCallback, useEffect, useRef, type RefObject } from "react"; |
| 6 | import { StatusBar } from "react-native"; |
| 7 | import { GestureHandlerRootView } from "react-native-gesture-handler"; |
| 8 | import { KeyboardProvider } from "react-native-keyboard-controller"; |
| 9 | import { SafeAreaProvider } from "react-native-safe-area-context"; |
| 10 | import { createStaticNavigation, type NavigationState } from "@react-navigation/native"; |
| 11 | |
| 12 | import { RegistryContext } from "@effect/atom-react"; |
| 13 | import { ThreadArrangementHost } from "./features/threads/ThreadArrangementSheet"; |
| 14 | import { ConfirmDialogHost } from "./components/ConfirmDialogHost"; |
| 15 | import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider"; |
| 16 | import { prepareNativeShowcaseCapture } from "./features/showcase/nativeShowcaseScene"; |
| 17 | import { IncomingShareProvider } from "./features/sharing/IncomingShareProvider"; |
| 18 | import { |
| 19 | AppearancePreferencesProvider, |
| 20 | useAppearancePreferences, |
| 21 | } from "./features/settings/appearance/AppearancePreferencesProvider"; |
| 22 | import { RootStack } from "./Stack"; |
| 23 | import { appAtomRegistry } from "./state/atom-registry"; |
| 24 | import { OverlayPortalHost } from "./components/OverlayPortal"; |
| 25 | import { shouldHandleAppLink } from "./lib/appLinking"; |
| 26 | import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme"; |
| 27 | import { useUiRuntimeMemoryWarningGc } from "./lib/useUiRuntimeMemoryWarningGc"; |
| 28 | import { SubscriptionUsageCoordinator } from "./widgets/SubscriptionUsageCoordinator"; |
| 29 | import { VoiceInputProvider } from "./features/voice-input/VoiceInputProvider"; |
| 30 | import { GlobalVoiceInputControl } from "./features/voice-input/GlobalVoiceInputControl"; |
| 31 | import { NativeLayoutMetricsProvider } from "./native/native-layout-metrics"; |
| 32 | |
| 33 | import "../global.css"; |
| 34 | |
| 35 | if (process.env.EXPO_PUBLIC_SHOWCASE === "1") { |
| 36 | prepareNativeShowcaseCapture(); |
| 37 | } |
| 38 | |
| 39 | void SplashScreen.preventAutoHideAsync().catch(() => { |
| 40 | // The native module can be unavailable in non-native test environments. |
| 41 | }); |
| 42 | |
| 43 | const appLinking = { |
| 44 | prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"], |
| 45 | // Keep the compact thread list available beneath a directly opened thread. |
| 46 | config: { initialRouteName: "Home" }, |
| 47 | filter: shouldHandleAppLink, |
| 48 | }; |
| 49 | |
| 50 | const Navigation = createStaticNavigation(RootStack); |
| 51 | |
| 52 | function SplashScreenCoordinator() { |
| 53 | const { isReady } = useAppearancePreferences(); |
| 54 | |
| 55 | useEffect(() => { |
| 56 | if (isReady) void SplashScreen.hide(); |
| 57 | }, [isReady]); |
| 58 | |
| 59 | return null; |
| 60 | } |
| 61 | |
| 62 | export default function App() { |
| 63 | useUiRuntimeMemoryWarningGc(); |
| 64 | const navigationState = useRef<NavigationState | undefined>(undefined); |
| 65 | const saveNavigationState = useCallback((state: NavigationState | undefined) => { |
| 66 | navigationState.current = state; |
| 67 | }, []); |
| 68 | |
| 69 | return ( |
| 70 | <RegistryContext.Provider value={appAtomRegistry}> |
| 71 | <CloudAuthProvider> |
| 72 | <AppearancePreferencesProvider> |
| 73 | <UiSettingsSyncProvider> |
| 74 | <AppContent |
| 75 | navigationState={navigationState} |
| 76 | saveNavigationState={saveNavigationState} |
| 77 | /> |
| 78 | </UiSettingsSyncProvider> |
| 79 | </AppearancePreferencesProvider> |
| 80 | </CloudAuthProvider> |
| 81 | </RegistryContext.Provider> |
| 82 | ); |
| 83 | } |
| 84 | |
| 85 | function AppContent(props: { |
| 86 | readonly navigationState: RefObject<NavigationState | undefined>; |
| 87 | readonly saveNavigationState: (state: NavigationState | undefined) => void; |
| 88 | }) { |
| 89 | const { themeAppearance } = useAppearancePreferences(); |
| 90 | const navigationTheme = useMobileNavigationTheme(); |
| 91 | |
| 92 | return ( |
| 93 | <> |
| 94 | <SplashScreenCoordinator /> |
| 95 | <SubscriptionUsageCoordinator /> |
| 96 | <PermissionUpdateNotice /> |
| 97 | <GestureHandlerRootView className="flex-1"> |
| 98 | <KeyboardProvider statusBarTranslucent> |
| 99 | <SafeAreaProvider> |
| 100 | <VoiceInputProvider> |
| 101 | <StatusBar barStyle={themeAppearance === "dark" ? "light-content" : "dark-content"} /> |
| 102 | {/* The navigation theme drives the NATIVE header appearance: native-stack |
| 103 | forwards `dark` as the nav bar's overrideUserInterfaceStyle. Without |
| 104 | this, React Navigation defaults to its light theme and every native |
| 105 | header (glass buttons, title, materials) is forced light even when |
| 106 | the system is in dark mode. */} |
| 107 | <GlobalVoiceInputControl> |
| 108 | <NativeLayoutMetricsProvider> |
| 109 | <IncomingShareProvider> |
| 110 | <Navigation |
| 111 | linking={appLinking} |
| 112 | theme={navigationTheme} |
| 113 | initialState={props.navigationState.current} |
| 114 | onStateChange={props.saveNavigationState} |
| 115 | /> |
| 116 | </IncomingShareProvider> |
| 117 | <ConfirmDialogHost /> |
| 118 | <ThreadArrangementHost /> |
| 119 | </NativeLayoutMetricsProvider> |
| 120 | </GlobalVoiceInputControl> |
| 121 | {/* Anchored-menu overlays render here — in-window, so the |
| 122 | keyboard stays up while a dropdown is open. */} |
| 123 | <OverlayPortalHost /> |
| 124 | </VoiceInputProvider> |
| 125 | </SafeAreaProvider> |
| 126 | </KeyboardProvider> |
| 127 | </GestureHandlerRootView> |
| 128 | </> |
| 129 | ); |
| 130 | } |