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
11 "start:dev": "APP_VARIANT=development expo start",11 "start:dev": "APP_VARIANT=development expo start",
12 "start:preview": "APP_VARIANT=preview expo start",12 "start:preview": "APP_VARIANT=preview expo start",
13 "start:prod": "APP_VARIANT=production expo start",13 "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",
15 "screenshots": "node ../../scripts/mobile-showcase.ts",15 "screenshots": "node ../../scripts/mobile-showcase.ts",
16 "android": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && expo run:android",16 "android": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && expo run:android",
17 "android:dev": "APP_VARIANT=development EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && REACT_NATIVE_PACKAGER_HOSTNAME=localhost expo run:android",17 "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
15import { useResolveClassNames } from "uniwind";15import { useResolveClassNames } from "uniwind";
16 16
17import { AppText as Text } from "./components/AppText";17import { AppText as Text } from "./components/AppText";
18import { getCompactBrandHeaderOptions } from "./components/CompactBrandTitle";
18import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen";19import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen";
19import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation";20import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation";
20import { ClerkSettingsSheetDetentProvider } from "./features/cloud/ClerkSettingsSheetDetent";21import { ClerkSettingsSheetDetentProvider } from "./features/cloud/ClerkSettingsSheetDetent";
@@ -392,7 +393,7 @@ export const RootStack = createNativeStackNavigator({ · 371 unmodified lines
392 ...GLASS_HEADER_OPTIONS,393 ...GLASS_HEADER_OPTIONS,
393 contentStyle: { backgroundColor: "transparent" },394 contentStyle: { backgroundColor: "transparent" },
394 headerBackVisible: false,395 headerBackVisible: false,
395 title: "Threads",396 ...getCompactBrandHeaderOptions(),
396 },397 },
397 }),398 }),
398 Thread: createNativeStackScreen({399 Thread: createNativeStackScreen({
apps/mobile/src/components/CompactBrandTitle.tsx+121−0
@@ -0,0 +1,121 @@
1import Constants from "expo-constants";
2import type {
3 NativeStackHeaderItem,
4 NativeStackNavigationOptions,
5} from "@react-navigation/native-stack";
6import { Platform, View } from "react-native";
7
8import { AppText as Text } from "./AppText";
9import { T3Wordmark } from "./T3Wordmark";
10import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics";
11import { resolveMobileStageLabel } from "../lib/mobileBranding";
12import { useThemeColor } from "../lib/useThemeColor";
13import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../native/native-glass";
14
15// Native leading items inherit different UIKit margins than title views.
16const IOS_NATIVE_LEADING_TITLE_OFFSET = -6;
17const IPAD_NATIVE_LEADING_TITLE_OFFSET = 7;
18
19/**
20 * Compact brand lockup sized for native navigation bars.
21 */
22export 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
90export function renderCompactBrandTitle() {
91 return <CompactBrandTitle />;
92}
93
94export function renderCompactBrandHeaderItems(): NativeStackHeaderItem[] {
95 return [
96 {
97 element: <CompactBrandTitle nativeLeadingItem />,
98 hidesSharedBackground: true,
99 type: "custom",
100 },
101 ];
102}
103
104export 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
3import { useNavigation } from "@react-navigation/native";3import { useNavigation } from "@react-navigation/native";
4import { useEffect, useMemo, useState } from "react";4import { useEffect, useMemo, useState } from "react";
5 5
6import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle";
6import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";7import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
7import { useProjects, useThreadShells } from "../../state/entities";8import { useProjects, useThreadShells } from "../../state/entities";
8import { usePendingNewTasks } from "../../state/use-pending-new-tasks";9import { usePendingNewTasks } from "../../state/use-pending-new-tasks";
@@ -93,7 +94,9 @@ export function HomeRouteScreen() { · 84 unmodified lines
93 if (layout.usesSplitView) {94 if (layout.usesSplitView) {
94 return (95 return (
95 <>96 <>
96 <NativeStackScreenOptions options={{ title: "", headerTitle: "" }} />97 <NativeStackScreenOptions
98 options={{ title: "", headerTitle: "", unstable_headerLeftItems: () => [] }}
99 />
97 <WorkspaceSidebarToolbar100 <WorkspaceSidebarToolbar
98 afterSidebarButton={101 afterSidebarButton={
99 <NativeHeaderToolbar.Button102 <NativeHeaderToolbar.Button
@@ -115,8 +118,8 @@ export function HomeRouteScreen() { · 15 unmodified lines
115 onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}118 onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
116 >119 >
117 <>120 <>
118 {/* Restore the compact title in case the split branch blanked it. */}121 {/* Restore the compact title after the split branch blanks the detail header. */}
119 <NativeStackScreenOptions options={{ title: "Threads", headerTitle: "Threads" }} />122 <NativeStackScreenOptions options={getCompactBrandHeaderOptions()} />
120 <HomeHeader123 <HomeHeader
121 environments={environments}124 environments={environments}
122 projects={projectFilterOptions}125 projects={projectFilterOptions}
apps/mobile/src/features/threads/sidebar-navigation-shell.tsx+2−2
@@ -11,6 +11,7 @@ import { · 10 unmodified lines
11import type { ReactNode } from "react";11import type { ReactNode } from "react";
12import { Platform, useColorScheme } from "react-native";12import { Platform, useColorScheme } from "react-native";
13 13
14import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle";
14import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";15import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
15import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";16import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
16 17
@@ -35,10 +36,9 @@ const SIDEBAR_SCREEN_OPTIONS: SidebarScreenOptions = { · 18 unmodified lines
35 headerShadowVisible: false,36 headerShadowVisible: false,
36 headerShown: true,37 headerShown: true,
37 headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? { backgroundColor: "transparent" } : undefined,38 headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? { backgroundColor: "transparent" } : undefined,
38 headerTitleStyle: { fontSize: 18, fontWeight: "800" },39 ...getCompactBrandHeaderOptions({ fontSize: 18, fontWeight: "800" }),
39 headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED,40 headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED,
40 scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? SCROLL_EDGE_EFFECTS : undefined,41 scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? SCROLL_EDGE_EFFECTS : undefined,
41 title: "Threads",
42 unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined,42 unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined,
43};43};
44 44
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
15import { ControlPillMenu } from "../../components/ControlPill";15import { ControlPillMenu } from "../../components/ControlPill";
16import { ProjectFavicon } from "../../components/ProjectFavicon";16import { ProjectFavicon } from "../../components/ProjectFavicon";
17import { cn } from "../../lib/cn";17import { cn } from "../../lib/cn";
18import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics";
18import { relativeTime } from "../../lib/time";19import { relativeTime } from "../../lib/time";
19import { useThemeColor } from "../../lib/useThemeColor";20import { useThemeColor } from "../../lib/useThemeColor";
20import type { PendingNewTask } from "../../state/use-pending-new-tasks";21import type { PendingNewTask } from "../../state/use-pending-new-tasks";
@@ -33,7 +34,7 @@ import { ThreadSearchMatchExcerpt } from "./thread-search-match"; · 12 unmodified lines
33export type ThreadListVariant = "compact" | "sidebar";34export type ThreadListVariant = "compact" | "sidebar";
34 35
35/** Left inset that aligns compact secondary rows with the title column. */36/** Left inset that aligns compact secondary rows with the title column. */
36export const THREAD_LIST_COMPACT_INSET = 20;37export const THREAD_LIST_COMPACT_INSET = HOME_HORIZONTAL_INSET;
37const SIDEBAR_ROW_RADIUS = 12;38const SIDEBAR_ROW_RADIUS = 12;
38 39
39function pullRequestTintColor(40function 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. */
2export const HOME_HORIZONTAL_INSET = 20;
3
4/** Compensates for the tighter native sidebar title margin on iPad. */
5export const IPAD_HOME_TITLE_OFFSET = 10;
apps/mobile/src/lib/mobileBranding.test.ts+14−0
@@ -0,0 +1,14 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import { resolveMobileStageLabel } from "./mobileBranding";
4
5describe("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 @@
1export type MobileStageLabel = "Alpha" | "Dev" | "Nightly";
2
3export 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
370 const navigation = useNativeStackNavigation();370 const navigation = useNativeStackNavigation();
371 const items = useMemo(() => collectToolbarItems(props.children), [props.children]);371 const items = useMemo(() => collectToolbarItems(props.children), [props.children]);
372 372
373 useEffect(() => {373 // Swap toolbar owners before paint so split and compact headers cannot clear each other.
374 useLayoutEffect(() => {
374 if (!navigation) {375 if (!navigation) {
375 return;376 return;
376 }377 }
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
43active. Both platforms record readiness in the simulator/emulator app container. A final settle43active. Both platforms record readiness in the simulator/emulator app container. A final settle
44delay allows native terminal and Git review data to finish rendering.44delay allows native terminal and Git review data to finish rendering.
45 45
46A full capture regenerates the selected native project with Expo's clean development prebuild before46A full capture regenerates the selected native project with Expo's clean production prebuild before
47building it. Use --skip-build for repeated captures after the first build.47building it. Use --skip-build for repeated captures after the first build.
48 48
49The harness uses fixed Metro port `8199`, which separates it from Expo's normal default port but is49The 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
244});244});
245 245
246it("maps capture scenes to the real application routes", () => {246it("maps capture scenes to the real application routes", () => {
247 assert.equal(showcaseSceneUrl("threads", "environment-1"), "t3code-dev://");247 assert.equal(showcaseSceneUrl("threads", "environment-1"), "t3code://");
248 assert.equal(248 assert.equal(showcaseSceneUrl("environments", "environment-1"), "t3code://settings/environments");
249 showcaseSceneUrl("environments", "environment-1"),
250 "t3code-dev://settings/environments",
251 );
252 assert.equal(249 assert.equal(
253 showcaseSceneUrl("thread", "environment-1"),250 showcaseSceneUrl("thread", "environment-1"),
254 "t3code-dev://threads/environment-1/remote-command-center",251 "t3code://threads/environment-1/remote-command-center",
255 );252 );
256 assert.equal(253 assert.equal(
257 showcaseSceneUrl("terminal", "environment-1"),254 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",
259 );256 );
260 assert.equal(257 assert.equal(
261 showcaseSceneUrl("review", "environment-1"),258 showcaseSceneUrl("review", "environment-1"),
262 "t3code-dev://threads/environment-1/remote-command-center/review",259 "t3code://threads/environment-1/remote-command-center/review",
263 );260 );
264});261});
265 262
scripts/mobile-showcase.ts+6−6
@@ -31,14 +31,14 @@ import { · 30 unmodified lines
31 31
32const REPO_ROOT = NodePath.resolve(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), "..");32const REPO_ROOT = NodePath.resolve(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), "..");
33const MOBILE_ROOT = NodePath.join(REPO_ROOT, "apps/mobile");33const MOBILE_ROOT = NodePath.join(REPO_ROOT, "apps/mobile");
34const ANDROID_PACKAGE = "com.t3tools.t3code.dev";34const ANDROID_PACKAGE = "com.t3tools.t3code";
35const APP_SCHEME = "t3code-dev";35const APP_SCHEME = "t3code";
36const IOS_READY_FILENAME = "T3ShowcaseReadyScene";36const IOS_READY_FILENAME = "T3ShowcaseReadyScene";
37const SERVER_HOST = "0.0.0.0";37const SERVER_HOST = "0.0.0.0";
38const IOS_SIMULATOR_ARCH = NodeProcess.arch === "arm64" ? "arm64" : "x86_64";38const IOS_SIMULATOR_ARCH = NodeProcess.arch === "arm64" ? "arm64" : "x86_64";
39const IOS_APP_PATH = NodePath.join(39const IOS_APP_PATH = NodePath.join(
40 MOBILE_ROOT,40 MOBILE_ROOT,
41 ".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/T3CodeDev.app",41 ".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/T3Code.app",
42);42);
43const ANDROID_APK_PATH = NodePath.join(43const ANDROID_APK_PATH = NodePath.join(
44 MOBILE_ROOT,44 MOBILE_ROOT,
@@ -58,7 +58,7 @@ const ANDROID_SDK_ROOT = resolveAndroidSdkRoot(NodeProcess.env); · 13 unmodified lines
58const MOBILE_BUILD_ENV = {58const MOBILE_BUILD_ENV = {
59 ...NodeProcess.env,59 ...NodeProcess.env,
60 ANDROID_HOME: ANDROID_SDK_ROOT,60 ANDROID_HOME: ANDROID_SDK_ROOT,
61 APP_VARIANT: "development",61 APP_VARIANT: "production",
62 EXPO_NO_GIT_STATUS: "1",62 EXPO_NO_GIT_STATUS: "1",
63 JAVA_HOME:63 JAVA_HOME:
64 NodeProcess.env.JAVA_HOME ??64 NodeProcess.env.JAVA_HOME ??
@@ -664,9 +664,9 @@ async function buildIos(): Promise<string> { · 599 unmodified lines
664 "xcodebuild",664 "xcodebuild",
665 [665 [
666 "-workspace",666 "-workspace",
667 NodePath.join(MOBILE_ROOT, "ios/T3CodeDev.xcworkspace"),667 NodePath.join(MOBILE_ROOT, "ios/T3Code.xcworkspace"),
668 "-scheme",668 "-scheme",
669 "T3CodeDev",669 "T3Code",
670 "-configuration",670 "-configuration",
671 "Debug",671 "Debug",
672 "-sdk",672 "-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