apps/mobile/src/features/workspace/workspace-detail-selection.ts

import type {
  NavigationAction,
  ParamListBase,
  PartialState,
  NavigationState,
  Router,
  StackNavigationState,
} from "@react-navigation/native";
import { workspaceRoutesOf } from "../layout/workspace-overlay-routes";

type RootParams = ReactNavigation.RootParamList;

/** A page the workspace's detail column shows on its own: what a list row selects. */
export type WorkspaceDetail =
  | { readonly name: "Thread"; readonly params: RootParams["Thread"] }
  | { readonly name: "PullRequest"; readonly params: RootParams["PullRequest"] };

/** Android opens the reviewers sheet over the pull request in the same step. */
export type WorkspaceDetailSheet = {
  readonly name: "PullRequestReviewers";
  readonly params: RootParams["PullRequestReviewers"];
};

const SELECT_WORKSPACE_DETAIL = "SELECT_WORKSPACE_DETAIL";

export interface SelectWorkspaceDetailAction {
  readonly type: typeof SELECT_WORKSPACE_DETAIL;
  readonly payload: { readonly detail: WorkspaceDetail; readonly sheet?: WorkspaceDetailSheet };
}

/**
 * Shows `detail` as the workspace's only page, the way selecting a list row does. Drilling in
 * from a page pushes instead.
 */
export function selectWorkspaceDetail(
  detail: WorkspaceDetail,
  sheet?: WorkspaceDetailSheet,
): SelectWorkspaceDetailAction {
  return { type: SELECT_WORKSPACE_DETAIL, payload: sheet ? { detail, sheet } : { detail } };
}

const IDENTITY = {
  Thread: ["environmentId", "threadId"],
  PullRequest: ["environmentId", "projectId", "repository", "number", "host"],
} as const;

type RouteLike = { readonly name: string; readonly key?: string; readonly params?: object };

function showsDetail(route: RouteLike, detail: WorkspaceDetail) {
  if (route.name !== detail.name) return false;
  const current = route.params as Readonly<Record<string, unknown>> | undefined;
  const target = detail.params as Readonly<Record<string, unknown>> | undefined;
  return IDENTITY[detail.name].every((key) => current?.[key] === target?.[key]);
}

// UIKit pops a card by native Back before the router hears of it, so a row tapped meanwhile
// must not reuse the card on its way out.
let nativelyPoppingRoute: string | null = null;

export function setNativelyPoppingWorkspaceRoute(key: string | null) {
  nativelyPoppingRoute = key;
}

/**
 * The root stack after selecting `detail`: Home, then the detail alone. The route already showing
 * it, or else the detail column's current page of the same kind, keeps its key, so the column
 * updates in place instead of pushing.
 */
function workspaceDetailState<
  State extends { readonly index: number; readonly routes: readonly RouteLike[] },
>(state: State, detail: WorkspaceDetail, sheet?: WorkspaceDetailSheet, popping?: string | null) {
  const { workspace } = workspaceRoutesOf(state.routes.slice(0, state.index + 1));
  const leaving = workspace.findIndex((route) => route.key !== undefined && route.key === popping);
  const live = leaving < 0 ? workspace : workspace.slice(0, leaving);
  const home = live.find((route) => route.name === "Home") ?? { name: "Home" };
  const page = live.find((route) => route.name !== "Home" && route.name !== "Settings");
  const reused =
    live.findLast((route) => showsDetail(route, detail)) ??
    (page?.name === detail.name ? page : undefined);
  const routes = [
    home,
    reused ? { ...reused, params: detail.params } : { name: detail.name, params: detail.params },
    ...(sheet ? [sheet] : []),
  ];
  return { ...state, index: routes.length - 1, routes };
}

/** Root stack router overrides that handle `selectWorkspaceDetail`. */
export function workspaceDetailRouter<Action extends NavigationAction>(
  original: Router<StackNavigationState<ParamListBase>, Action>,
): Partial<Router<StackNavigationState<ParamListBase>, Action>> {
  return {
    getStateForAction(state, action, options) {
      if (action.type !== SELECT_WORKSPACE_DETAIL) {
        return original.getStateForAction(state, action, options);
      }
      const { detail, sheet } = (action as unknown as SelectWorkspaceDetailAction).payload;
      const next = workspaceDetailState(state, detail, sheet, nativelyPoppingRoute);
      const reset = { type: "RESET", payload: { ...next, stale: false } };
      return original.getStateForAction(state, reset as unknown as Action, options);
    },
  };
}

/** A link to a single thread or pull request selects it, like its list row would. */
export function workspaceDetailLinkAction(
  state: PartialState<NavigationState> | NavigationState,
): SelectWorkspaceDetailAction | undefined {
  const pages = state.routes.filter((route) => route.name !== "Home");
  const [page] = pages;
  if (pages.length !== 1 || !page || page.state) return undefined;
  if (page.name !== "Thread" && page.name !== "PullRequest") return undefined;
  return selectWorkspaceDetail({ name: page.name, params: page.params } as WorkspaceDetail);
}