Skip to content
Source
Sign inSign up
Open History to see the last commit

App.tsx

309 lines11.9 KB

1import { installPermissionUpdateNotice } from "./features/connection/permissionUpdateNotice";
2import { installComposerAttachmentUploads } from "./state/composer-attachment-uploads";
3import { installThreadOutboxDelivery } from "./state/threadOutboxDelivery";
4import * as Linking from "expo-linking";
5import * as SplashScreen from "expo-splash-screen";
6import { requireNativeModule } from "expo";
7import { useEffect, useRef, useState } from "react";
8import { Platform, StatusBar } from "react-native";
9import { GestureHandlerRootView } from "react-native-gesture-handler";
10import { KeyboardProvider } from "react-native-keyboard-controller";
11import { SafeAreaProvider } from "react-native-safe-area-context";
12import {
13 CommonActions,
14 createPathConfigForStaticNavigation,
15 createStaticNavigation,
16 getActionFromState,
17 getStateFromPath,
18 type NavigationAction,
19 type NavigationState,
20} from "@react-navigation/native";
21import { pipe } from "effect/Function";
22import { DEFAULT_HOSTED_APP_URL } from "@t3tools/shared/connectAuth";
23​
24import { RegistryContext, useAtomValue } from "@effect/atom-react";
25import { ProjectChipIconRasterizer } from "./native/ProjectChipIconImages";
26import { ThreadArrangementHost } from "./features/threads/ThreadArrangementSheet";
27import * as Dialog from "./components/ui/Dialog";
28import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider";
29import { prepareNativeShowcaseCapture } from "./features/showcase/nativeShowcaseScene";
30import { installShowcaseCapture } from "./features/showcase/ShowcaseCaptureCoordinator";
31import {
32 presentPendingIncomingShare,
33 pruneIncomingShareImports,
34 startIncomingShareInbox,
35} from "./features/sharing/IncomingShareProvider";
36import {
37 installAgentNotificationNavigation,
38 setAgentNotificationPathname,
39} from "./features/agent-awareness/notificationNavigation";
40import {
41 installAppShortcuts,
42 recordAppShortcutNavigationState,
43} from "./features/shortcuts/appShortcutsRuntime";
44import {
45 AppearancePreferencesProvider,
46 installAppearanceRuntime,
47 useAppearancePreferences,
48} from "./features/settings/appearance/AppearancePreferencesProvider";
49import { RootStack, rootPathnameFromState, workspaceLocationFromState } from "./Stack";
50import { appAtomRegistry } from "./state/atom-registry";
51import { subscribeComposerDraftModelReconciliation } from "./state/use-composer-drafts";
52import { environmentThreadShells, threadEnvironment } from "./state/threads";
53import { environmentShell, shellCachesReadAtom } from "./state/shell";
54import { OverlayPortalHost } from "./components/ui/OverlayPortal";
55import { shouldHandleAppLink, restoreDraftLinkState } from "./lib/appLinking";
56import { hasCloudPublicConfig } from "./features/cloud/publicConfig";
57import { takeScheduledTaskNewLink } from "./features/settings/scheduled-task-editor-state";
58import {
59 pairingFormLinkAction,
60 startDevAutoConnect,
61 withoutPairingTarget,
62} from "./features/connection/devAutoConnect";
63import {
64 resolvePendingNewTaskPromptLink,
65 takeNewTaskPromptLink,
66} from "./features/prompt-links/newTaskPromptLink";
67import {
68 resolvePendingThreadPromptLink,
69 takeThreadPromptLink,
70} from "./features/prompt-links/threadPromptLink";
71import {
72 disposeOnFoundationReplace,
73 type FoundationHotModule,
74} from "./lib/foundation-fast-refresh";
75import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme";
76import { installUiRuntimeMemoryWarningGc } from "./lib/useUiRuntimeMemoryWarningGc";
77import { SubscriptionUsageCoordinator } from "./widgets/SubscriptionUsageCoordinator";
78import { installSubscriptionUsageRefresh } from "./widgets/useSubscriptionUsage";
79import { GlobalVoiceInputControl } from "./features/voice-input/GlobalVoiceInputControl";
80import {
81 launchSplashHomeRowsPending,
82 settleLaunchSplashHomeRows,
83 splashCanHide,
84 useLaunchSplashHomeRowsPending,
85} from "./features/home/home-launch-splash";
86import {
87 restoreSettingsLinkState,
88 settingsLinkAction,
89} from "./features/settings/settings-navigation";
90import {
91 hydrateWorkspaceSelections,
92 launchWorkspaceURL,
93 recordWorkspaceSelections,
94 settingsLinkPage,
95} from "./features/layout/workspace/workspace-selection-store";
96import { workspaceDetailLinkAction } from "./features/layout/workspace/workspace-detail-selection";
97import { NativeLayoutMetricsProvider } from "./native/native-layout-metrics";
98import {
99 attachRootNavigation,
100 flushRootNavigationQueue,
101 linkToRootPath,
102 rootNavigationRef,
103} from "./lib/rootNavigation";
104import { pruneNewTaskFlowStates } from "./features/threads/new-task-flow-state";
105import { pruneNewTaskBranchCheckouts } from "./features/threads/NewTaskDraftRouteScreen";
106import { installAppUpdateChecks } from "./features/updates/app-updates";
107​
108import "../global.css";
109​
110if (process.env.EXPO_PUBLIC_SHOWCASE === "1") {
111 prepareNativeShowcaseCapture();
113​
114void SplashScreen.preventAutoHideAsync().catch(() => {});
115void hydrateWorkspaceSelections();
116​
117const linkingConfig = {
118 initialRouteName: "Home",
119 screens: createPathConfigForStaticNavigation(RootStack, { initialRouteName: "Home" }) ?? {},
120} as const;
121​
122function getAppStateFromPath(path: string, options: Parameters<typeof getStateFromPath>[1]) {
123 return pipe(
124 getStateFromPath(path, options),
125 (state) => restoreDraftLinkState(state, path),
126 (state) => restoreSettingsLinkState(state, settingsLinkPage()),
127 takeThreadPromptLink,
128 takeNewTaskPromptLink,
129 withoutPairingTarget,
130 startDevAutoConnect,
131 takeScheduledTaskNewLink,
132 );
134​
135const appLinking = {
136 prefixes: [
137 Linking.createURL("/"),
138 "t3code://",
139 "t3code-dev://",
140 "t3code-preview://",
141 DEFAULT_HOSTED_APP_URL,
142 ],
143 config: { initialRouteName: "Home" },
144 filter: (url: string) => shouldHandleAppLink(url, hasCloudPublicConfig()),
145 getInitialURL: async () => (await Linking.getInitialURL()) ?? (await launchWorkspaceURL()),
146 getActionFromState: (
147 state: Parameters<typeof getActionFromState>[0],
148 options: Parameters<typeof getActionFromState>[1],
149 ): ReturnType<typeof getActionFromState> => {
150 const current = rootNavigationRef.isReady() ? rootNavigationRef.getRootState() : undefined;
151 return (
152 // Our router handles the selection; React Navigation's type only lists its own actions.
153 (workspaceDetailLinkAction(state) as ReturnType<typeof getActionFromState>) ??
154 pairingFormLinkAction(state, current) ??
155 settingsLinkAction(state, current) ??
156 getActionFromState(state, options)
157 );
158 },
159 getStateFromPath: getAppStateFromPath,
160};
161​
162function appLinkAction(href: string): NavigationAction {
163 const state = getAppStateFromPath(href, linkingConfig);
164 if (!state) throw new Error(`Failed to parse the href to a navigation state (${href}).`);
165 return appLinking.getActionFromState(state, linkingConfig) ?? CommonActions.reset(state);
167​
168function linkToAppPath(href: string): void {
169 linkToRootPath(href, appLinkAction);
171​
172const Navigation = createStaticNavigation(RootStack);
173​
174function handleNavigationStateChange(state: NavigationState | undefined): void {
175 recordWorkspaceSelections(state);
176 presentPendingIncomingShare();
177 resolvePendingNewTaskPromptLink(state);
178 resolvePendingThreadPromptLink(state);
179 if (!state) return;
180 // A launch that opens another route has no Home rows for the splash to wait for.
181 if (state.routes[state.index]?.name !== "Home") settleLaunchSplashHomeRows();
182 setAgentNotificationPathname(workspaceLocationFromState(state).pathname);
183 recordAppShortcutNavigationState(state);
184 pruneNewTaskFlowStates(state);
185 pruneNewTaskBranchCheckouts(state);
186 pruneIncomingShareImports(state);
188​
189function handleNavigationReady(): void {
190 handleNavigationStateChange(rootNavigationRef.getRootState());
191 flushRootNavigationQueue();
193​
194declare const module: { readonly hot?: FoundationHotModule } | undefined;
195​
196const appServiceDisposers = [
197 installUiRuntimeMemoryWarningGc(),
198 subscribeComposerDraftModelReconciliation({
199 registry: appAtomRegistry,
200 threadShellAtom: environmentThreadShells.threadShellAtom,
201 threadRefsAtom: environmentThreadShells.threadRefsAtom,
202 shellStateAtom: environmentShell.stateAtom,
203 pendingThreadIdsAtom: threadEnvironment.pendingThreadIdsAtom,
204 }),
205 installAppearanceRuntime(),
206 installSubscriptionUsageRefresh(),
207 installPermissionUpdateNotice(),
208 installThreadOutboxDelivery(),
209 installComposerAttachmentUploads(),
210 startIncomingShareInbox(),
211 installAgentNotificationNavigation(linkToAppPath),
212 installAppShortcuts(linkToAppPath),
213 installAppUpdateChecks(),
214 installShowcaseCapture(rootPathnameFromState),
215];
216disposeOnFoundationReplace(typeof module === "undefined" ? undefined : module.hot, () => {
217 for (const dispose of appServiceDisposers) dispose();
218});
219​
220// Cached shells are local reads that end in a fraction of this time; the limit keeps a failed
221// read, or a list that does not lay out, from holding the splash screen.
222const SPLASH_WAIT_MS = 1_500;
223​
224function SplashScreenCoordinator() {
225 const { isReady } = useAppearancePreferences();
226 const shellCachesRead = useAtomValue(shellCachesReadAtom);
227 const homeRowsPending = useLaunchSplashHomeRowsPending();
228 const [waitEnded, setWaitEnded] = useState(false);
229 const hidden = useRef(false);
230​
231 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- starts the limit on how long the native splash screen waits for cached shells and their rows
232 useEffect(() => {
233 const timer = setTimeout(() => setWaitEnded(true), SPLASH_WAIT_MS);
234 return () => clearTimeout(timer);
235 }, []);
236​
237 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- hides the native splash screen once preferences have loaded and the cached rows are laid out
238 useEffect(() => {
239 const canHide = splashCanHide({
240 preferencesReady: isReady,
241 shellCachesRead,
242 // Home holds the splash in a layout effect of the commit that gives it rows. That can be
243 // this commit, after this render read the store.
244 homeRowsPending: homeRowsPending || launchSplashHomeRowsPending(),
245 waitEnded,
246 });
247 if (!canHide || hidden.current) return;
248 hidden.current = true;
249 settleLaunchSplashHomeRows();
250 void SplashScreen.hide();
251 }, [isReady, shellCachesRead, homeRowsPending, waitEnded]);
252​
253 return null;
255​
256export default function App() {
257 return (
258 <RegistryContext.Provider value={appAtomRegistry}>
259 <CloudAuthProvider>
260 <AppearancePreferencesProvider>
261 <AppContent />
262 </AppearancePreferencesProvider>
263 </CloudAuthProvider>
264 </RegistryContext.Provider>
265 );
267​
268function AppContent() {
269 const { themeAppearance } = useAppearancePreferences();
270 const navigationTheme = useMobileNavigationTheme();
271​
272 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- matches the Android navigation bar to the app theme, as StatusBar does for the status bar
273 useEffect(() => {
274 if (Platform.OS !== "android") return;
275 void requireNativeModule<{ setNavigationBarStyle(light: boolean): Promise<void> }>(
276 "T3NativeControls",
277 ).setNavigationBarStyle(themeAppearance === "light");
278 }, [themeAppearance]);
279​
280 return (
281 <>
282 <SplashScreenCoordinator />
283 <SubscriptionUsageCoordinator />
284 <GestureHandlerRootView className="flex-1">
285 <KeyboardProvider statusBarTranslucent>
286 <SafeAreaProvider>
287 <StatusBar barStyle={themeAppearance === "dark" ? "light-content" : "dark-content"} />
288 {/* native-stack needs the theme's dark flag to select dark native headers. */}
289 <GlobalVoiceInputControl>
290 <NativeLayoutMetricsProvider>
291 <Navigation
292 ref={attachRootNavigation}
293 linking={appLinking}
294 onReady={handleNavigationReady}
295 theme={navigationTheme}
296 onStateChange={handleNavigationStateChange}
297 />
298 <Dialog.Host />
299 <ThreadArrangementHost />
300 <ProjectChipIconRasterizer />
301 </NativeLayoutMetricsProvider>
302 </GlobalVoiceInputControl>
303 <OverlayPortalHost />
304 </SafeAreaProvider>
305 </KeyboardProvider>
306 </GestureHandlerRootView>
307 </>
308 );