authorgravatar for me@pixp.ccAlex <me@pixp.cc> 2026-10-11 11:30:03-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-11 11:30:03-07:00
loga1f81a44e5ba18f099a7c43b310ddb7f7fa7fb37
tree13e5a2c3ad3bae9b3429d1c978bb54cef14629a3
parent80cd57585d7e5ca19c9a495bb34cf3bda8b5d872
signature Signed by PGP key B5690EEEBB952194

fix(android): Match thread fallback layouts to the loaded pane (#14669)


2 files changed, 71 insertions(+), 21 deletions(-)

apps/mobile/src/components/LoadingScreen.tsx+6-1
...@@ -7,6 +7,8 @@ import { BrandMark } from "./BrandMark";...@@ -7,6 +7,8 @@ import { BrandMark } from "./BrandMark";
77
8export function LoadingScreen(props: {8export function LoadingScreen(props: {
9 readonly message: string;9 readonly message: string;
10 /** Let the enclosing pane supply the background and top inset. */
11 readonly embedded?: boolean;
10 readonly messagePlacement?: "above-spinner" | "below-spinner";12 readonly messagePlacement?: "above-spinner" | "below-spinner";
11}) {13}) {
12 const { themeAppearance: colorScheme } = useAppearancePreferences();14 const { themeAppearance: colorScheme } = useAppearancePreferences();
...@@ -14,7 +16,10 @@ export function LoadingScreen(props: {...@@ -14,7 +16,10 @@ export function LoadingScreen(props: {
14 const messagePlacement = props.messagePlacement ?? "below-spinner";16 const messagePlacement = props.messagePlacement ?? "below-spinner";
1517
16 return (18 return (
17 <View className="flex-1 bg-screen" style={{ paddingTop: insets.top }}>19 <View
20 className={props.embedded ? "flex-1" : "flex-1 bg-screen"}
21 style={{ paddingTop: props.embedded ? 0 : insets.top }}
22 >
18 <StatusBar barStyle={colorScheme === "dark" ? "light-content" : "dark-content"} />23 <StatusBar barStyle={colorScheme === "dark" ? "light-content" : "dark-content"} />
19 <View className="flex-1 items-center justify-center gap-5 px-6">24 <View className="flex-1 items-center justify-center gap-5 px-6">
20 <BrandMark compact />25 <BrandMark compact />
apps/mobile/src/features/threads/ThreadRouteScreen.tsx+65-20
...@@ -2,6 +2,7 @@ import { makeTurnCommandMetadata } from "../../lib/commandMetadata";...@@ -2,6 +2,7 @@ import { makeTurnCommandMetadata } from "../../lib/commandMetadata";
2import { buildProjectThreadStartTurnInput } from "../../lib/projectThreadStartTurn";2import { buildProjectThreadStartTurnInput } from "../../lib/projectThreadStartTurn";
3import { useWorktreeSetup } from "./use-worktree-setup";3import { useWorktreeSetup } from "./use-worktree-setup";
4import { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setup";4import { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setup";
5import { ScreenHeader } from "../../components/ScreenHeader";
5import {6import {
6 StackActions,7 StackActions,
7 useFocusEffect,8 useFocusEffect,
...@@ -103,8 +104,50 @@ function firstRouteParam(value: string | string[] | undefined): string | null {...@@ -103,8 +104,50 @@ function firstRouteParam(value: string | string[] | undefined): string | null {
103 return first === undefined || first.trim().length === 0 ? null : first;104 return first === undefined || first.trim().length === 0 ? null : first;
104}105}
105106
107function ThreadPane(props: { readonly children: ReactNode }) {
108 return (
109 <View className="flex-1 bg-screen android:overflow-hidden android:rounded-t-[28px] android:bg-thread-canvas">
110 {props.children}
111 </View>
112 );
113}
114
115function ThreadRouteFallbackLayout(props: { readonly children: ReactNode }) {
116 const navigation = useNavigation();
117 const { layout } = useAdaptiveWorkspaceLayout();
118 const { themeVariables } = useAppearancePreferences();
119 if (Platform.OS !== "android") return props.children;
120
121 return (
122 <>
123 <ScreenHeader
124 title="Thread"
125 options={{ contentStyle: { backgroundColor: themeVariables["--color-header"] } }}
126 onBack={
127 layout.usesSplitView
128 ? undefined
129 : () => {
130 if (navigation.canGoBack()) navigation.goBack();
131 else navigation.dispatch(StackActions.replace("Home"));
132 }
133 }
134 hideBottomBorder
135 />
136 <ThreadPane>{props.children}</ThreadPane>
137 </>
138 );
139}
140
106function OpeningThreadLoadingScreen() {141function OpeningThreadLoadingScreen() {
107 return <LoadingScreen message="Opening thread…" messagePlacement="above-spinner" />;142 return (
143 <ThreadRouteFallbackLayout>
144 <LoadingScreen
145 message="Opening thread…"
146 messagePlacement="above-spinner"
147 embedded={Platform.OS === "android"}
148 />
149 </ThreadRouteFallbackLayout>
150 );
108}151}
109152
110type ThreadRouteScreenRouteProps = StaticScreenProps<{153type ThreadRouteScreenRouteProps = StaticScreenProps<{
...@@ -123,23 +166,25 @@ function ThreadUnavailableScreen(props: {...@@ -123,23 +166,25 @@ function ThreadUnavailableScreen(props: {
123 readonly onAction: () => void;166 readonly onAction: () => void;
124}) {167}) {
125 return (168 return (
126 <ScrollView169 <ThreadRouteFallbackLayout>
127 contentInsetAdjustmentBehavior="automatic"170 <ScrollView
128 contentContainerStyle={{171 contentInsetAdjustmentBehavior="automatic"
129 flexGrow: 1,172 contentContainerStyle={{
130 justifyContent: "center",173 flexGrow: 1,
131 paddingHorizontal: 24,174 justifyContent: "center",
132 paddingVertical: 32,175 paddingHorizontal: 24,
133 }}176 paddingVertical: 32,
134 className="bg-screen flex-1"177 }}
135 >178 className="bg-screen flex-1 android:bg-transparent"
136 <EmptyState179 >
137 title="Thread unavailable"180 <EmptyState
138 detail="This thread is not available in the current mobile snapshot."181 title="Thread unavailable"
139 actionLabel={props.actionLabel}182 detail="This thread is not available in the current mobile snapshot."
140 onAction={props.onAction}183 actionLabel={props.actionLabel}
141 />184 onAction={props.onAction}
142 </ScrollView>185 />
186 </ScrollView>
187 </ThreadRouteFallbackLayout>
143 );188 );
144}189}
145190
...@@ -931,7 +976,7 @@ function ThreadRouteContent(...@@ -931,7 +976,7 @@ function ThreadRouteContent(
931 <>976 <>
932 <GitActionProgressOverlay progress={gitActionProgress} onDismiss={dismissGitActionResult} />977 <GitActionProgressOverlay progress={gitActionProgress} onDismiss={dismissGitActionResult} />
933978
934 <View className="flex-1 bg-screen android:overflow-hidden android:rounded-t-[28px] android:bg-thread-canvas">979 <ThreadPane>
935 <ThreadDetailScreen980 <ThreadDetailScreen
936 canOperateThread={canOperateThread}981 canOperateThread={canOperateThread}
937 selectedThread={selectedThreadWithDraftSettings ?? selectedThread}982 selectedThread={selectedThreadWithDraftSettings ?? selectedThread}
...@@ -1023,7 +1068,7 @@ function ThreadRouteContent(...@@ -1023,7 +1068,7 @@ function ThreadRouteContent(
1023 onSubmitUserInput={requests.onSubmitUserInput}1068 onSubmitUserInput={requests.onSubmitUserInput}
1024 onDismissUserInput={requests.onDismissUserInput}1069 onDismissUserInput={requests.onDismissUserInput}
1025 />1070 />
1026 </View>1071 </ThreadPane>
1027 </>1072 </>
1028 );1073 );
10291074