| 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,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.onRefresh | 141 | ...(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"; |
| 14 | import { useCallback, useMemo, useRef, type ComponentProps } from "react"; | 14 | import { useCallback, useMemo, useRef, type ComponentProps } from "react"; |
| 15 | import { | 15 | import { |
| 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 that | 44 | * 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 | } |
| 50 | 48 | ||
| 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/error | 51 | * with connect switches, availability status, and loading/error |
| 54 | * states. Shared between the Settings environments screen and the T3 Connect | 52 | * 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} |
| 148 | 125 |
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 | ); |
| 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 | // SPIKE: render the FlatList as the screen's DIRECT content (no wrapping View), and | 237 | // 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-edge | 239 | // 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 | <FlatList | 243 | <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"; |
| 3 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | 3 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 4 | 4 | ||
| 5 | import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; | 5 | import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 6 | import { AndroidAnchoredMenu } from "../../components/AndroidAnchoredMenu"; | ||
| 6 | import { MaterialSearchField } from "../../components/MaterialSearchField"; | 7 | import { MaterialSearchField } from "../../components/MaterialSearchField"; |
| 7 | 8 | ||
| 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 | ); |
| 81 | 71 | ||
| 82 | const fileTree = ( | 72 | const fileTree = ( |
apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx+60-1| ... | @@ -1,16 +1,23 @@ | ... | @@ -1,16 +1,23 @@ |
| 1 | import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; | 1 | import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; |
| 2 | import { useNavigation } from "@react-navigation/native"; | 2 | import { useNavigation } from "@react-navigation/native"; |
| 3 | import { useAtomValue } from "@effect/atom-react"; | ||
| 4 | import { managedRelaySessionAtom } from "@t3tools/client-runtime/relay"; | ||
| 3 | import type { EnvironmentId } from "@t3tools/contracts"; | 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 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | 8 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 6 | 9 | ||
| 7 | import { SettingsScreen } from "./components/SettingsScreen"; | 10 | import { SettingsScreen } from "./components/SettingsScreen"; |
| 11 | import { AndroidAnchoredMenu } from "../../components/AndroidAnchoredMenu"; | ||
| 12 | import { AndroidHeaderIconButton } from "../../components/AndroidScreenHeader"; | ||
| 8 | import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows"; | 13 | import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows"; |
| 9 | import { LocalEnvironmentList } from "../connection/LocalEnvironmentList"; | 14 | import { LocalEnvironmentList } from "../connection/LocalEnvironmentList"; |
| 10 | import { GitHubRoutingSettings } from "../connection/GitHubRoutingSettings"; | 15 | import { GitHubRoutingSettings } from "../connection/GitHubRoutingSettings"; |
| 11 | import { splitEnvironmentSections } from "../connection/environmentSections"; | 16 | import { splitEnvironmentSections } from "../connection/environmentSections"; |
| 12 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | 17 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; |
| 13 | import { useRemoteConnections } from "../../state/use-remote-environment-registry"; | 18 | import { useRemoteConnections } from "../../state/use-remote-environment-registry"; |
| 19 | import { relayEnvironmentDiscovery } from "../../state/relay"; | ||
| 20 | import { useAtomCommand } from "../../state/use-atom-command"; | ||
| 14 | import { | 21 | import { |
| 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 | } | ||
| 45 | 70 | ||
| 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 | <SettingsScreen | 102 | <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 | <ScrollView | 142 | <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 | <LocalEnvironmentList | 160 | <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"; |
| 29 | import { AndroidAnchoredMenu } from "../../../components/AndroidAnchoredMenu"; | ||
| 29 | import { MaterialScreenContent } from "../../../components/MaterialScreenContent"; | 30 | import { MaterialScreenContent } from "../../../components/MaterialScreenContent"; |
| 30 | import { useAdaptiveWorkspaceLayout } from "../../layout/AdaptiveWorkspaceLayout"; | 31 | import { useAdaptiveWorkspaceLayout } from "../../layout/AdaptiveWorkspaceLayout"; |
| 31 | import { AppText as Text } from "../../../components/AppText"; | 32 | import { AppText as Text } from "../../../components/AppText"; |
| ... | @@ -232,6 +233,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) { | ... | @@ -232,6 +233,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) { |
| 232 | 233 | ||
| 233 | const content = ( | 234 | const content = ( |
| 234 | <ScrollView | 235 | <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 a | 402 | // 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 the | 403 | // 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 native | 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 | 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 | } |
| 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 | return ( | 460 | return ( |
| 435 | <View | 461 | <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 | <Pressable | 496 | <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 | )} |
| 519 | 543 |