diff --git a/apps/mobile/src/components/ScreenHeader.android.tsx b/apps/mobile/src/components/ScreenHeader.android.tsx index edb0b770e93c..f7b70e695ad9 100644 --- a/apps/mobile/src/components/ScreenHeader.android.tsx +++ b/apps/mobile/src/components/ScreenHeader.android.tsx @@ -138,7 +138,7 @@ export function ScreenHeader(props: ScreenHeaderProps) { icon: "magnifyingglass", onPress: () => setSearchOpen(true), }, - ...(search.onRefresh + ...(search.refreshInToolbar && search.onRefresh ? [ { accessibilityLabel: search.refreshAccessibilityLabel ?? "Refresh", diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index a191a9c7175b..ea150180393c 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -14,6 +14,7 @@ import { SymbolView } from "../../components/AppSymbol"; import { useCallback, useMemo, useRef, type ComponentProps } from "react"; import { ActivityIndicator, + Platform, Pressable, RefreshControl, useWindowDimensions, @@ -63,9 +64,6 @@ function ArchivedThreadsHeader(props: { compactPlaceholder: "Search", mode: "inline", compactToolbar: width < 700, - onRefresh: props.onRefresh, - refreshInToolbar: true, - refreshAccessibilityLabel: "Refresh archived threads", }} menus={[ { @@ -110,6 +108,15 @@ function ArchivedThreadsHeader(props: { }, ], }, + ...(Platform.OS === "android" + ? [ + { + id: "refresh", + title: "Refresh archived threads", + onPress: props.onRefresh, + }, + ] + : []), ], }, ]} diff --git a/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx b/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx index cccc6425ec13..2828392ba8f3 100644 --- a/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx +++ b/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx @@ -41,16 +41,14 @@ interface CloudEnvironmentRowsProps { readonly showcaseAvailableEnvironments?: ReadonlyArray; readonly showcaseSignedIn?: boolean; /** - * Hide the "T3 Connect" section title + refresh button for hosts that - * provide their own chrome (the onboarding sheet's native header and - * pull-to-refresh). + * Hide the "T3 Connect" section title when the host provides its own header. */ readonly showHeader?: boolean; } /** * "T3 Connect" section: every environment published to the signed-in account, - * with connect switches, availability status, refresh, and loading/error + * with connect switches, availability status, and loading/error * states. Shared between the Settings environments screen and the T3 Connect * onboarding sheet. * @@ -120,29 +118,8 @@ function CloudEnvironmentRowsContent( return ( {showHeader ? ( - + T3 Connect - {discoveryAvailable ? ( - { - void controller.refreshRelayEnvironments(); - }} - className="h-9 w-9 items-center justify-center rounded-full bg-subtle active:opacity-70 disabled:opacity-50" - > - {controller.relayDiscovery.isRefreshing ? ( - - ) : ( - - )} - - ) : null} ) : null} diff --git a/apps/mobile/src/features/files/FileTreeBrowser.tsx b/apps/mobile/src/features/files/FileTreeBrowser.tsx index f2dfd3f15a97..df18347f336c 100644 --- a/apps/mobile/src/features/files/FileTreeBrowser.tsx +++ b/apps/mobile/src/features/files/FileTreeBrowser.tsx @@ -234,15 +234,6 @@ export function FileTreeBrowser(props: { ], ); - if (props.error && props.entries.length === 0) { - return ( - - Files unavailable - {props.error} - - ); - } - // SPIKE: render the FlatList as the screen's DIRECT content (no wrapping View), and // mirror the Home ScrollView exactly — `contentInsetAdjustmentBehavior: "automatic"` // with NO manual contentInset. iOS only applies the nav-bar top inset + scroll-edge @@ -250,6 +241,7 @@ export function FileTreeBrowser(props: { // flex-1 Views is ignored, which is why the tree rendered under the header with no blur. return ( item.node.path} @@ -270,7 +262,7 @@ export function FileTreeBrowser(props: { renderItem={renderItem} ListHeaderComponent={ <> - {props.error ? ( + {props.error && props.entries.length > 0 ? ( {props.error} @@ -284,7 +276,25 @@ export function FileTreeBrowser(props: { } ListEmptyComponent={ - {props.isPending ? ( + {props.error && props.entries.length === 0 ? ( + <> + Files unavailable + + {props.error} + + + Try again + + + ) : props.isPending ? ( ) : ( <> diff --git a/apps/mobile/src/features/files/MaterialFilesHeader.tsx b/apps/mobile/src/features/files/MaterialFilesHeader.tsx index 8936ae9b37ed..61d007ea5c3e 100644 --- a/apps/mobile/src/features/files/MaterialFilesHeader.tsx +++ b/apps/mobile/src/features/files/MaterialFilesHeader.tsx @@ -3,6 +3,7 @@ import { BackHandler, Keyboard, type TextInput, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; +import { AndroidAnchoredMenu } from "../../components/AndroidAnchoredMenu"; import { MaterialSearchField } from "../../components/MaterialSearchField"; /** Keep Files search in the same header row on compact and expanded layouts. */ @@ -55,12 +56,24 @@ export function MaterialFilesHeader(props: { icon: "magnifyingglass", onPress: () => setSearchOpen(true), }, - { - accessibilityLabel: "Refresh files", - icon: "arrow.clockwise", - onPress: props.onRefresh, - }, ]} + trailing={ + { + if (nativeEvent.event === "refresh") props.onRefresh(); + }} + > + {(open) => ( + + )} + + } /> {searching ? ( diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 9e89ea1b24e4..06ef8f6ba4d9 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -77,11 +77,20 @@ function FilesBrowserHeader(props: { value: props.searchQuery, onChangeText: props.onSearchQueryChange, placeholder: "Search files", - onRefresh: props.onRefresh, - refreshAccessibilityLabel: "Refresh files", closeAccessibilityLabel: "Close file search", clearAccessibilityLabel: "Clear file search", }} + menus={ + Platform.OS === "android" + ? [ + { + title: "File options", + icon: "ellipsis", + items: [{ id: "refresh", title: "Refresh files", onPress: props.onRefresh }], + }, + ] + : undefined + } /> ); } diff --git a/apps/mobile/src/features/files/thread-file-navigator-pane.tsx b/apps/mobile/src/features/files/thread-file-navigator-pane.tsx index f62ba6b7abba..c3c906e3d515 100644 --- a/apps/mobile/src/features/files/thread-file-navigator-pane.tsx +++ b/apps/mobile/src/features/files/thread-file-navigator-pane.tsx @@ -55,16 +55,6 @@ export function ThreadFileNavigatorPane(props: { const nativeHeaderRightBarButtonItems = useMemo( () => [ - { - accessibilityLabel: "Refresh files", - icon: { name: "arrow.clockwise", type: "sfSymbol" as const }, - identifier: "thread-file-navigator-refresh", - onPress: entriesQuery.refresh, - sharesBackground: false, - tintColor: foregroundColor, - type: "button" as const, - width: 44, - }, { accessibilityLabel: "Close files", icon: { name: "xmark", type: "sfSymbol" as const }, @@ -76,7 +66,7 @@ export function ThreadFileNavigatorPane(props: { width: 44, }, ] as ComponentProps["headerRightBarButtonItems"], - [entriesQuery.refresh, foregroundColor, toggleAuxiliaryPane], + [foregroundColor, toggleAuxiliaryPane], ); const fileTree = ( diff --git a/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx index ce6077db6163..7ca791afec6d 100644 --- a/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx @@ -1,16 +1,23 @@ import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; import { useNavigation } from "@react-navigation/native"; +import { useAtomValue } from "@effect/atom-react"; +import { managedRelaySessionAtom } from "@t3tools/client-runtime/relay"; import type { EnvironmentId } from "@t3tools/contracts"; -import { useCallback, useState } from "react"; +import { useCallback, useRef, useState } from "react"; +import { Platform, RefreshControl } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { SettingsScreen } from "./components/SettingsScreen"; +import { AndroidAnchoredMenu } from "../../components/AndroidAnchoredMenu"; +import { AndroidHeaderIconButton } from "../../components/AndroidScreenHeader"; import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows"; import { LocalEnvironmentList } from "../connection/LocalEnvironmentList"; import { GitHubRoutingSettings } from "../connection/GitHubRoutingSettings"; import { splitEnvironmentSections } from "../connection/environmentSections"; import { useUniwindTheme } from "../../lib/useUniwindTheme"; import { useRemoteConnections } from "../../state/use-remote-environment-registry"; +import { relayEnvironmentDiscovery } from "../../state/relay"; +import { useAtomCommand } from "../../state/use-atom-command"; import { applyShowcaseLocalEnvironmentDisplayUrls, resolveShowcaseEnvironmentUpdateDisplayUrl, @@ -42,6 +49,24 @@ export function SettingsEnvironmentsRouteScreen() { : environmentSections.connectedCloudEnvironments; const [expandedId, setExpandedId] = useState(null); const headerIconColor = useUniwindTheme()["--color-icon"]; + const relaySession = useAtomValue(managedRelaySessionAtom); + const refreshRelayEnvironments = useAtomCommand( + relayEnvironmentDiscovery.refresh, + "relay environment refresh", + ); + const [isRefreshingCloud, setIsRefreshingCloud] = useState(false); + const cloudRefreshPendingRef = useRef(false); + async function refreshCloudEnvironments() { + if (!relaySession || cloudRefreshPendingRef.current) return; + cloudRefreshPendingRef.current = true; + setIsRefreshingCloud(true); + try { + await refreshRelayEnvironments(); + } finally { + cloudRefreshPendingRef.current = false; + setIsRefreshingCloud(false); + } + } const handleToggle = useCallback((environmentId: EnvironmentId) => { setExpandedId((prev) => (prev === environmentId ? null : environmentId)); @@ -76,6 +101,31 @@ export function SettingsEnvironmentsRouteScreen() { return ( { + if (nativeEvent.event === "refresh") void refreshCloudEnvironments(); + }} + > + {(open) => ( + + )} + + ) : undefined + } actions={[ { accessibilityLabel: "Add environment", @@ -90,6 +140,7 @@ export function SettingsEnvironmentsRouteScreen() { ]} > void refreshCloudEnvironments()} + /> + ) : undefined + } > @@ -431,6 +433,30 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) { ); } + const refreshMenu = ( + { + if (nativeEvent.event === "refresh") void handlePullRefresh(); + }} + > + {(open) => ( + + )} + + ); + return ( {Platform.OS === "android" ? ( - - void gitActions.refreshSelectedThreadGitStatus()} - /> - + {refreshMenu} ) : ( navigation.goBack()} - actions={[ - { - accessibilityLabel: "Refresh repository status", - disabled: busy, - icon: "arrow.clockwise", - onPress: () => void gitActions.refreshSelectedThreadGitStatus(), - }, - ]} + trailing={Platform.OS === "android" ? refreshMenu : undefined} + actions={ + Platform.OS === "android" + ? undefined + : [ + { + accessibilityLabel: "Refresh repository status", + disabled: busy, + icon: "arrow.clockwise", + onPress: () => void gitActions.refreshSelectedThreadGitStatus(), + }, + ] + } /> )}