| author | |
| committer | |
| log | 8c3b5bef17a637bf53f297189350359790ef7afb |
| tree | e331bfdec7c792784270861b839672439ffca643 |
| parent | d99bc5a4d707df2506e60cbb411b8d30022bbf89 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 138 | icon: "magnifyingglass", |
| 139 | 139 | onPress: () => setSearchOpen(true), |
| 140 | 140 | }, |
| 141 | ...(search.onRefresh | |
| 141 | ...(search.refreshInToolbar && search.onRefresh | |
| 142 | 142 | ? [ |
| 143 | 143 | { |
| 144 | 144 | accessibilityLabel: search.refreshAccessibilityLabel ?? "Refresh", |
apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx+10-3| ... | ... | @@ -14,6 +14,7 @@ import { SymbolView } from "../../components/AppSymbol"; |
| 14 | 14 | import { useCallback, useMemo, useRef, type ComponentProps } from "react"; |
| 15 | 15 | import { |
| 16 | 16 | ActivityIndicator, |
| 17 | Platform, | |
| 17 | 18 | Pressable, |
| 18 | 19 | RefreshControl, |
| 19 | 20 | useWindowDimensions, |
| ... | ... | @@ -63,9 +64,6 @@ function ArchivedThreadsHeader(props: { |
| 63 | 64 | compactPlaceholder: "Search", |
| 64 | 65 | mode: "inline", |
| 65 | 66 | compactToolbar: width < 700, |
| 66 | onRefresh: props.onRefresh, | |
| 67 | refreshInToolbar: true, | |
| 68 | refreshAccessibilityLabel: "Refresh archived threads", | |
| 69 | 67 | }} |
| 70 | 68 | menus={[ |
| 71 | 69 | { |
| ... | ... | @@ -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 | 41 | readonly showcaseAvailableEnvironments?: ReadonlyArray<RelayEnvironmentView>; |
| 42 | 42 | readonly showcaseSignedIn?: boolean; |
| 43 | 43 | /** |
| 44 | * Hide the "T3 Connect" section title + refresh button for hosts that | |
| 45 | * provide their own chrome (the onboarding sheet's native header and | |
| 46 | * pull-to-refresh). | |
| 44 | * Hide the "T3 Connect" section title when the host provides its own header. | |
| 47 | 45 | */ |
| 48 | 46 | readonly showHeader?: boolean; |
| 49 | 47 | } |
| 50 | 48 | |
| 51 | 49 | /** |
| 52 | 50 | * "T3 Connect" section: every environment published to the signed-in account, |
| 53 | * with connect switches, availability status, refresh, and loading/error | |
| 51 | * with connect switches, availability status, and loading/error | |
| 54 | 52 | * states. Shared between the Settings environments screen and the T3 Connect |
| 55 | 53 | * onboarding sheet. |
| 56 | 54 | * |
| ... | ... | @@ -120,29 +118,8 @@ function CloudEnvironmentRowsContent( |
| 120 | 118 | return ( |
| 121 | 119 | <View collapsable={false} className={cn("gap-3", showHeader && "mt-5")}> |
| 122 | 120 | {showHeader ? ( |
| 123 | <View className="flex-row items-center justify-between px-1"> | |
| 121 | <View className="px-1"> | |
| 124 | 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 | 123 | </View> |
| 147 | 124 | ) : null} |
| 148 | 125 |
apps/mobile/src/features/files/FileTreeBrowser.tsx+21-11| ... | ... | @@ -234,15 +234,6 @@ export function FileTreeBrowser(props: { |
| 234 | 234 | ], |
| 235 | 235 | ); |
| 236 | 236 | |
| 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 | 237 | // SPIKE: render the FlatList as the screen's DIRECT content (no wrapping View), and |
| 247 | 238 | // mirror the Home ScrollView exactly — `contentInsetAdjustmentBehavior: "automatic"` |
| 248 | 239 | // with NO manual contentInset. iOS only applies the nav-bar top inset + scroll-edge |
| ... | ... | @@ -250,6 +241,7 @@ export function FileTreeBrowser(props: { |
| 250 | 241 | // flex-1 Views is ignored, which is why the tree rendered under the header with no blur. |
| 251 | 242 | return ( |
| 252 | 243 | <FlatList |
| 244 | alwaysBounceVertical | |
| 253 | 245 | className="flex-1" |
| 254 | 246 | data={visibleNodes} |
| 255 | 247 | keyExtractor={(item) => item.node.path} |
| ... | ... | @@ -270,7 +262,7 @@ export function FileTreeBrowser(props: { |
| 270 | 262 | renderItem={renderItem} |
| 271 | 263 | ListHeaderComponent={ |
| 272 | 264 | <> |
| 273 | {props.error ? ( | |
| 265 | {props.error && props.entries.length > 0 ? ( | |
| 274 | 266 | <Text accessibilityRole="alert" className="mx-4 my-2 text-xs text-foreground-muted"> |
| 275 | 267 | {props.error} |
| 276 | 268 | </Text> |
| ... | ... | @@ -284,7 +276,25 @@ export function FileTreeBrowser(props: { |
| 284 | 276 | } |
| 285 | 277 | ListEmptyComponent={ |
| 286 | 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 | 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 | 3 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 4 | 4 | |
| 5 | 5 | import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 6 | import { AndroidAnchoredMenu } from "../../components/AndroidAnchoredMenu"; | |
| 6 | 7 | import { MaterialSearchField } from "../../components/MaterialSearchField"; |
| 7 | 8 | |
| 8 | 9 | /** Keep Files search in the same header row on compact and expanded layouts. */ |
| ... | ... | @@ -55,12 +56,24 @@ export function MaterialFilesHeader(props: { |
| 55 | 56 | icon: "magnifyingglass", |
| 56 | 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 | 78 | </View> |
| 66 | 79 | {searching ? ( |
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+11-2| ... | ... | @@ -77,11 +77,20 @@ function FilesBrowserHeader(props: { |
| 77 | 77 | value: props.searchQuery, |
| 78 | 78 | onChangeText: props.onSearchQueryChange, |
| 79 | 79 | placeholder: "Search files", |
| 80 | onRefresh: props.onRefresh, | |
| 81 | refreshAccessibilityLabel: "Refresh files", | |
| 82 | 80 | closeAccessibilityLabel: "Close file search", |
| 83 | 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 | 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 | 59 | accessibilityLabel: "Close files", |
| 70 | 60 | icon: { name: "xmark", type: "sfSymbol" as const }, |
| ... | ... | @@ -76,7 +66,7 @@ export function ThreadFileNavigatorPane(props: { |
| 76 | 66 | width: 44, |
| 77 | 67 | }, |
| 78 | 68 | ] as ComponentProps<typeof ScreenStackHeaderConfig>["headerRightBarButtonItems"], |
| 79 | [entriesQuery.refresh, foregroundColor, toggleAuxiliaryPane], | |
| 69 | [foregroundColor, toggleAuxiliaryPane], | |
| 80 | 70 | ); |
| 81 | 71 | |
| 82 | 72 | const fileTree = ( |
apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx+60-1| ... | ... | @@ -1,16 +1,23 @@ |
| 1 | 1 | import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; |
| 2 | 2 | import { useNavigation } from "@react-navigation/native"; |
| 3 | import { useAtomValue } from "@effect/atom-react"; | |
| 4 | import { managedRelaySessionAtom } from "@t3tools/client-runtime/relay"; | |
| 3 | 5 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 4 | import { useCallback, useState } from "react"; | |
| 6 | import { useCallback, useRef, useState } from "react"; | |
| 7 | import { Platform, RefreshControl } from "react-native"; | |
| 5 | 8 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 6 | 9 | |
| 7 | 10 | import { SettingsScreen } from "./components/SettingsScreen"; |
| 11 | import { AndroidAnchoredMenu } from "../../components/AndroidAnchoredMenu"; | |
| 12 | import { AndroidHeaderIconButton } from "../../components/AndroidScreenHeader"; | |
| 8 | 13 | import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows"; |
| 9 | 14 | import { LocalEnvironmentList } from "../connection/LocalEnvironmentList"; |
| 10 | 15 | import { GitHubRoutingSettings } from "../connection/GitHubRoutingSettings"; |
| 11 | 16 | import { splitEnvironmentSections } from "../connection/environmentSections"; |
| 12 | 17 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; |
| 13 | 18 | import { useRemoteConnections } from "../../state/use-remote-environment-registry"; |
| 19 | import { relayEnvironmentDiscovery } from "../../state/relay"; | |
| 20 | import { useAtomCommand } from "../../state/use-atom-command"; | |
| 14 | 21 | import { |
| 15 | 22 | applyShowcaseLocalEnvironmentDisplayUrls, |
| 16 | 23 | resolveShowcaseEnvironmentUpdateDisplayUrl, |
| ... | ... | @@ -42,6 +49,24 @@ export function SettingsEnvironmentsRouteScreen() { |
| 42 | 49 | : environmentSections.connectedCloudEnvironments; |
| 43 | 50 | const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null); |
| 44 | 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 | } | |
| 45 | 70 | |
| 46 | 71 | const handleToggle = useCallback((environmentId: EnvironmentId) => { |
| 47 | 72 | setExpandedId((prev) => (prev === environmentId ? null : environmentId)); |
| ... | ... | @@ -76,6 +101,31 @@ export function SettingsEnvironmentsRouteScreen() { |
| 76 | 101 | return ( |
| 77 | 102 | <SettingsScreen |
| 78 | 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 | 129 | actions={[ |
| 80 | 130 | { |
| 81 | 131 | accessibilityLabel: "Add environment", |
| ... | ... | @@ -90,6 +140,7 @@ export function SettingsEnvironmentsRouteScreen() { |
| 90 | 140 | ]} |
| 91 | 141 | > |
| 92 | 142 | <ScrollView |
| 143 | alwaysBounceVertical | |
| 93 | 144 | contentInsetAdjustmentBehavior="automatic" |
| 94 | 145 | showsVerticalScrollIndicator={false} |
| 95 | 146 | className="flex-1" |
| ... | ... | @@ -97,6 +148,14 @@ export function SettingsEnvironmentsRouteScreen() { |
| 97 | 148 | contentContainerStyle={{ |
| 98 | 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 | 160 | <LocalEnvironmentList |
| 102 | 161 | environments={localEnvironments} |
apps/mobile/src/features/threads/git/GitOverviewSheet.tsx+41-17| ... | ... | @@ -26,6 +26,7 @@ import { |
| 26 | 26 | AndroidHeaderIconButton, |
| 27 | 27 | AndroidSheetHeader, |
| 28 | 28 | } from "../../../components/AndroidScreenHeader"; |
| 29 | import { AndroidAnchoredMenu } from "../../../components/AndroidAnchoredMenu"; | |
| 29 | 30 | import { MaterialScreenContent } from "../../../components/MaterialScreenContent"; |
| 30 | 31 | import { useAdaptiveWorkspaceLayout } from "../../layout/AdaptiveWorkspaceLayout"; |
| 31 | 32 | import { AppText as Text } from "../../../components/AppText"; |
| ... | ... | @@ -232,6 +233,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) { |
| 232 | 233 | |
| 233 | 234 | const content = ( |
| 234 | 235 | <ScrollView |
| 236 | alwaysBounceVertical | |
| 235 | 237 | className={Platform.OS === "android" ? "flex-1 bg-sheet-solid" : "flex-1 bg-screen"} |
| 236 | 238 | contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"} |
| 237 | 239 | showsVerticalScrollIndicator={false} |
| ... | ... | @@ -400,7 +402,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) { |
| 400 | 402 | // stack header, so — like the Settings sheet — the header must come from a |
| 401 | 403 | // nested native stack INSIDE the sheet. This reuses the exact structure of the |
| 402 | 404 | // 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 | 406 | return ( |
| 405 | 407 | <View collapsable={false} className="flex-1 bg-sheet"> |
| 406 | 408 | <ScreenStack style={{ flex: 1 }}> |
| ... | ... | @@ -431,6 +433,30 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) { |
| 431 | 433 | ); |
| 432 | 434 | } |
| 433 | 435 | |
| 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 | 460 | return ( |
| 435 | 461 | <View |
| 436 | 462 | collapsable={false} |
| ... | ... | @@ -465,14 +491,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) { |
| 465 | 491 | } |
| 466 | 492 | > |
| 467 | 493 | {Platform.OS === "android" ? ( |
| 468 | <View className="absolute right-3 top-4 z-[1]"> | |
| 469 | <AndroidHeaderIconButton | |
| 470 | accessibilityLabel="Refresh repository status" | |
| 471 | disabled={busy} | |
| 472 | icon="arrow.clockwise" | |
| 473 | onPress={() => void gitActions.refreshSelectedThreadGitStatus()} | |
| 474 | /> | |
| 475 | </View> | |
| 494 | <View className="absolute right-3 top-4 z-[1]">{refreshMenu}</View> | |
| 476 | 495 | ) : ( |
| 477 | 496 | <Pressable |
| 478 | 497 | className={ |
| ... | ... | @@ -506,14 +525,19 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) { |
| 506 | 525 | hideBottomBorder={Platform.OS === "android"} |
| 507 | 526 | subtitle={currentStatusSummary} |
| 508 | 527 | onBack={() => navigation.goBack()} |
| 509 | actions={[ | |
| 510 | { | |
| 511 | accessibilityLabel: "Refresh repository status", | |
| 512 | disabled: busy, | |
| 513 | icon: "arrow.clockwise", | |
| 514 | onPress: () => void gitActions.refreshSelectedThreadGitStatus(), | |
| 515 | }, | |
| 516 | ]} | |
| 528 | trailing={Platform.OS === "android" ? refreshMenu : undefined} | |
| 529 | actions={ | |
| 530 | Platform.OS === "android" | |
| 531 | ? undefined | |
| 532 | : [ | |
| 533 | { | |
| 534 | accessibilityLabel: "Refresh repository status", | |
| 535 | disabled: busy, | |
| 536 | icon: "arrow.clockwise", | |
| 537 | onPress: () => void gitActions.refreshSelectedThreadGitStatus(), | |
| 538 | }, | |
| 539 | ] | |
| 540 | } | |
| 517 | 541 | /> |
| 518 | 542 | )} |
| 519 | 543 |