1import { UiSettingsSyncProvider } from "./state/uiSettingsSync";
2import { PermissionUpdateNotice } from "./components/PermissionUpdateNotice";
3import * as Linking from "expo-linking";
4import * as SplashScreen from "expo-splash-screen";
5import { useCallback, useEffect, useRef, type RefObject } from "react";
6import { StatusBar } from "react-native";
7import { GestureHandlerRootView } from "react-native-gesture-handler";
8import { KeyboardProvider } from "react-native-keyboard-controller";
9import { SafeAreaProvider } from "react-native-safe-area-context";
10import { createStaticNavigation, type NavigationState } from "@react-navigation/native";
11
12import { RegistryContext } from "@effect/atom-react";
13import { ThreadArrangementHost } from "./features/threads/ThreadArrangementSheet";
14import { ConfirmDialogHost } from "./components/ConfirmDialogHost";
15import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider";
16import { prepareNativeShowcaseCapture } from "./features/showcase/nativeShowcaseScene";
17import { IncomingShareProvider } from "./features/sharing/IncomingShareProvider";
18import {
19 AppearancePreferencesProvider,
20 useAppearancePreferences,
21} from "./features/settings/appearance/AppearancePreferencesProvider";
22import { RootStack } from "./Stack";
23import { appAtomRegistry } from "./state/atom-registry";
24import { OverlayPortalHost } from "./components/OverlayPortal";
25import { shouldHandleAppLink } from "./lib/appLinking";
26import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme";
27import { useUiRuntimeMemoryWarningGc } from "./lib/useUiRuntimeMemoryWarningGc";
28import { SubscriptionUsageCoordinator } from "./widgets/SubscriptionUsageCoordinator";
29import { VoiceInputProvider } from "./features/voice-input/VoiceInputProvider";
30import { GlobalVoiceInputControl } from "./features/voice-input/GlobalVoiceInputControl";
31import { NativeLayoutMetricsProvider } from "./native/native-layout-metrics";
32
33import "../global.css";
34
35if (process.env.EXPO_PUBLIC_SHOWCASE === "1") {
36 prepareNativeShowcaseCapture();
37}
38
39void SplashScreen.preventAutoHideAsync().catch(() => {
40 // The native module can be unavailable in non-native test environments.
41});
42
43const 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
50const Navigation = createStaticNavigation(RootStack);
51
52function SplashScreenCoordinator() {
53 const { isReady } = useAppearancePreferences();
54
55 useEffect(() => {
56 if (isReady) void SplashScreen.hide();
57 }, [isReady]);
58
59 return null;
60}
61
62export 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
85function 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}