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";
77
88export function LoadingScreen(props: {
99 readonly message: string;
10 /** Let the enclosing pane supply the background and top inset. */
11 readonly embedded?: boolean;
1012 readonly messagePlacement?: "above-spinner" | "below-spinner";
1113}) {
1214 const { themeAppearance: colorScheme } = useAppearancePreferences();
......@@ -14,7 +16,10 @@ export function LoadingScreen(props: {
1416 const messagePlacement = props.messagePlacement ?? "below-spinner";
1517
1618 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 >
1823 <StatusBar barStyle={colorScheme === "dark" ? "light-content" : "dark-content"} />
1924 <View className="flex-1 items-center justify-center gap-5 px-6">
2025 <BrandMark compact />
apps/mobile/src/features/threads/ThreadRouteScreen.tsx+65-20
......@@ -2,6 +2,7 @@ import { makeTurnCommandMetadata } from "../../lib/commandMetadata";
22import { buildProjectThreadStartTurnInput } from "../../lib/projectThreadStartTurn";
33import { useWorktreeSetup } from "./use-worktree-setup";
44import { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setup";
5import { ScreenHeader } from "../../components/ScreenHeader";
56import {
67 StackActions,
78 useFocusEffect,
......@@ -103,8 +104,50 @@ function firstRouteParam(value: string | string[] | undefined): string | null {
103104 return first === undefined || first.trim().length === 0 ? null : first;
104105}
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
106141function 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 );
108151}
109152
110153type ThreadRouteScreenRouteProps = StaticScreenProps<{
......@@ -123,23 +166,25 @@ function ThreadUnavailableScreen(props: {
123166 readonly onAction: () => void;
124167}) {
125168 return (
126 <ScrollView
127 contentInsetAdjustmentBehavior="automatic"
128 contentContainerStyle={{
129 flexGrow: 1,
130 justifyContent: "center",
131 paddingHorizontal: 24,
132 paddingVertical: 32,
133 }}
134 className="bg-screen flex-1"
135 >
136 <EmptyState
137 title="Thread unavailable"
138 detail="This thread is not available in the current mobile snapshot."
139 actionLabel={props.actionLabel}
140 onAction={props.onAction}
141 />
142 </ScrollView>
169 <ThreadRouteFallbackLayout>
170 <ScrollView
171 contentInsetAdjustmentBehavior="automatic"
172 contentContainerStyle={{
173 flexGrow: 1,
174 justifyContent: "center",
175 paddingHorizontal: 24,
176 paddingVertical: 32,
177 }}
178 className="bg-screen flex-1 android:bg-transparent"
179 >
180 <EmptyState
181 title="Thread unavailable"
182 detail="This thread is not available in the current mobile snapshot."
183 actionLabel={props.actionLabel}
184 onAction={props.onAction}
185 />
186 </ScrollView>
187 </ThreadRouteFallbackLayout>
143188 );
144189}
145190
......@@ -931,7 +976,7 @@ function ThreadRouteContent(
931976 <>
932977 <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>
935980 <ThreadDetailScreen
936981 canOperateThread={canOperateThread}
937982 selectedThread={selectedThreadWithDraftSettings ?? selectedThread}
......@@ -1023,7 +1068,7 @@ function ThreadRouteContent(
10231068 onSubmitUserInput={requests.onSubmitUserInput}
10241069 onDismissUserInput={requests.onDismissUserInput}
10251070 />
1026 </View>
1071 </ThreadPane>
10271072 </>
10281073 );
10291074