Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { requireNativeView } from "expo";
import { CommonActions, useNavigation } from "@react-navigation/native";
import { androidWorkspaceNavigationState } from "../../lib/android-workspace-navigation";
import { useNavigation } from "@react-navigation/native";
import { resetAndroidWorkspace } from "../../lib/android-workspace-navigation";
import {
Keyboard,
processColor,
Expand Down Expand Up @@ -81,9 +81,7 @@ export function AndroidWorkspaceNavigation(props: {
const tab = event.nativeEvent.tab;
if (tab !== workspace.state.selectedTab) {
workspace.dispatch({ type: "select", tab });
navigation.dispatch((state) =>
CommonActions.reset(androidWorkspaceNavigationState(state)),
);
resetAndroidWorkspace(navigation);
}
props.onSelect?.();
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import {
CommonActions,
StackActions,
useFocusEffect,
useNavigation,
} from "@react-navigation/native";
import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native";
import { useCallback } from "react";
import { BackHandler, Keyboard, Platform } from "react-native";
import { androidWorkspaceNavigationState } from "../../lib/android-workspace-navigation";
import { resetAndroidWorkspace } from "../../lib/android-workspace-navigation";
import { useWorkspaceTabs } from "./WorkspaceTabsContext";
import type { WorkspaceTab } from "./workspace-tabs-state";

Expand All @@ -16,7 +11,7 @@ export function useWorkspaceListNavigation(tab: WorkspaceTab) {
return useCallback(() => {
if (Platform.OS === "android") {
dispatch?.({ type: "select", tab });
navigation.dispatch((state) => CommonActions.reset(androidWorkspaceNavigationState(state)));
resetAndroidWorkspace(navigation);
} else if (navigation.canGoBack()) navigation.goBack();
else navigation.dispatch(StackActions.replace("Home"));
}, [navigation, dispatch, tab]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { CommonActions, useNavigation } from "@react-navigation/native";
import { useNavigation } from "@react-navigation/native";
import { useCallback } from "react";
import { Keyboard } from "react-native";
import {
androidWorkspaceNavigationState,
resetAndroidWorkspace,
type AndroidWorkspaceDestination,
} from "../../lib/android-workspace-navigation";
import { useWorkspaceTabs } from "./WorkspaceTabsContext";
Expand All @@ -20,9 +20,7 @@ export function useAndroidWorkspaceSelection() {
type: "select",
tab: destination.name === "Thread" ? "threads" : "pullRequests",
});
navigation.dispatch((state) =>
CommonActions.reset(androidWorkspaceNavigationState(state, destination, reviewers)),
);
resetAndroidWorkspace(navigation, destination, reviewers);
},
[navigation, dispatch],
);
Expand Down
27 changes: 27 additions & 0 deletions apps/mobile/src/lib/android-workspace-navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import * as NodeModule from "node:module";
import { describe, expect, it } from "vite-plus/test";
import {
androidWorkspaceNavigationState,
resetAndroidWorkspace,
type AndroidWorkspaceDestination,
type AndroidWorkspaceResetAction,
} from "./android-workspace-navigation";

const require = NodeModule.createRequire(import.meta.url);
Expand Down Expand Up @@ -183,4 +185,29 @@ describe("Android workspace navigation", () => {
expect(state.routes[0]?.key).toBe(previous.routes[0]?.key);
expect(state.index).toBe(0);
});

it("resets the root stack when a nested Settings stack has focus", () => {
const root = apply(router.getInitialState(options), StackActions.push("SettingsSheet"));
const settings = router.getInitialState({
...options,
routeNames: ["SettingsContent", "SettingsEnvironments"],
});
const actions: AndroidWorkspaceResetAction[] = [];
const rootNavigation = {
dispatch: () => undefined,
getParent: () => undefined,
getState: () => root,
};
resetAndroidWorkspace(
{
dispatch: (action) => actions.push(action),
getParent: () => rootNavigation,
getState: () => settings,
},
thread,
);
const [action] = actions;
expect(action?.target).toBe(root.key);
expect(names(apply(root, action!))).toEqual(["Home", "Thread"]);
});
});
35 changes: 34 additions & 1 deletion apps/mobile/src/lib/android-workspace-navigation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import type { NavigationState } from "@react-navigation/native";
import type { CommonActions, NavigationState } from "@react-navigation/native";

export type AndroidWorkspaceDestination =
| { readonly name: "Thread"; readonly params: ReactNavigation.RootParamList["Thread"] }
| { readonly name: "PullRequest"; readonly params: ReactNavigation.RootParamList["PullRequest"] };

export type AndroidWorkspaceResetAction = ReturnType<typeof CommonActions.reset> & {
readonly target: string;
};

interface WorkspaceNavigation {
readonly dispatch: (action: AndroidWorkspaceResetAction) => void;
readonly getParent: () => WorkspaceNavigation | undefined;
readonly getState: () => NavigationState | undefined;
}

export function androidWorkspaceNavigationState(
state: NavigationState,
destination?: AndroidWorkspaceDestination,
Expand Down Expand Up @@ -31,3 +41,26 @@ export function androidWorkspaceNavigationState(
],
};
}

/**
* Resets the root stack to a workspace list or detail. Always reads and targets the root
* navigator: the sidebar and rail render in the root layout, where `useNavigation()` is the
* container, and a container dispatch runs on the deepest focused navigator. With Settings
* or New task open, that is their nested stack, which rejects the reset.
*/
export function resetAndroidWorkspace(
navigation: WorkspaceNavigation,
destination?: AndroidWorkspaceDestination,
reviewers?: ReactNavigation.RootParamList["PullRequestReviewers"],
) {
let root = navigation;
for (let parent = root.getParent(); parent; parent = root.getParent()) root = parent;
const state = root.getState();
if (!state) return;
// Same shape as `CommonActions.reset`, which would pull React Native into node tests.
navigation.dispatch({
type: "RESET",
payload: androidWorkspaceNavigationState(state, destination, reviewers),
target: state.key,
});
}
9 changes: 2 additions & 7 deletions apps/mobile/src/native/useOpenPullRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { useNavigation, useRoute, StackActions } from "@react-navigation/native"
import { nativePullRequestDestination } from "../lib/pullRequestNavigation";
import { useCallback } from "react";
import { Platform } from "react-native";
import { CommonActions } from "@react-navigation/native";
import { androidWorkspaceNavigationState } from "../lib/android-workspace-navigation";
import { resetAndroidWorkspace } from "../lib/android-workspace-navigation";
import { EnvironmentId, ThreadId } from "@t3tools/contracts";
import { useThreadShell } from "../state/entities";
import { tryOpenExternalUrl } from "../lib/openExternalUrl";
Expand Down Expand Up @@ -45,11 +44,7 @@ export function useOpenPullRequest(options?: {
);
if (!params) return tryOpenExternalUrl(url, "pull-request");
if (Platform.OS === "android")
navigation.dispatch((state) =>
CommonActions.reset(
androidWorkspaceNavigationState(state, { name: "PullRequest", params }),
),
);
resetAndroidWorkspace(navigation, { name: "PullRequest", params });
else if (replace) navigation.dispatch(StackActions.replace("PullRequest", params));
else navigation.navigate("PullRequest", params);
return true;
Expand Down
Loading