Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,13 @@ import {
currentWorkspaceSelections,
workspaceSelectionCheck,
} from "../workspace/workspace-selection-store";
import { workspaceTabNavigationState } from "../workspace/workspace-tab-navigation";
import {
workspaceTabColumns,
workspaceTabNavigationState,
} from "../workspace/workspace-tab-navigation";
import type { WorkspaceTab } from "../workspace/workspace-tabs-state";
import { workspaceRoutesOf } from "./workspace-overlay-routes";
import { SettingsWorkspaceLayout } from "../settings/SettingsWorkspaceLayout";

export { useAdaptiveWorkspaceLayout };

Expand Down Expand Up @@ -478,7 +482,7 @@ function AdaptiveWorkspaceLayoutContent(
tab,
currentWorkspaceSelections(),
{
columns: Platform.OS === "ios" && (options?.columns ?? layout.usesSplitView),
columns: workspaceTabColumns(Platform.OS, tab, options?.columns ?? layout.usesSplitView),
check: workspaceSelectionCheck,
overlays,
},
Expand Down Expand Up @@ -905,7 +909,9 @@ function AdaptiveWorkspaceLayoutContent(
compactAndroidSettings ? { ...safeAreaInsets, bottom: 0 } : safeAreaInsets
}
>
<View className="flex-1">{props.children}</View>
<View className="flex-1">
<SettingsWorkspaceLayout>{props.children}</SettingsWorkspaceLayout>
</View>
</SafeAreaInsetsContext>
{compactAndroidSettings ? (
<AndroidWorkspaceNavigation onNewThread={handleStartNewTask} />
Expand Down
16 changes: 13 additions & 3 deletions apps/mobile/src/features/settings/SettingsRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { deriveProjectGroupLabel } from "@t3tools/client-runtime/state/project-g
import { useSafeAreaInsets } from "react-native-safe-area-context";

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

export function SettingsRouteScreen() {
const surface = useWorkspaceSurface();
const workspace = useWorkspaceTabs();
const { layout } = useAdaptiveWorkspaceLayout();
const inSidebar = use(NativePrimaryColumnContext) !== null && layout.usesSplitView;
// The page open beside the list: the route right above the Settings tab's route. While
Expand All @@ -52,6 +50,17 @@ export function SettingsRouteScreen() {
? rememberedSettingsPage(currentWorkspaceSelections())
: (state.routes[list + 1]?.name ?? null);
});
return <SettingsListScreen sidebar={inSidebar} selected={selected} />;
}

export function SettingsListScreen(props: {
readonly sidebar: boolean;
readonly selected: string | null;
}) {
const surface = useWorkspaceSurface();
const workspace = useWorkspaceTabs();
const inSidebar = props.sidebar;
const selected = props.selected;
const sidebar = useMemo(() => (inSidebar ? { selected } : null), [inSidebar, selected]);
const content = hasCloudPublicConfig() ? (
<ConfiguredSettingsRouteScreen />
Expand All @@ -66,6 +75,7 @@ export function SettingsRouteScreen() {
{Platform.OS === "android" ? (
<SettingsScreen
title="Settings"
workspaceRoot={inSidebar}
trailing={<AndroidSettingsEnvironmentFilter />}
search={
workspace
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { NavigationRouteContext } from "@react-navigation/native";
import { Activity, useState, type ReactNode } from "react";
import { View } from "react-native";

import { AppText } from "../../components/ui/AppText";
import { useAdaptiveWorkspaceLayout } from "../layout/adaptive-workspace-context";
import { useAndroidWorkspaceRoot } from "../workspace/AndroidWorkspaceTabsProvider";
import { SettingsListScreen } from "./SettingsRouteScreen";
import { SettingsScreen } from "./components/SettingsScreen";

const SETTINGS_LIST_ROUTE = { key: "settings-sidebar", name: "Settings" };

export function SettingsWorkspaceLayout(props: { readonly children: ReactNode }) {
const root = useAndroidWorkspaceRoot();
const { layout } = useAdaptiveWorkspaceLayout();
const split = root?.settingsOpen === true && layout.usesSplitView;
const [visited, setVisited] = useState(split);
if (split && !visited) setVisited(true);
const emptyDetail = split && root.settingsPage === null;
const route = root?.settingsRoute ?? SETTINGS_LIST_ROUTE;

return (
<View className="flex-1 flex-row">
{visited ? (
<Activity mode={split ? "visible" : "hidden"}>
<View style={{ width: layout.listPaneWidth ?? 0, marginRight: layout.listPaneGap ?? 0 }}>
<NavigationRouteContext value={route}>
<SettingsListScreen sidebar selected={root?.settingsPage ?? null} />
</NavigationRouteContext>
</View>
</Activity>
) : null}
<View className="flex-1">
<View
className="flex-1"
pointerEvents={emptyDetail ? "none" : "auto"}
accessibilityElementsHidden={emptyDetail}
importantForAccessibility={emptyDetail ? "no-hide-descendants" : "auto"}
style={emptyDetail ? { opacity: 0 } : undefined}
>
{props.children}
</View>
{emptyDetail ? (
<View className="absolute inset-0">
<NavigationRouteContext value={route}>
<SettingsScreen title="Settings" workspaceRoot>
<View className="flex-1 items-center justify-center p-6">
<AppText className="text-foreground-muted">Select a setting</AppText>
</View>
</SettingsScreen>
</NavigationRouteContext>
</View>
) : null}
</View>
</View>
);
}
5 changes: 5 additions & 0 deletions apps/mobile/src/features/settings/SettingsWorkspaceLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { ReactNode } from "react";

export function SettingsWorkspaceLayout(props: { readonly children: ReactNode }) {
return props.children;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useWorkspaceTabs } from "../../workspace/WorkspaceTabsContext";
import { useWorkspaceSurface } from "../../workspace/useWorkspaceSurface";
import { useDeviceType } from "../../../native/native-layout-metrics";
import { useCompactWorkspaceTabs } from "../../workspace/useSystemWorkspaceTabBar";
import { useAdaptiveWorkspaceLayout } from "../../layout/adaptive-workspace-context";
import {
ScreenScrollViewBottomInset,
ScreenScrollViewSurface,
Expand All @@ -29,6 +30,7 @@ export function SettingsScreen(
const surface = useWorkspaceSurface();
const deviceType = useDeviceType();
const compactTabs = useCompactWorkspaceTabs();
const { layout } = useAdaptiveWorkspaceLayout();
const cover = deviceType === "duo-folded";
// Android opens Settings as a workspace tab; its root page has nothing to go back to.
const workspaceRoot =
Expand All @@ -55,7 +57,11 @@ export function SettingsScreen(
{props.formSheet ? (
props.children
) : (
<SettingsScreenContent>{props.children}</SettingsScreenContent>
<SettingsScreenContent
insetHorizontal={Platform.OS === "android" && layout.usesSplitView}
>
{props.children}
</SettingsScreenContent>
)}
</ScreenScrollViewSurface>
</ScreenScrollViewBottomInset>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ export function AndroidWorkspaceNavigation(props: {
// Reset the root stack even when a nested settings navigator has focus.
if (root) {
navigation.dispatch({
...CommonActions.reset(androidWorkspaceTabNavigationState(root.getState(), tab)),
...CommonActions.reset(
androidWorkspaceTabNavigationState(root.getState(), tab, rail),
),
target: root.key,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ interface AndroidWorkspaceRoot {
readonly getState: () => NavigationState;
/** Settings is derived from navigation rather than kept in the tab reducer. */
readonly settingsOpen: boolean;
readonly settingsRoute: NavigationState["routes"][number] | undefined;
readonly settingsPage: string | null;
}

const AndroidWorkspaceRootContext = createContext<AndroidWorkspaceRoot | null>(null);
Expand All @@ -41,7 +43,14 @@ export function AndroidWorkspaceTabsProvider(props: {
const getState = useCallback(() => stateRef.current, []);
const key = props.state.key;
const settingsOpen = Platform.OS === "android" && androidWorkspaceSettingsOpen(props.state);
const root = useMemo(() => ({ key, getState, settingsOpen }), [key, getState, settingsOpen]);
const settingsIndex = props.state.routes.findIndex((route) => route.name === "Settings");
const settingsRoute = settingsIndex < 0 ? undefined : props.state.routes[settingsIndex];
const settingsPage =
settingsIndex < 0 ? null : (props.state.routes[settingsIndex + 1]?.name ?? null);
const root = useMemo(
() => ({ key, getState, settingsOpen, settingsRoute, settingsPage }),
[key, getState, settingsOpen, settingsRoute, settingsPage],
);
return Platform.OS === "android" ? (
<WorkspaceTabsProvider>
<AndroidWorkspaceRootContext value={root}>{props.children}</AndroidWorkspaceRootContext>
Expand Down
9 changes: 8 additions & 1 deletion apps/mobile/src/features/workspace/android-workspace-tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
settingsOpenIn,
} from "../settings/settings-navigation";
import type { WorkspaceTab } from "./workspace-tabs-state";
import { rememberedSettingsPage, workspaceSelectionsIn } from "./workspace-tab-navigation";

/** Settings joins the two list tabs as a top-level Android destination. */
export type AndroidWorkspaceNavigationTab = WorkspaceTab | "settings";
Expand All @@ -16,9 +17,15 @@ const WORKSPACE_ROUTES = new Set(["Home", "Thread", "PullRequest"]);
export function androidWorkspaceTabNavigationState(
state: NavigationState,
tab: AndroidWorkspaceNavigationTab,
columns = false,
) {
const home = androidWorkspaceNavigationState(state);
return tab === "settings" ? settingsNavigationState(home, undefined) : home;
if (tab !== "settings") return home;
const page = columns
? rememberedSettingsPage(workspaceSelectionsIn(state.routes, {}))
: undefined;
const current = state.routes.find((route) => route.name === page);
return settingsNavigationState(state, page, current?.params);
}

/** Settings stays selected while one of its pages is the topmost workspace route. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback } from "react";
import { Keyboard } from "react-native";

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isDuoDevice } from "../../lib/device-type";
import { Platform } from "react-native";
import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
import { useAdaptiveWorkspaceLayout } from "../layout/adaptive-workspace-context";
import { useDeviceType, useNativeLayoutMetrics } from "../../native/native-layout-metrics";
import { useWorkspaceTabs } from "./WorkspaceTabsContext";
import { workspaceUsesColumns } from "./workspace-tabs-state";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { describe, expect, it } from "vite-plus/test";

import { restoreSettingsLinkState } from "../settings/settings-navigation";
import { workspaceSelectionsIn, workspaceTabNavigationState } from "./workspace-tab-navigation";
import {
workspaceSelectionsIn,
workspaceTabColumns,
workspaceTabNavigationState,
} from "./workspace-tab-navigation";

const home = { key: "home", name: "Home" };
const thread = {
Expand Down Expand Up @@ -47,6 +51,36 @@ describe("workspace tab selections", () => {
describe("opening a workspace tab", () => {
const columns = { columns: true, check: available };

it("opens Android Settings beside its default page only in a wide window", () => {
const open = (wide: boolean) =>
workspaceTabNavigationState(
{ routes: [home, thread] },
"settings",
{},
{
columns: workspaceTabColumns("android", "settings", wide),
check: available,
},
);
expect(names(open(false))).toEqual(["Home", "Settings"]);
expect(names(open(true))).toEqual(["Home", "Settings", "SettingsEnvironments"]);
});

it("keeps Android list tabs and iOS remembered-detail navigation unchanged", () => {
const open = (platform: string) =>
workspaceTabNavigationState(
{ routes: [home, thread] },
"threads",
{ threads: thread.params },
{
columns: workspaceTabColumns(platform, "threads", true),
check: available,
},
);
expect(names(open("android"))).toEqual(["Home"]);
expect(names(open("ios"))).toEqual(["Home", "Thread"]);
});

it("opens the remembered page beside the list with columns, and the list without", () => {
const state = { routes: [home, thread] };
const selections = { threads: thread.params, pullRequests: pullRequest.params };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ type PullRequestSelection = NonNullable<WorkspaceSelections["pullRequests"]>;
/** Opened when Settings has columns and none of its pages has been opened yet. */
export const DEFAULT_SETTINGS_PAGE = "SettingsEnvironments" satisfies SettingsPageTarget;

export function workspaceTabColumns(
platform: string,
tab: WorkspaceTab | "settings",
columns: boolean,
) {
return columns && (platform === "ios" || (platform === "android" && tab === "settings"));
}

// Pages a Settings row opens, minus Project overview: it shows the project chosen in the
// Settings scope, which resets whenever Settings closes.
const REMEMBERED_SETTINGS_PAGES: ReadonlySet<string> = new Set(
Expand Down
24 changes: 22 additions & 2 deletions apps/mobile/src/lib/android-workspace-navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ const options = {
"PullRequestFile",
"PullRequestReviewers",
"Settings",
"SettingsEnvironments",
"SettingsAppearance",
"SettingsOpenSourceLicenses",
"SettingsUsage",
"SettingsUsageAccount",
],
Expand Down Expand Up @@ -218,11 +221,28 @@ describe("Android workspace navigation", () => {
describe("Android Settings tab", () => {
type State = ReturnType<typeof router.getInitialState>;
type Tab = AndroidWorkspaceNavigationTab;
const openTab = (state: State, tab: Tab) =>
apply(state, CommonActions.reset(androidWorkspaceTabNavigationState(state, tab)));
const openTab = (state: State, tab: Tab, columns = false) =>
apply(state, CommonActions.reset(androidWorkspaceTabNavigationState(state, tab, columns)));
const selectedTab = (state: State, previous: Tab) =>
androidWorkspaceTabFromNavigationState(state, previous);

it("opens a default Settings page beside the category list in wide layouts", () => {
const settings = openTab(legacyHistory(), "settings", true);
expect(names(settings)).toEqual(["Home", "Settings", "SettingsEnvironments"]);
expect(selectedTab(settings, "threads")).toBe("settings");
expect(names(apply(settings, StackActions.pop()))).toEqual(["Home", "Settings"]);
});

it("keeps the selected category mounted when reselecting wide Settings", () => {
let settings = openTab(legacyHistory(), "settings");
settings = apply(settings, StackActions.push("SettingsAppearance"));
const category = settings.routes.at(-1);
settings = apply(settings, StackActions.push("SettingsOpenSourceLicenses"));
const reselected = openTab(settings, "settings", true);
expect(names(reselected)).toEqual(["Home", "Settings", "SettingsAppearance"]);
expect(reselected.routes.at(-1)?.key).toBe(category?.key);
});

it("opens over Home alone, dropping chat and PR history, and Back returns to Home", () => {
const previous = legacyHistory();
const settings = openTab(previous, "settings");
Expand Down
Loading
Loading