authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-18 16:50:19-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-18 16:50:19-07:00
log8c3b5bef17a637bf53f297189350359790ef7afb
treee331bfdec7c792784270861b839672439ffca643
parentd99bc5a4d707df2506e60cbb411b8d30022bbf89
signature Signed by PGP key B5690EEEBB952194

fix(mobile): prefer pull-to-refresh on list screens (#12515)


9 files changed, 166 insertions(+), 77 deletions(-)

apps/mobile/src/components/ScreenHeader.android.tsx+1-1
...@@ -138,7 +138,7 @@ export function ScreenHeader(props: ScreenHeaderProps) {...@@ -138,7 +138,7 @@ export function ScreenHeader(props: ScreenHeaderProps) {
138 icon: "magnifyingglass",138 icon: "magnifyingglass",
139 onPress: () => setSearchOpen(true),139 onPress: () => setSearchOpen(true),
140 },140 },
141 ...(search.onRefresh141 ...(search.refreshInToolbar && search.onRefresh
142 ? [142 ? [
143 {143 {
144 accessibilityLabel: search.refreshAccessibilityLabel ?? "Refresh",144 accessibilityLabel: search.refreshAccessibilityLabel ?? "Refresh",
apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx+10-3
...@@ -14,6 +14,7 @@ import { SymbolView } from "../../components/AppSymbol";...@@ -14,6 +14,7 @@ import { SymbolView } from "../../components/AppSymbol";
14import { useCallback, useMemo, useRef, type ComponentProps } from "react";14import { useCallback, useMemo, useRef, type ComponentProps } from "react";
15import {15import {
16 ActivityIndicator,16 ActivityIndicator,
17 Platform,
17 Pressable,18 Pressable,
18 RefreshControl,19 RefreshControl,
19 useWindowDimensions,20 useWindowDimensions,
...@@ -63,9 +64,6 @@ function ArchivedThreadsHeader(props: {...@@ -63,9 +64,6 @@ function ArchivedThreadsHeader(props: {
63 compactPlaceholder: "Search",64 compactPlaceholder: "Search",
64 mode: "inline",65 mode: "inline",
65 compactToolbar: width < 700,66 compactToolbar: width < 700,
66 onRefresh: props.onRefresh,
67 refreshInToolbar: true,
68 refreshAccessibilityLabel: "Refresh archived threads",
69 }}67 }}
70 menus={[68 menus={[
71 {69 {
...@@ -110,6 +108,15 @@ function ArchivedThreadsHeader(props: {...@@ -110,6 +108,15 @@ function ArchivedThreadsHeader(props: {
110 },108 },
111 ],109 ],
112 },110 },
111 ...(Platform.OS === "android"
112 ? [
113 {
114 id: "refresh",
115 title: "Refresh archived threads",
116 onPress: props.onRefresh,
117 },
118 ]
119 : []),
113 ],120 ],
114 },121 },
115 ]}122 ]}
apps/mobile/src/features/connection/CloudEnvironmentRows.tsx+3-26
...@@ -41,16 +41,14 @@ interface CloudEnvironmentRowsProps {...@@ -41,16 +41,14 @@ interface CloudEnvironmentRowsProps {
41 readonly showcaseAvailableEnvironments?: ReadonlyArray<RelayEnvironmentView>;41 readonly showcaseAvailableEnvironments?: ReadonlyArray<RelayEnvironmentView>;
42 readonly showcaseSignedIn?: boolean;42 readonly showcaseSignedIn?: boolean;
43 /**43 /**
44 * Hide the "T3 Connect" section title + refresh button for hosts that44 * Hide the "T3 Connect" section title when the host provides its own header.
45 * provide their own chrome (the onboarding sheet's native header and
46 * pull-to-refresh).
47 */45 */
48 readonly showHeader?: boolean;46 readonly showHeader?: boolean;
49}47}
5048
51/**49/**
52 * "T3 Connect" section: every environment published to the signed-in account,50 * "T3 Connect" section: every environment published to the signed-in account,
53 * with connect switches, availability status, refresh, and loading/error51 * with connect switches, availability status, and loading/error
54 * states. Shared between the Settings environments screen and the T3 Connect52 * states. Shared between the Settings environments screen and the T3 Connect
55 * onboarding sheet.53 * onboarding sheet.
56 *54 *
...@@ -120,29 +118,8 @@ function CloudEnvironmentRowsContent(...@@ -120,29 +118,8 @@ function CloudEnvironmentRowsContent(
120 return (118 return (
121 <View collapsable={false} className={cn("gap-3", showHeader && "mt-5")}>119 <View collapsable={false} className={cn("gap-3", showHeader && "mt-5")}>
122 {showHeader ? (120 {showHeader ? (
123 <View className="flex-row items-center justify-between px-1">121 <View className="px-1">
124 <Text className="text-sm font-t3-bold uppercase text-foreground-muted">T3 Connect</Text>122 <Text className="text-sm font-t3-bold uppercase text-foreground-muted">T3 Connect</Text>
125 {discoveryAvailable ? (
126 <Pressable
127 accessibilityRole="button"
128 disabled={controller.relayDiscovery.isRefreshing}
129 onPress={() => {
130 void controller.refreshRelayEnvironments();
131 }}
132 className="h-9 w-9 items-center justify-center rounded-full bg-subtle active:opacity-70 disabled:opacity-50"
133 >
134 {controller.relayDiscovery.isRefreshing ? (
135 <ActivityIndicator colorClassName={"accent-icon"} size="small" />
136 ) : (
137 <SymbolView
138 name="arrow.clockwise"
139 size={14}
140 tintColorClassName={"accent-icon"}
141 type="monochrome"
142 />
143 )}
144 </Pressable>
145 ) : null}
146 </View>123 </View>
147 ) : null}124 ) : null}
148125
apps/mobile/src/features/files/FileTreeBrowser.tsx+21-11
...@@ -234,15 +234,6 @@ export function FileTreeBrowser(props: {...@@ -234,15 +234,6 @@ export function FileTreeBrowser(props: {
234 ],234 ],
235 );235 );
236236
237 if (props.error && props.entries.length === 0) {
238 return (
239 <View className="flex-1 bg-sheet px-4 py-5">
240 <Text className="text-sm font-t3-bold text-foreground">Files unavailable</Text>
241 <Text className="mt-1 text-xs leading-normal text-foreground-muted">{props.error}</Text>
242 </View>
243 );
244 }
245
246 // SPIKE: render the FlatList as the screen's DIRECT content (no wrapping View), and237 // SPIKE: render the FlatList as the screen's DIRECT content (no wrapping View), and
247 // mirror the Home ScrollView exactly — `contentInsetAdjustmentBehavior: "automatic"`238 // mirror the Home ScrollView exactly — `contentInsetAdjustmentBehavior: "automatic"`
248 // with NO manual contentInset. iOS only applies the nav-bar top inset + scroll-edge239 // with NO manual contentInset. iOS only applies the nav-bar top inset + scroll-edge
...@@ -250,6 +241,7 @@ export function FileTreeBrowser(props: {...@@ -250,6 +241,7 @@ export function FileTreeBrowser(props: {
250 // flex-1 Views is ignored, which is why the tree rendered under the header with no blur.241 // flex-1 Views is ignored, which is why the tree rendered under the header with no blur.
251 return (242 return (
252 <FlatList243 <FlatList
244 alwaysBounceVertical
253 className="flex-1"245 className="flex-1"
254 data={visibleNodes}246 data={visibleNodes}
255 keyExtractor={(item) => item.node.path}247 keyExtractor={(item) => item.node.path}
...@@ -270,7 +262,7 @@ export function FileTreeBrowser(props: {...@@ -270,7 +262,7 @@ export function FileTreeBrowser(props: {
270 renderItem={renderItem}262 renderItem={renderItem}
271 ListHeaderComponent={263 ListHeaderComponent={
272 <>264 <>
273 {props.error ? (265 {props.error && props.entries.length > 0 ? (
274 <Text accessibilityRole="alert" className="mx-4 my-2 text-xs text-foreground-muted">266 <Text accessibilityRole="alert" className="mx-4 my-2 text-xs text-foreground-muted">
275 {props.error}267 {props.error}
276 </Text>268 </Text>
...@@ -284,7 +276,25 @@ export function FileTreeBrowser(props: {...@@ -284,7 +276,25 @@ export function FileTreeBrowser(props: {
284 }276 }
285 ListEmptyComponent={277 ListEmptyComponent={
286 <View className="px-4 py-5">278 <View className="px-4 py-5">
287 {props.isPending ? (279 {props.error && props.entries.length === 0 ? (
280 <>
281 <Text className="text-sm font-t3-bold text-foreground">Files unavailable</Text>
282 <Text
283 accessibilityRole="alert"
284 className="mt-1 text-xs leading-normal text-foreground-muted"
285 >
286 {props.error}
287 </Text>
288 <Pressable
289 accessibilityRole="button"
290 onPress={props.onRefresh}
291 disabled={props.isPending}
292 className="mt-3 min-h-11 self-start justify-center rounded-full bg-subtle px-4 active:opacity-70 disabled:opacity-50"
293 >
294 <Text className="text-sm font-t3-medium text-foreground">Try again</Text>
295 </Pressable>
296 </>
297 ) : props.isPending ? (
288 <ActivityIndicator size="small" />298 <ActivityIndicator size="small" />
289 ) : (299 ) : (
290 <>300 <>
apps/mobile/src/features/files/MaterialFilesHeader.tsx+18-5
...@@ -3,6 +3,7 @@ import { BackHandler, Keyboard, type TextInput, View } from "react-native";...@@ -3,6 +3,7 @@ import { BackHandler, Keyboard, type TextInput, View } from "react-native";
3import { useSafeAreaInsets } from "react-native-safe-area-context";3import { useSafeAreaInsets } from "react-native-safe-area-context";
44
5import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader";5import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader";
6import { AndroidAnchoredMenu } from "../../components/AndroidAnchoredMenu";
6import { MaterialSearchField } from "../../components/MaterialSearchField";7import { MaterialSearchField } from "../../components/MaterialSearchField";
78
8/** Keep Files search in the same header row on compact and expanded layouts. */9/** Keep Files search in the same header row on compact and expanded layouts. */
...@@ -55,12 +56,24 @@ export function MaterialFilesHeader(props: {...@@ -55,12 +56,24 @@ export function MaterialFilesHeader(props: {
55 icon: "magnifyingglass",56 icon: "magnifyingglass",
56 onPress: () => setSearchOpen(true),57 onPress: () => setSearchOpen(true),
57 },58 },
58 {
59 accessibilityLabel: "Refresh files",
60 icon: "arrow.clockwise",
61 onPress: props.onRefresh,
62 },
63 ]}59 ]}
60 trailing={
61 <AndroidAnchoredMenu
62 title="File options"
63 actions={[{ id: "refresh", title: "Refresh files" }]}
64 onPressAction={({ nativeEvent }) => {
65 if (nativeEvent.event === "refresh") props.onRefresh();
66 }}
67 >
68 {(open) => (
69 <AndroidHeaderIconButton
70 accessibilityLabel="File options"
71 icon="ellipsis"
72 onPress={open}
73 />
74 )}
75 </AndroidAnchoredMenu>
76 }
64 />77 />
65 </View>78 </View>
66 {searching ? (79 {searching ? (
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+11-2
...@@ -77,11 +77,20 @@ function FilesBrowserHeader(props: {...@@ -77,11 +77,20 @@ function FilesBrowserHeader(props: {
77 value: props.searchQuery,77 value: props.searchQuery,
78 onChangeText: props.onSearchQueryChange,78 onChangeText: props.onSearchQueryChange,
79 placeholder: "Search files",79 placeholder: "Search files",
80 onRefresh: props.onRefresh,
81 refreshAccessibilityLabel: "Refresh files",
82 closeAccessibilityLabel: "Close file search",80 closeAccessibilityLabel: "Close file search",
83 clearAccessibilityLabel: "Clear file search",81 clearAccessibilityLabel: "Clear file search",
84 }}82 }}
83 menus={
84 Platform.OS === "android"
85 ? [
86 {
87 title: "File options",
88 icon: "ellipsis",
89 items: [{ id: "refresh", title: "Refresh files", onPress: props.onRefresh }],
90 },
91 ]
92 : undefined
93 }
85 />94 />
86 );95 );
87}96}
apps/mobile/src/features/files/thread-file-navigator-pane.tsx+1-11
...@@ -55,16 +55,6 @@ export function ThreadFileNavigatorPane(props: {...@@ -55,16 +55,6 @@ export function ThreadFileNavigatorPane(props: {
55 const nativeHeaderRightBarButtonItems = useMemo(55 const nativeHeaderRightBarButtonItems = useMemo(
56 () =>56 () =>
57 [57 [
58 {
59 accessibilityLabel: "Refresh files",
60 icon: { name: "arrow.clockwise", type: "sfSymbol" as const },
61 identifier: "thread-file-navigator-refresh",
62 onPress: entriesQuery.refresh,
63 sharesBackground: false,
64 tintColor: foregroundColor,
65 type: "button" as const,
66 width: 44,
67 },
68 {58 {
69 accessibilityLabel: "Close files",59 accessibilityLabel: "Close files",
70 icon: { name: "xmark", type: "sfSymbol" as const },60 icon: { name: "xmark", type: "sfSymbol" as const },
...@@ -76,7 +66,7 @@ export function ThreadFileNavigatorPane(props: {...@@ -76,7 +66,7 @@ export function ThreadFileNavigatorPane(props: {
76 width: 44,66 width: 44,
77 },67 },
78 ] as ComponentProps<typeof ScreenStackHeaderConfig>["headerRightBarButtonItems"],68 ] as ComponentProps<typeof ScreenStackHeaderConfig>["headerRightBarButtonItems"],
79 [entriesQuery.refresh, foregroundColor, toggleAuxiliaryPane],69 [foregroundColor, toggleAuxiliaryPane],
80 );70 );
8171
82 const fileTree = (72 const fileTree = (
apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx+60-1
...@@ -1,16 +1,23 @@...@@ -1,16 +1,23 @@
1import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";1import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
2import { useNavigation } from "@react-navigation/native";2import { useNavigation } from "@react-navigation/native";
3import { useAtomValue } from "@effect/atom-react";
4import { managedRelaySessionAtom } from "@t3tools/client-runtime/relay";
3import type { EnvironmentId } from "@t3tools/contracts";5import type { EnvironmentId } from "@t3tools/contracts";
4import { useCallback, useState } from "react";6import { useCallback, useRef, useState } from "react";
7import { Platform, RefreshControl } from "react-native";
5import { useSafeAreaInsets } from "react-native-safe-area-context";8import { useSafeAreaInsets } from "react-native-safe-area-context";
69
7import { SettingsScreen } from "./components/SettingsScreen";10import { SettingsScreen } from "./components/SettingsScreen";
11import { AndroidAnchoredMenu } from "../../components/AndroidAnchoredMenu";
12import { AndroidHeaderIconButton } from "../../components/AndroidScreenHeader";
8import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows";13import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows";
9import { LocalEnvironmentList } from "../connection/LocalEnvironmentList";14import { LocalEnvironmentList } from "../connection/LocalEnvironmentList";
10import { GitHubRoutingSettings } from "../connection/GitHubRoutingSettings";15import { GitHubRoutingSettings } from "../connection/GitHubRoutingSettings";
11import { splitEnvironmentSections } from "../connection/environmentSections";16import { splitEnvironmentSections } from "../connection/environmentSections";
12import { useUniwindTheme } from "../../lib/useUniwindTheme";17import { useUniwindTheme } from "../../lib/useUniwindTheme";
13import { useRemoteConnections } from "../../state/use-remote-environment-registry";18import { useRemoteConnections } from "../../state/use-remote-environment-registry";
19import { relayEnvironmentDiscovery } from "../../state/relay";
20import { useAtomCommand } from "../../state/use-atom-command";
14import {21import {
15 applyShowcaseLocalEnvironmentDisplayUrls,22 applyShowcaseLocalEnvironmentDisplayUrls,
16 resolveShowcaseEnvironmentUpdateDisplayUrl,23 resolveShowcaseEnvironmentUpdateDisplayUrl,
...@@ -42,6 +49,24 @@ export function SettingsEnvironmentsRouteScreen() {...@@ -42,6 +49,24 @@ export function SettingsEnvironmentsRouteScreen() {
42 : environmentSections.connectedCloudEnvironments;49 : environmentSections.connectedCloudEnvironments;
43 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);50 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);
44 const headerIconColor = useUniwindTheme()["--color-icon"];51 const headerIconColor = useUniwindTheme()["--color-icon"];
52 const relaySession = useAtomValue(managedRelaySessionAtom);
53 const refreshRelayEnvironments = useAtomCommand(
54 relayEnvironmentDiscovery.refresh,
55 "relay environment refresh",
56 );
57 const [isRefreshingCloud, setIsRefreshingCloud] = useState(false);
58 const cloudRefreshPendingRef = useRef(false);
59 async function refreshCloudEnvironments() {
60 if (!relaySession || cloudRefreshPendingRef.current) return;
61 cloudRefreshPendingRef.current = true;
62 setIsRefreshingCloud(true);
63 try {
64 await refreshRelayEnvironments();
65 } finally {
66 cloudRefreshPendingRef.current = false;
67 setIsRefreshingCloud(false);
68 }
69 }
4570
46 const handleToggle = useCallback((environmentId: EnvironmentId) => {71 const handleToggle = useCallback((environmentId: EnvironmentId) => {
47 setExpandedId((prev) => (prev === environmentId ? null : environmentId));72 setExpandedId((prev) => (prev === environmentId ? null : environmentId));
...@@ -76,6 +101,31 @@ export function SettingsEnvironmentsRouteScreen() {...@@ -76,6 +101,31 @@ export function SettingsEnvironmentsRouteScreen() {
76 return (101 return (
77 <SettingsScreen102 <SettingsScreen
78 title="Environments"103 title="Environments"
104 trailing={
105 Platform.OS === "android" && relaySession ? (
106 <AndroidAnchoredMenu
107 title="Environment options"
108 actions={[
109 {
110 id: "refresh",
111 title: "Refresh cloud environments",
112 attributes: { disabled: isRefreshingCloud },
113 },
114 ]}
115 onPressAction={({ nativeEvent }) => {
116 if (nativeEvent.event === "refresh") void refreshCloudEnvironments();
117 }}
118 >
119 {(open) => (
120 <AndroidHeaderIconButton
121 accessibilityLabel="Environment options"
122 icon="ellipsis"
123 onPress={open}
124 />
125 )}
126 </AndroidAnchoredMenu>
127 ) : undefined
128 }
79 actions={[129 actions={[
80 {130 {
81 accessibilityLabel: "Add environment",131 accessibilityLabel: "Add environment",
...@@ -90,6 +140,7 @@ export function SettingsEnvironmentsRouteScreen() {...@@ -90,6 +140,7 @@ export function SettingsEnvironmentsRouteScreen() {
90 ]}140 ]}
91 >141 >
92 <ScrollView142 <ScrollView
143 alwaysBounceVertical
93 contentInsetAdjustmentBehavior="automatic"144 contentInsetAdjustmentBehavior="automatic"
94 showsVerticalScrollIndicator={false}145 showsVerticalScrollIndicator={false}
95 className="flex-1"146 className="flex-1"
...@@ -97,6 +148,14 @@ export function SettingsEnvironmentsRouteScreen() {...@@ -97,6 +148,14 @@ export function SettingsEnvironmentsRouteScreen() {
97 contentContainerStyle={{148 contentContainerStyle={{
98 paddingBottom: Math.max(insets.bottom, 18) + 18,149 paddingBottom: Math.max(insets.bottom, 18) + 18,
99 }}150 }}
151 refreshControl={
152 relaySession ? (
153 <RefreshControl
154 refreshing={isRefreshingCloud}
155 onRefresh={() => void refreshCloudEnvironments()}
156 />
157 ) : undefined
158 }
100 >159 >
101 <LocalEnvironmentList160 <LocalEnvironmentList
102 environments={localEnvironments}161 environments={localEnvironments}
apps/mobile/src/features/threads/git/GitOverviewSheet.tsx+41-17
...@@ -26,6 +26,7 @@ import {...@@ -26,6 +26,7 @@ import {
26 AndroidHeaderIconButton,26 AndroidHeaderIconButton,
27 AndroidSheetHeader,27 AndroidSheetHeader,
28} from "../../../components/AndroidScreenHeader";28} from "../../../components/AndroidScreenHeader";
29import { AndroidAnchoredMenu } from "../../../components/AndroidAnchoredMenu";
29import { MaterialScreenContent } from "../../../components/MaterialScreenContent";30import { MaterialScreenContent } from "../../../components/MaterialScreenContent";
30import { useAdaptiveWorkspaceLayout } from "../../layout/AdaptiveWorkspaceLayout";31import { useAdaptiveWorkspaceLayout } from "../../layout/AdaptiveWorkspaceLayout";
31import { AppText as Text } from "../../../components/AppText";32import { AppText as Text } from "../../../components/AppText";
...@@ -232,6 +233,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {...@@ -232,6 +233,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {
232233
233 const content = (234 const content = (
234 <ScrollView235 <ScrollView
236 alwaysBounceVertical
235 className={Platform.OS === "android" ? "flex-1 bg-sheet-solid" : "flex-1 bg-screen"}237 className={Platform.OS === "android" ? "flex-1 bg-sheet-solid" : "flex-1 bg-screen"}
236 contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"}238 contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"}
237 showsVerticalScrollIndicator={false}239 showsVerticalScrollIndicator={false}
...@@ -400,7 +402,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {...@@ -400,7 +402,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {
400 // stack header, so — like the Settings sheet — the header must come from a402 // stack header, so — like the Settings sheet — the header must come from a
401 // nested native stack INSIDE the sheet. This reuses the exact structure of the403 // nested native stack INSIDE the sheet. This reuses the exact structure of the
402 // inspector branch below: branch as the title, status summary as the native404 // inspector branch below: branch as the title, status summary as the native
403 // subtitle, refresh as a header button.405 // subtitle, and content that owns pull-to-refresh.
404 return (406 return (
405 <View collapsable={false} className="flex-1 bg-sheet">407 <View collapsable={false} className="flex-1 bg-sheet">
406 <ScreenStack style={{ flex: 1 }}>408 <ScreenStack style={{ flex: 1 }}>
...@@ -431,6 +433,30 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {...@@ -431,6 +433,30 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {
431 );433 );
432 }434 }
433435
436 const refreshMenu = (
437 <AndroidAnchoredMenu
438 title="Repository options"
439 actions={[
440 {
441 id: "refresh",
442 title: "Refresh repository status",
443 attributes: { disabled: busy || isPullRefreshing },
444 },
445 ]}
446 onPressAction={({ nativeEvent }) => {
447 if (nativeEvent.event === "refresh") void handlePullRefresh();
448 }}
449 >
450 {(open) => (
451 <AndroidHeaderIconButton
452 accessibilityLabel="Repository options"
453 icon="ellipsis"
454 onPress={open}
455 />
456 )}
457 </AndroidAnchoredMenu>
458 );
459
434 return (460 return (
435 <View461 <View
436 collapsable={false}462 collapsable={false}
...@@ -465,14 +491,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {...@@ -465,14 +491,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {
465 }491 }
466 >492 >
467 {Platform.OS === "android" ? (493 {Platform.OS === "android" ? (
468 <View className="absolute right-3 top-4 z-[1]">494 <View className="absolute right-3 top-4 z-[1]">{refreshMenu}</View>
469 <AndroidHeaderIconButton
470 accessibilityLabel="Refresh repository status"
471 disabled={busy}
472 icon="arrow.clockwise"
473 onPress={() => void gitActions.refreshSelectedThreadGitStatus()}
474 />
475 </View>
476 ) : (495 ) : (
477 <Pressable496 <Pressable
478 className={497 className={
...@@ -506,14 +525,19 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {...@@ -506,14 +525,19 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {
506 hideBottomBorder={Platform.OS === "android"}525 hideBottomBorder={Platform.OS === "android"}
507 subtitle={currentStatusSummary}526 subtitle={currentStatusSummary}
508 onBack={() => navigation.goBack()}527 onBack={() => navigation.goBack()}
509 actions={[528 trailing={Platform.OS === "android" ? refreshMenu : undefined}
510 {529 actions={
511 accessibilityLabel: "Refresh repository status",530 Platform.OS === "android"
512 disabled: busy,531 ? undefined
513 icon: "arrow.clockwise",532 : [
514 onPress: () => void gitActions.refreshSelectedThreadGitStatus(),533 {
515 },534 accessibilityLabel: "Refresh repository status",
516 ]}535 disabled: busy,
536 icon: "arrow.clockwise",
537 onPress: () => void gitActions.refreshSelectedThreadGitStatus(),
538 },
539 ]
540 }
517 />541 />
518 )}542 )}
519543