Skip to content

Commit 6f0f1a9

Browse files
authored
feat(mobile): split wide Android Settings into two panes (#24298)
1 parent b84e121 commit 6f0f1a9

15 files changed

Lines changed: 206 additions & 19 deletions

‎apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,13 @@ import {
9595
currentWorkspaceSelections,
9696
workspaceSelectionCheck,
9797
} from "../workspace/workspace-selection-store";
98-
import { workspaceTabNavigationState } from "../workspace/workspace-tab-navigation";
98+
import {
99+
workspaceTabColumns,
100+
workspaceTabNavigationState,
101+
} from "../workspace/workspace-tab-navigation";
99102
import type { WorkspaceTab } from "../workspace/workspace-tabs-state";
100103
import { workspaceRoutesOf } from "./workspace-overlay-routes";
104+
import { SettingsWorkspaceLayout } from "../settings/SettingsWorkspaceLayout";
101105

102106
export { useAdaptiveWorkspaceLayout };
103107

@@ -478,7 +482,7 @@ function AdaptiveWorkspaceLayoutContent(
478482
tab,
479483
currentWorkspaceSelections(),
480484
{
481-
columns: Platform.OS === "ios" && (options?.columns ?? layout.usesSplitView),
485+
columns: workspaceTabColumns(Platform.OS, tab, options?.columns ?? layout.usesSplitView),
482486
check: workspaceSelectionCheck,
483487
overlays,
484488
},
@@ -908,7 +912,9 @@ function AdaptiveWorkspaceLayoutContent(
908912
compactAndroidSettings ? { ...safeAreaInsets, bottom: 0 } : safeAreaInsets
909913
}
910914
>
911-
<View className="flex-1">{props.children}</View>
915+
<View className="flex-1">
916+
<SettingsWorkspaceLayout>{props.children}</SettingsWorkspaceLayout>
917+
</View>
912918
</SafeAreaInsetsContext>
913919
{compactAndroidSettings ? (
914920
<AndroidWorkspaceNavigation onNewThread={handleStartNewTask} />

‎apps/mobile/src/features/settings/SettingsRouteScreen.tsx‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { deriveProjectGroupLabel } from "@t3tools/client-runtime/state/project-g
2424
import { useSafeAreaInsets } from "react-native-safe-area-context";
2525

2626
import { hasCloudPublicConfig } from "../cloud/publicConfig";
27-
import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
27+
import { useAdaptiveWorkspaceLayout } from "../layout/adaptive-workspace-context";
2828
import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";
2929
import { SettingsRow } from "./components/SettingsRow";
3030
import { SettingsSection, SettingsSidebarContext } from "./components/SettingsSection";
@@ -39,8 +39,6 @@ import { filterSettingsSections } from "./settings-search";
3939
import { NativePrimaryColumnContext } from "../../native/v5-workspace-context";
4040

4141
export function SettingsRouteScreen() {
42-
const surface = useWorkspaceSurface();
43-
const workspace = useWorkspaceTabs();
4442
const { layout } = useAdaptiveWorkspaceLayout();
4543
const inSidebar = use(NativePrimaryColumnContext) !== null && layout.usesSplitView;
4644
// The page open beside the list: the route right above the Settings tab's route. While
@@ -52,6 +50,17 @@ export function SettingsRouteScreen() {
5250
? rememberedSettingsPage(currentWorkspaceSelections())
5351
: (state.routes[list + 1]?.name ?? null);
5452
});
53+
return <SettingsListScreen sidebar={inSidebar} selected={selected} />;
54+
}
55+
56+
export function SettingsListScreen(props: {
57+
readonly sidebar: boolean;
58+
readonly selected: string | null;
59+
}) {
60+
const surface = useWorkspaceSurface();
61+
const workspace = useWorkspaceTabs();
62+
const inSidebar = props.sidebar;
63+
const selected = props.selected;
5564
const sidebar = useMemo(() => (inSidebar ? { selected } : null), [inSidebar, selected]);
5665
const content = hasCloudPublicConfig() ? (
5766
<ConfiguredSettingsRouteScreen />
@@ -66,6 +75,7 @@ export function SettingsRouteScreen() {
6675
{Platform.OS === "android" ? (
6776
<SettingsScreen
6877
title="Settings"
78+
workspaceRoot={inSidebar}
6979
trailing={<AndroidSettingsEnvironmentFilter />}
7080
search={
7181
workspace
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { NavigationRouteContext } from "@react-navigation/native";
2+
import { Activity, useState, type ReactNode } from "react";
3+
import { View } from "react-native";
4+
5+
import { AppText } from "../../components/ui/AppText";
6+
import { useAdaptiveWorkspaceLayout } from "../layout/adaptive-workspace-context";
7+
import { useAndroidWorkspaceRoot } from "../workspace/AndroidWorkspaceTabsProvider";
8+
import { SettingsListScreen } from "./SettingsRouteScreen";
9+
import { SettingsScreen } from "./components/SettingsScreen";
10+
11+
const SETTINGS_LIST_ROUTE = { key: "settings-sidebar", name: "Settings" };
12+
13+
export function SettingsWorkspaceLayout(props: { readonly children: ReactNode }) {
14+
const root = useAndroidWorkspaceRoot();
15+
const { layout } = useAdaptiveWorkspaceLayout();
16+
const split = root?.settingsOpen === true && layout.usesSplitView;
17+
const [visited, setVisited] = useState(split);
18+
if (split && !visited) setVisited(true);
19+
const emptyDetail = split && root.settingsPage === null;
20+
const route = root?.settingsRoute ?? SETTINGS_LIST_ROUTE;
21+
22+
return (
23+
<View className="flex-1 flex-row">
24+
{visited ? (
25+
<Activity mode={split ? "visible" : "hidden"}>
26+
<View style={{ width: layout.listPaneWidth ?? 0, marginRight: layout.listPaneGap ?? 0 }}>
27+
<NavigationRouteContext value={route}>
28+
<SettingsListScreen sidebar selected={root?.settingsPage ?? null} />
29+
</NavigationRouteContext>
30+
</View>
31+
</Activity>
32+
) : null}
33+
<View className="flex-1">
34+
<View
35+
className="flex-1"
36+
pointerEvents={emptyDetail ? "none" : "auto"}
37+
accessibilityElementsHidden={emptyDetail}
38+
importantForAccessibility={emptyDetail ? "no-hide-descendants" : "auto"}
39+
style={emptyDetail ? { opacity: 0 } : undefined}
40+
>
41+
{props.children}
42+
</View>
43+
{emptyDetail ? (
44+
<View className="absolute inset-0">
45+
<NavigationRouteContext value={route}>
46+
<SettingsScreen title="Settings" workspaceRoot>
47+
<View className="flex-1 items-center justify-center p-6">
48+
<AppText className="text-foreground-muted">Select a setting</AppText>
49+
</View>
50+
</SettingsScreen>
51+
</NavigationRouteContext>
52+
</View>
53+
) : null}
54+
</View>
55+
</View>
56+
);
57+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { ReactNode } from "react";
2+
3+
export function SettingsWorkspaceLayout(props: { readonly children: ReactNode }) {
4+
return props.children;
5+
}

‎apps/mobile/src/features/settings/components/SettingsScreen.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useWorkspaceTabs } from "../../workspace/WorkspaceTabsContext";
99
import { useWorkspaceSurface } from "../../workspace/useWorkspaceSurface";
1010
import { useDeviceType } from "../../../native/native-layout-metrics";
1111
import { useCompactWorkspaceTabs } from "../../workspace/useSystemWorkspaceTabBar";
12+
import { useAdaptiveWorkspaceLayout } from "../../layout/adaptive-workspace-context";
1213
import {
1314
ScreenScrollViewBottomInset,
1415
ScreenScrollViewSurface,
@@ -29,6 +30,7 @@ export function SettingsScreen(
2930
const surface = useWorkspaceSurface();
3031
const deviceType = useDeviceType();
3132
const compactTabs = useCompactWorkspaceTabs();
33+
const { layout } = useAdaptiveWorkspaceLayout();
3234
const cover = deviceType === "duo-folded";
3335
// Android opens Settings as a workspace tab; its root page has nothing to go back to.
3436
const workspaceRoot =
@@ -55,7 +57,11 @@ export function SettingsScreen(
5557
{props.formSheet ? (
5658
props.children
5759
) : (
58-
<SettingsScreenContent>{props.children}</SettingsScreenContent>
60+
<SettingsScreenContent
61+
insetHorizontal={Platform.OS === "android" && layout.usesSplitView}
62+
>
63+
{props.children}
64+
</SettingsScreenContent>
5965
)}
6066
</ScreenScrollViewSurface>
6167
</ScreenScrollViewBottomInset>

‎apps/mobile/src/features/workspace/AndroidWorkspaceNavigation.android.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ export function AndroidWorkspaceNavigation(props: {
118118
// Reset the root stack even when a nested settings navigator has focus.
119119
if (root) {
120120
navigation.dispatch({
121-
...CommonActions.reset(androidWorkspaceTabNavigationState(root.getState(), tab)),
121+
...CommonActions.reset(
122+
androidWorkspaceTabNavigationState(root.getState(), tab, rail),
123+
),
122124
target: root.key,
123125
});
124126
}

‎apps/mobile/src/features/workspace/AndroidWorkspaceTabsProvider.tsx‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ interface AndroidWorkspaceRoot {
2121
readonly getState: () => NavigationState;
2222
/** Settings is derived from navigation rather than kept in the tab reducer. */
2323
readonly settingsOpen: boolean;
24+
readonly settingsRoute: NavigationState["routes"][number] | undefined;
25+
readonly settingsPage: string | null;
2426
}
2527

2628
const AndroidWorkspaceRootContext = createContext<AndroidWorkspaceRoot | null>(null);
@@ -41,7 +43,14 @@ export function AndroidWorkspaceTabsProvider(props: {
4143
const getState = useCallback(() => stateRef.current, []);
4244
const key = props.state.key;
4345
const settingsOpen = Platform.OS === "android" && androidWorkspaceSettingsOpen(props.state);
44-
const root = useMemo(() => ({ key, getState, settingsOpen }), [key, getState, settingsOpen]);
46+
const settingsIndex = props.state.routes.findIndex((route) => route.name === "Settings");
47+
const settingsRoute = settingsIndex < 0 ? undefined : props.state.routes[settingsIndex];
48+
const settingsPage =
49+
settingsIndex < 0 ? null : (props.state.routes[settingsIndex + 1]?.name ?? null);
50+
const root = useMemo(
51+
() => ({ key, getState, settingsOpen, settingsRoute, settingsPage }),
52+
[key, getState, settingsOpen, settingsRoute, settingsPage],
53+
);
4554
return Platform.OS === "android" ? (
4655
<WorkspaceTabsProvider>
4756
<AndroidWorkspaceRootContext value={root}>{props.children}</AndroidWorkspaceRootContext>

‎apps/mobile/src/features/workspace/android-workspace-tabs.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
settingsOpenIn,
77
} from "../settings/settings-navigation";
88
import type { WorkspaceTab } from "./workspace-tabs-state";
9+
import { rememberedSettingsPage, workspaceSelectionsIn } from "./workspace-tab-navigation";
910

1011
/** Settings joins the two list tabs as a top-level Android destination. */
1112
export type AndroidWorkspaceNavigationTab = WorkspaceTab | "settings";
@@ -16,9 +17,15 @@ const WORKSPACE_ROUTES = new Set(["Home", "Thread", "PullRequest"]);
1617
export function androidWorkspaceTabNavigationState(
1718
state: NavigationState,
1819
tab: AndroidWorkspaceNavigationTab,
20+
columns = false,
1921
) {
2022
const home = androidWorkspaceNavigationState(state);
21-
return tab === "settings" ? settingsNavigationState(home, undefined) : home;
23+
if (tab !== "settings") return home;
24+
const page = columns
25+
? rememberedSettingsPage(workspaceSelectionsIn(state.routes, {}))
26+
: undefined;
27+
const current = state.routes.find((route) => route.name === page);
28+
return settingsNavigationState(state, page, current?.params);
2229
}
2330

2431
/** Settings stays selected while one of its pages is the topmost workspace route. */

‎apps/mobile/src/features/workspace/useSelectWorkspaceTab.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useCallback } from "react";
22
import { Keyboard } from "react-native";
33

4-
import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
4+
import { useAdaptiveWorkspaceLayout } from "../layout/adaptive-workspace-context";
55
import { useWorkspaceTabs } from "./WorkspaceTabsContext";
66
import type { WorkspaceTab } from "./workspace-tabs-state";
77

‎apps/mobile/src/features/workspace/useSystemWorkspaceTabBar.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { isDuoDevice } from "../../lib/device-type";
22
import { Platform } from "react-native";
3-
import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
3+
import { useAdaptiveWorkspaceLayout } from "../layout/adaptive-workspace-context";
44
import { useDeviceType, useNativeLayoutMetrics } from "../../native/native-layout-metrics";
55
import { useWorkspaceTabs } from "./WorkspaceTabsContext";
66
import { workspaceUsesColumns } from "./workspace-tabs-state";

0 commit comments

Comments
 (0)