History

fix(mobile): restore iOS Threads branding (#4862)

Alex committed 3 months ago

Commit cb293fe1 parent 1d694dc13 files · +174 −24Browse files

Unified · Split

13 files
  1. apps/mobile/package.json +1 −1
  2. apps/mobile/src/Stack.tsx +2 −1
  3. apps/mobile/src/components/CompactBrandTitle.tsx +121 −0
  4. apps/mobile/src/features/home/HomeRouteScreen.tsx +6 −3
  5. apps/mobile/src/features/threads/sidebar-navigation-shell.tsx +2 −2
  6. apps/mobile/src/features/threads/thread-list-items.tsx +2 −1
  7. apps/mobile/src/lib/layoutMetrics.ts +5 −0
  8. apps/mobile/src/lib/mobileBranding.test.ts +14 −0
  9. apps/mobile/src/lib/mobileBranding.ts +7 −0
  10. apps/mobile/src/native/StackHeader.tsx +2 −1
  11. docs/operations/mobile-app-store-screenshots.md +1 −1
  12. scripts/mobile-showcase.test.ts +5 −8
  13. scripts/mobile-showcase.ts +6 −6
apps/mobile/package.json+1−1
@@ -11,7 +11,7 @@ · 10 unmodified lines
1111 "start:dev": "APP_VARIANT=development expo start",
1212 "start:preview": "APP_VARIANT=preview expo start",
1313 "start:prod": "APP_VARIANT=production expo start",
14− "showcase": "APP_VARIANT=development EXPO_PUBLIC_SHOWCASE=1 expo start --dev-client --scheme t3code-dev --clear",
14+ "showcase": "APP_VARIANT=production EXPO_PUBLIC_SHOWCASE=1 expo start --dev-client --scheme t3code --clear",
1515 "screenshots": "node ../../scripts/mobile-showcase.ts",
1616 "android": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && expo run:android",
1717 "android:dev": "APP_VARIANT=development EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && REACT_NATIVE_PACKAGER_HOSTNAME=localhost expo run:android",
apps/mobile/src/Stack.tsx+2−1
@@ -15,6 +15,7 @@ import { DynamicColorIOS, Platform, Pressable, ScrollView, StyleSheet } from "re · 14 unmodified lines
1515 import { useResolveClassNames } from "uniwind";
1616
1717 import { AppText as Text } from "./components/AppText";
18+import { getCompactBrandHeaderOptions } from "./components/CompactBrandTitle";
1819 import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen";
1920 import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation";
2021 import { ClerkSettingsSheetDetentProvider } from "./features/cloud/ClerkSettingsSheetDetent";
@@ -392,7 +393,7 @@ export const RootStack = createNativeStackNavigator({ · 371 unmodified lines
392393 ...GLASS_HEADER_OPTIONS,
393394 contentStyle: { backgroundColor: "transparent" },
394395 headerBackVisible: false,
395− title: "Threads",
396+ ...getCompactBrandHeaderOptions(),
396397 },
397398 }),
398399 Thread: createNativeStackScreen({
apps/mobile/src/components/CompactBrandTitle.tsx+121−0
@@ -0,0 +1,121 @@
1+import Constants from "expo-constants";
2+import type {
3+ NativeStackHeaderItem,
4+ NativeStackNavigationOptions,
5+} from "@react-navigation/native-stack";
6+import { Platform, View } from "react-native";
7+
8+import { AppText as Text } from "./AppText";
9+import { T3Wordmark } from "./T3Wordmark";
10+import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics";
11+import { resolveMobileStageLabel } from "../lib/mobileBranding";
12+import { useThemeColor } from "../lib/useThemeColor";
13+import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../native/native-glass";
14+
15+// Native leading items inherit different UIKit margins than title views.
16+const IOS_NATIVE_LEADING_TITLE_OFFSET = -6;
17+const IPAD_NATIVE_LEADING_TITLE_OFFSET = 7;
18+
19+/**
20+ * Compact brand lockup sized for native navigation bars.
21+ */
22+export function CompactBrandTitle(
23+ props: {
24+ readonly nativeLeadingItem?: boolean;
25+ } = {},
26+) {
27+ const iconColor = useThemeColor("--color-icon");
28+ const mutedColor = useThemeColor("--color-foreground-muted");
29+ const subtleColor = useThemeColor("--color-subtle");
30+ const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);
31+ const titleOffset =
32+ Platform.OS !== "ios"
33+ ? 0
34+ : props.nativeLeadingItem
35+ ? Platform.isPad
36+ ? IPAD_NATIVE_LEADING_TITLE_OFFSET
37+ : IOS_NATIVE_LEADING_TITLE_OFFSET
38+ : Platform.isPad
39+ ? IPAD_HOME_TITLE_OFFSET
40+ : 0;
41+
42+ return (
43+ <View
44+ aria-level={1}
45+ accessibilityLabel="T3 Code, Threads"
46+ accessible
47+ role="heading"
48+ style={{
49+ alignItems: "center",
50+ flexDirection: "row",
51+ gap: 6,
52+ marginLeft: titleOffset,
53+ }}
54+ >
55+ <T3Wordmark color={iconColor} height={15} />
56+ <Text
57+ style={{
58+ color: mutedColor,
59+ fontFamily: "DMSans-Medium",
60+ fontSize: 21,
61+ letterSpacing: -0.5,
62+ }}
63+ >
64+ Code
65+ </Text>
66+ <View
67+ style={{
68+ backgroundColor: subtleColor,
69+ borderRadius: 999,
70+ paddingHorizontal: 6,
71+ paddingVertical: 2,
72+ }}
73+ >
74+ <Text
75+ style={{
76+ color: mutedColor,
77+ fontFamily: "DMSans-Bold",
78+ fontSize: 9,
79+ letterSpacing: 0.9,
80+ textTransform: "uppercase",
81+ }}
82+ >
83+ {stageLabel}
84+ </Text>
85+ </View>
86+ </View>
87+ );
88+}
89+
90+export function renderCompactBrandTitle() {
91+ return <CompactBrandTitle />;
92+}
93+
94+export function renderCompactBrandHeaderItems(): NativeStackHeaderItem[] {
95+ return [
96+ {
97+ element: <CompactBrandTitle nativeLeadingItem />,
98+ hidesSharedBackground: true,
99+ type: "custom",
100+ },
101+ ];
102+}
103+
104+export function getCompactBrandHeaderOptions(
105+ fallbackTitleStyle?: NativeStackNavigationOptions["headerTitleStyle"],
106+): NativeStackNavigationOptions {
107+ if (Platform.OS === "ios" && NATIVE_LIQUID_GLASS_SUPPORTED) {
108+ return {
109+ headerTitle: "Threads",
110+ headerTitleStyle: { color: "transparent", fontSize: 18, fontWeight: "800" },
111+ title: "Threads",
112+ unstable_headerLeftItems: renderCompactBrandHeaderItems,
113+ };
114+ }
115+
116+ return {
117+ headerTitle: renderCompactBrandTitle,
118+ headerTitleStyle: fallbackTitleStyle,
119+ title: "Threads",
120+ };
121+}
apps/mobile/src/features/home/HomeRouteScreen.tsx+6−3
@@ -3,6 +3,7 @@ import * as Order from "effect/Order"; · 2 unmodified lines
33 import { useNavigation } from "@react-navigation/native";
44 import { useEffect, useMemo, useState } from "react";
55
6+import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle";
67 import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
78 import { useProjects, useThreadShells } from "../../state/entities";
89 import { usePendingNewTasks } from "../../state/use-pending-new-tasks";
@@ -93,7 +94,9 @@ export function HomeRouteScreen() { · 84 unmodified lines
9394 if (layout.usesSplitView) {
9495 return (
9596 <>
96− <NativeStackScreenOptions options={{ title: "", headerTitle: "" }} />
97+ <NativeStackScreenOptions
98+ options={{ title: "", headerTitle: "", unstable_headerLeftItems: () => [] }}
99+ />
97100 <WorkspaceSidebarToolbar
98101 afterSidebarButton={
99102 <NativeHeaderToolbar.Button
@@ -115,8 +118,8 @@ export function HomeRouteScreen() { · 15 unmodified lines
115118 onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
116119 >
117120 <>
118− {/* Restore the compact title in case the split branch blanked it. */}
119− <NativeStackScreenOptions options={{ title: "Threads", headerTitle: "Threads" }} />
121+ {/* Restore the compact title after the split branch blanks the detail header. */}
122+ <NativeStackScreenOptions options={getCompactBrandHeaderOptions()} />
120123 <HomeHeader
121124 environments={environments}
122125 projects={projectFilterOptions}
apps/mobile/src/features/threads/sidebar-navigation-shell.tsx+2−2
@@ -11,6 +11,7 @@ import { · 10 unmodified lines
1111 import type { ReactNode } from "react";
1212 import { Platform, useColorScheme } from "react-native";
1313
14+import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle";
1415 import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
1516 import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
1617
@@ -35,10 +36,9 @@ const SIDEBAR_SCREEN_OPTIONS: SidebarScreenOptions = { · 18 unmodified lines
3536 headerShadowVisible: false,
3637 headerShown: true,
3738 headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? { backgroundColor: "transparent" } : undefined,
38− headerTitleStyle: { fontSize: 18, fontWeight: "800" },
39+ ...getCompactBrandHeaderOptions({ fontSize: 18, fontWeight: "800" }),
3940 headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED,
4041 scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? SCROLL_EDGE_EFFECTS : undefined,
41− title: "Threads",
4242 unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined,
4343 };
4444
apps/mobile/src/features/threads/thread-list-items.tsx+2−1
@@ -15,6 +15,7 @@ import { AppText as Text } from "../../components/AppText"; · 14 unmodified lines
1515 import { ControlPillMenu } from "../../components/ControlPill";
1616 import { ProjectFavicon } from "../../components/ProjectFavicon";
1717 import { cn } from "../../lib/cn";
18+import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics";
1819 import { relativeTime } from "../../lib/time";
1920 import { useThemeColor } from "../../lib/useThemeColor";
2021 import type { PendingNewTask } from "../../state/use-pending-new-tasks";
@@ -33,7 +34,7 @@ import { ThreadSearchMatchExcerpt } from "./thread-search-match"; · 12 unmodified lines
3334 export type ThreadListVariant = "compact" | "sidebar";
3435
3536 /** Left inset that aligns compact secondary rows with the title column. */
36−export const THREAD_LIST_COMPACT_INSET = 20;
37+export const THREAD_LIST_COMPACT_INSET = HOME_HORIZONTAL_INSET;
3738 const SIDEBAR_ROW_RADIUS = 12;
3839
3940 function pullRequestTintColor(
apps/mobile/src/lib/layoutMetrics.ts+5−0
@@ -0,0 +1,5 @@
1+/** Horizontal inset shared by the home header and compact thread list. */
2+export const HOME_HORIZONTAL_INSET = 20;
3+
4+/** Compensates for the tighter native sidebar title margin on iPad. */
5+export const IPAD_HOME_TITLE_OFFSET = 10;
apps/mobile/src/lib/mobileBranding.test.ts+14−0
@@ -0,0 +1,14 @@
1+import { describe, expect, it } from "vite-plus/test";
2+
3+import { resolveMobileStageLabel } from "./mobileBranding";
4+
5+describe("resolveMobileStageLabel", () => {
6+ it.each([
7+ ["development", "Dev"],
8+ ["preview", "Nightly"],
9+ ["production", "Alpha"],
10+ [undefined, "Alpha"],
11+ ])("maps %s builds to %s", (appVariant, expected) => {
12+ expect(resolveMobileStageLabel(appVariant)).toBe(expected);
13+ });
14+});
apps/mobile/src/lib/mobileBranding.ts+7−0
@@ -0,0 +1,7 @@
1+export type MobileStageLabel = "Alpha" | "Dev" | "Nightly";
2+
3+export function resolveMobileStageLabel(appVariant: unknown): MobileStageLabel {
4+ if (appVariant === "development") return "Dev";
5+ if (appVariant === "preview") return "Nightly";
6+ return "Alpha";
7+}
apps/mobile/src/native/StackHeader.tsx+2−1
@@ -370,7 +370,8 @@ function NativeHeaderToolbarRoot(props: { · 369 unmodified lines
370370 const navigation = useNativeStackNavigation();
371371 const items = useMemo(() => collectToolbarItems(props.children), [props.children]);
372372
373− useEffect(() => {
373+ // Swap toolbar owners before paint so split and compact headers cannot clear each other.
374+ useLayoutEffect(() => {
374375 if (!navigation) {
375376 return;
376377 }
docs/operations/mobile-app-store-screenshots.md+1−1
@@ -43,7 +43,7 @@ Captures wait for the real environment snapshot to hydrate and for the requested · 42 unmodified lines
4343 active. Both platforms record readiness in the simulator/emulator app container. A final settle
4444 delay allows native terminal and Git review data to finish rendering.
4545
46−A full capture regenerates the selected native project with Expo's clean development prebuild before
46+A full capture regenerates the selected native project with Expo's clean production prebuild before
4747 building it. Use --skip-build for repeated captures after the first build.
4848
4949 The harness uses fixed Metro port `8199`, which separates it from Expo's normal default port but is
scripts/mobile-showcase.test.ts+5−8
@@ -244,22 +244,19 @@ it("selects a reachable LAN IPv4 address", () => { · 243 unmodified lines
244244 });
245245
246246 it("maps capture scenes to the real application routes", () => {
247− assert.equal(showcaseSceneUrl("threads", "environment-1"), "t3code-dev://");
248− assert.equal(
249− showcaseSceneUrl("environments", "environment-1"),
250− "t3code-dev://settings/environments",
251− );
247+ assert.equal(showcaseSceneUrl("threads", "environment-1"), "t3code://");
248+ assert.equal(showcaseSceneUrl("environments", "environment-1"), "t3code://settings/environments");
252249 assert.equal(
253250 showcaseSceneUrl("thread", "environment-1"),
254− "t3code-dev://threads/environment-1/remote-command-center",
251+ "t3code://threads/environment-1/remote-command-center",
255252 );
256253 assert.equal(
257254 showcaseSceneUrl("terminal", "environment-1"),
258− "t3code-dev://threads/environment-1/remote-command-center/terminal?terminalId=term-1",
255+ "t3code://threads/environment-1/remote-command-center/terminal?terminalId=term-1",
259256 );
260257 assert.equal(
261258 showcaseSceneUrl("review", "environment-1"),
262− "t3code-dev://threads/environment-1/remote-command-center/review",
259+ "t3code://threads/environment-1/remote-command-center/review",
263260 );
264261 });
265262
scripts/mobile-showcase.ts+6−6
@@ -31,14 +31,14 @@ import { · 30 unmodified lines
3131
3232 const REPO_ROOT = NodePath.resolve(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), "..");
3333 const MOBILE_ROOT = NodePath.join(REPO_ROOT, "apps/mobile");
34−const ANDROID_PACKAGE = "com.t3tools.t3code.dev";
35−const APP_SCHEME = "t3code-dev";
34+const ANDROID_PACKAGE = "com.t3tools.t3code";
35+const APP_SCHEME = "t3code";
3636 const IOS_READY_FILENAME = "T3ShowcaseReadyScene";
3737 const SERVER_HOST = "0.0.0.0";
3838 const IOS_SIMULATOR_ARCH = NodeProcess.arch === "arm64" ? "arm64" : "x86_64";
3939 const IOS_APP_PATH = NodePath.join(
4040 MOBILE_ROOT,
41− ".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/T3CodeDev.app",
41+ ".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/T3Code.app",
4242 );
4343 const ANDROID_APK_PATH = NodePath.join(
4444 MOBILE_ROOT,
@@ -58,7 +58,7 @@ const ANDROID_SDK_ROOT = resolveAndroidSdkRoot(NodeProcess.env); · 13 unmodified lines
5858 const MOBILE_BUILD_ENV = {
5959 ...NodeProcess.env,
6060 ANDROID_HOME: ANDROID_SDK_ROOT,
61− APP_VARIANT: "development",
61+ APP_VARIANT: "production",
6262 EXPO_NO_GIT_STATUS: "1",
6363 JAVA_HOME:
6464 NodeProcess.env.JAVA_HOME ??
@@ -664,9 +664,9 @@ async function buildIos(): Promise<string> { · 599 unmodified lines
664664 "xcodebuild",
665665 [
666666 "-workspace",
667− NodePath.join(MOBILE_ROOT, "ios/T3CodeDev.xcworkspace"),
667+ NodePath.join(MOBILE_ROOT, "ios/T3Code.xcworkspace"),
668668 "-scheme",
669− "T3CodeDev",
669+ "T3Code",
670670 "-configuration",
671671 "Debug",
672672 "-sdk",

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t