From 12b0751c28d567228b11b9446d26ec6a06f2e963 Mon Sep 17 00:00:00 2001 From: aaditagrawal <103925638+aaditagrawal@users.noreply.github.com> Date: Mon, 20 Jul 2026 06:53:13 +0530 Subject: [PATCH] sync: port upstream mobile iOS branding and native stack fixes (#4025, #4037) (#176) * fix(mobile): restore iOS home screen branding (#4025) Co-authored-by: Julius Marminge (cherry picked from commit 03e20c875ccd39e4cedba8f6b713195290ec4652) * fix(mobile): Stabilize native stack option updates (#4037) Co-authored-by: codex (cherry picked from commit 271454cbd2f93f25c8c158c0bb247164a0041578) --------- Co-authored-by: Alex Co-authored-by: Julius Marminge Co-authored-by: codex --- apps/mobile/src/Stack.tsx | 4 +- .../src/components/CompactBrandTitle.tsx | 60 ++++++++++ .../src/features/home/HomeRouteScreen.tsx | 5 +- .../threads/sidebar-navigation-shell.tsx | 4 +- apps/mobile/src/native/StackHeader.tsx | 103 +++++++++++++++++- ...act-navigation%2Fnative-stack@7.17.6.patch | 2 + pnpm-lock.yaml | 6 +- 7 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 apps/mobile/src/components/CompactBrandTitle.tsx diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index 4c865a9ba87e..0023340ce4b9 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -15,6 +15,7 @@ import { DynamicColorIOS, Platform, Pressable, ScrollView, StyleSheet } from "re import { useResolveClassNames } from "uniwind"; import { AppText as Text } from "./components/AppText"; +import { renderCompactBrandTitle } from "./components/CompactBrandTitle"; import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen"; import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation"; import { ClerkSettingsSheetDetentProvider } from "./features/cloud/ClerkSettingsSheetDetent"; @@ -383,7 +384,8 @@ export const RootStack = createNativeStackNavigator({ ...GLASS_HEADER_OPTIONS, contentStyle: { backgroundColor: "transparent" }, headerBackVisible: false, - title: "Threads", + headerTitle: renderCompactBrandTitle, + title: "T3 Code", }, }), Thread: createNativeStackScreen({ diff --git a/apps/mobile/src/components/CompactBrandTitle.tsx b/apps/mobile/src/components/CompactBrandTitle.tsx new file mode 100644 index 000000000000..2ecf34aa40cd --- /dev/null +++ b/apps/mobile/src/components/CompactBrandTitle.tsx @@ -0,0 +1,60 @@ +import { View } from "react-native"; + +import { AppText as Text } from "./AppText"; +import { T3Wordmark } from "./T3Wordmark"; +import { useThemeColor } from "../lib/useThemeColor"; + +/** + * Compact brand lockup sized for native navigation bars. + */ +export function CompactBrandTitle() { + const iconColor = useThemeColor("--color-icon"); + const mutedColor = useThemeColor("--color-foreground-muted"); + const subtleColor = useThemeColor("--color-subtle"); + + return ( + + + + Code + + + + Alpha + + + + ); +} + +export function renderCompactBrandTitle() { + return ; +} diff --git a/apps/mobile/src/features/home/HomeRouteScreen.tsx b/apps/mobile/src/features/home/HomeRouteScreen.tsx index 49cf06d85ec7..cd9467c774e8 100644 --- a/apps/mobile/src/features/home/HomeRouteScreen.tsx +++ b/apps/mobile/src/features/home/HomeRouteScreen.tsx @@ -4,6 +4,7 @@ import { useNavigation } from "@react-navigation/native"; import { useMemo, useState } from "react"; import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; +import { renderCompactBrandTitle } from "../../components/CompactBrandTitle"; import { useProjects, useThreadShells } from "../../state/entities"; import { usePendingNewTasks } from "../../state/use-pending-new-tasks"; import { useWorkspaceState } from "../../state/workspace"; @@ -86,7 +87,9 @@ export function HomeRouteScreen() { > <> {/* Restore the compact title in case the split branch blanked it. */} - + ()): string { + if (value === null) return "null"; + switch (typeof value) { + case "boolean": + case "number": + case "string": + return JSON.stringify(value); + case "undefined": + return "undefined"; + case "function": + // Header factories are frequently recreated inline. Their source is + // stable across equivalent renders, while a reference comparison would + // make navigation.setOptions re-enter the navigator indefinitely. + return `function:${Function.prototype.toString.call(value)}`; + case "symbol": + return `symbol:${String(value)}`; + case "bigint": + return `bigint:${String(value)}`; + case "object": { + const object = value as object; + if (seen.has(object)) return "[circular]"; + seen.add(object); + if (Array.isArray(value)) { + return `[${value.map((entry) => optionsSignature(entry, seen)).join(",")}]`; + } + // React refs carry mutable native instances that must not make static + // screen options appear different after every render. + if ("current" in object) return "[ref]"; + return `{${Object.keys(value as Record) + .sort() + .map( + (key) => + `${JSON.stringify(key)}:${optionsSignature((value as Record)[key], seen)}`, + ) + .join(",")}}`; + } + } + return String(value); +} + +function stabilizeOptionFunctions( + value: unknown, + path: string, + latestFunctions: Map unknown>, + wrappers: Map unknown>, + seen = new WeakSet(), +): unknown { + if (typeof value === "function") { + latestFunctions.set(path, value as (...args: unknown[]) => unknown); + let wrapper = wrappers.get(path); + if (!wrapper) { + wrapper = (...args: unknown[]) => { + return latestFunctions.get(path)?.(...args); + }; + wrappers.set(path, wrapper); + } + return wrapper; + } + if (Array.isArray(value)) { + if (seen.has(value)) return value; + seen.add(value); + return value.map((entry, index) => + stabilizeOptionFunctions(entry, `${path}[${index}]`, latestFunctions, wrappers, seen), + ); + } + if (value !== null && typeof value === "object") { + if (seen.has(value) || "current" in value) return value; + seen.add(value); + return Object.fromEntries( + Object.entries(value as Record).map(([key, entry]) => [ + key, + stabilizeOptionFunctions(entry, `${path}.${key}`, latestFunctions, wrappers, seen), + ]), + ); + } + return value; +} + export function NativeStackScreenOptions(props: { readonly options?: AppNativeStackNavigationOptions; readonly listeners?: Record void>; readonly name?: string; }) { const navigation = useNativeStackNavigation(); + const lastAppliedOptionsSignatureRef = useRef(undefined); + const latestOptionFunctionsRef = useRef(new Map unknown>()); + const optionFunctionWrappersRef = useRef(new Map unknown>()); const normalizedOptions = useMemo(() => normalizeScreenOptions(props.options), [props.options]); + const stableOptions = normalizedOptions + ? (stabilizeOptionFunctions( + normalizedOptions, + "options", + latestOptionFunctionsRef.current, + optionFunctionWrappersRef.current, + ) as NativeStackNavigationOptions) + : undefined; useLayoutEffect(() => { - if (!navigation || !normalizedOptions) { + if (!navigation || !stableOptions) { + return; + } + const signature = optionsSignature(stableOptions); + // Avoid re-entering navigation state when semantically equal options are + // reapplied every layout (common when callers pass unstable object literals). + if (lastAppliedOptionsSignatureRef.current === signature) { return; } - navigation.setOptions(normalizedOptions); - }, [navigation, normalizedOptions]); + lastAppliedOptionsSignatureRef.current = signature; + navigation.setOptions(stableOptions); + }, [navigation, stableOptions]); useEffect(() => { if (!navigation || !props.listeners) { diff --git a/patches/@react-navigation%2Fnative-stack@7.17.6.patch b/patches/@react-navigation%2Fnative-stack@7.17.6.patch index cd2b86fb76c8..1ec4d978529f 100644 --- a/patches/@react-navigation%2Fnative-stack@7.17.6.patch +++ b/patches/@react-navigation%2Fnative-stack@7.17.6.patch @@ -1,3 +1,5 @@ +diff --git a/lib/module/views/useHeaderConfigProps.js b/lib/module/views/useHeaderConfigProps.js +index 0b75c70b4e0d233ee3b5faaf9cfbc40d4f8ed494..eb174e3fde91a7783f132b3fb16b01175ec19220 100644 --- a/lib/module/views/useHeaderConfigProps.js +++ b/lib/module/views/useHeaderConfigProps.js @@ -19,6 +19,12 @@ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b409161ec63..6e40849babb2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,7 +76,7 @@ patchedDependencies: '@legendapp/list@3.2.0': 45e4cbcbeeca1b628b8480869374d7cbc77e3fbfa97ee0107a0d72c3c4a5d7f3 '@pierre/diffs@1.3.0-beta.5': 7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a '@react-native-menu/menu@2.0.0': 5ea3ae4bf1d9baf5443b65c269bb09621c27a68d556f713778f37b1e8d46aaae - '@react-navigation/native-stack@7.17.6': 2d7fec7933e324ccf082b1ae0df3907c35d16d48c117d628e27d9c3921c26bca + '@react-navigation/native-stack@7.17.6': c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273 effect@4.0.0-beta.78: c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5 expo-modules-jsi@56.0.10: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f react-native-gesture-handler@2.31.2: 808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3 @@ -233,7 +233,7 @@ importers: version: 7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@react-navigation/native-stack': specifier: 7.17.6 - version: 7.17.6(patch_hash=2d7fec7933e324ccf082b1ae0df3907c35d16d48c117d628e27d9c3921c26bca)(b7ff50e3b1e09b1815881c384829e9f8) + version: 7.17.6(patch_hash=c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273)(b7ff50e3b1e09b1815881c384829e9f8) '@shikijs/core': specifier: 4.2.0 version: 4.2.0 @@ -14446,7 +14446,7 @@ snapshots: optionalDependencies: '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - '@react-navigation/native-stack@7.17.6(patch_hash=2d7fec7933e324ccf082b1ae0df3907c35d16d48c117d628e27d9c3921c26bca)(b7ff50e3b1e09b1815881c384829e9f8)': + '@react-navigation/native-stack@7.17.6(patch_hash=c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273)(b7ff50e3b1e09b1815881c384829e9f8)': dependencies: '@react-navigation/elements': 2.9.26(c6a2ad0e2c930f8e3896e77c3ba11bc4) '@react-navigation/native': 7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)