authorgravatar for me@pixp.ccAlex <me@pixp.cc> 2026-10-11 20:45:15-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-11 20:45:15-07:00
log2e24ec44dbf3047ab248487ec1d05836e94775e4
tree3277f26f5bc7d33bd8c8d39da9253db10547b4af
parent4ab4cdf452374502b87d0a83047c6cd25a9db7aa
signature Signed by PGP key B5690EEEBB952194

fix(mobile): show rounded pull request selection in the sidebar (#18480)


10 files changed, 308 insertions(+), 123 deletions(-)

apps/mobile/src/Stack.tsx+2
...@@ -51,6 +51,7 @@ import { useConnectOnboardingNavigation } from "./features/cloud/connectOnboardi...@@ -51,6 +51,7 @@ import { useConnectOnboardingNavigation } from "./features/cloud/connectOnboardi
51import { AttachmentFileScreen } from "./features/files/AttachmentFileScreen";51import { AttachmentFileScreen } from "./features/files/AttachmentFileScreen";
52import { ThreadFilesTreeScreen, ThreadFileScreen } from "./features/files/ThreadFilesRouteScreen";52import { ThreadFilesTreeScreen, ThreadFileScreen } from "./features/files/ThreadFilesRouteScreen";
53import { AdaptiveWorkspaceLayout } from "./features/layout/AdaptiveWorkspaceLayout";53import { AdaptiveWorkspaceLayout } from "./features/layout/AdaptiveWorkspaceLayout";
54import { selectedPullRequestKey } from "./features/pull-requests/pullRequestSelection";
54import {55import {
55 HardwareKeyboardCommandOverlay,56 HardwareKeyboardCommandOverlay,
56 HardwareKeyboardCommandProvider,57 HardwareKeyboardCommandProvider,
...@@ -658,6 +659,7 @@ function RootStackLayout(props: {...@@ -658,6 +659,7 @@ function RootStackLayout(props: {
658 <AdaptiveWorkspaceLayout659 <AdaptiveWorkspaceLayout
659 pathname={workspaceLocation.pathname}660 pathname={workspaceLocation.pathname}
660 workspaceRouteKey={workspaceLocation.routeKey}661 workspaceRouteKey={workspaceLocation.routeKey}
662 selectedPullRequestKey={selectedPullRequestKey(props.state)}
661 >663 >
662 {props.children}664 {props.children}
663 <HardwareKeyboardCommandOverlay />665 <HardwareKeyboardCommandOverlay />
apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx+6-1
...@@ -220,6 +220,7 @@ export function AdaptiveWorkspaceLayout(props: {...@@ -220,6 +220,7 @@ export function AdaptiveWorkspaceLayout(props: {
220 readonly children: ReactNode;220 readonly children: ReactNode;
221 readonly pathname: string;221 readonly pathname: string;
222 readonly workspaceRouteKey: string | undefined;222 readonly workspaceRouteKey: string | undefined;
223 readonly selectedPullRequestKey?: string | null;
223}) {224}) {
224 const preferencesResult = useAtomValue(mobilePreferencesAtom);225 const preferencesResult = useAtomValue(mobilePreferencesAtom);
225 if (!AsyncResult.isSuccess(preferencesResult)) {226 if (!AsyncResult.isSuccess(preferencesResult)) {
...@@ -245,6 +246,7 @@ function AdaptiveWorkspaceLayoutContent(...@@ -245,6 +246,7 @@ function AdaptiveWorkspaceLayoutContent(
245 readonly children: ReactNode;246 readonly children: ReactNode;
246 readonly pathname: string;247 readonly pathname: string;
247 readonly workspaceRouteKey: string | undefined;248 readonly workspaceRouteKey: string | undefined;
249 readonly selectedPullRequestKey?: string | null;
248 } & {250 } & {
249 readonly projectGroupingMode: SidebarProjectGroupingMode;251 readonly projectGroupingMode: SidebarProjectGroupingMode;
250 readonly savedThreadListFilters?: ThreadListFilters | undefined;252 readonly savedThreadListFilters?: ThreadListFilters | undefined;
...@@ -739,7 +741,10 @@ function AdaptiveWorkspaceLayoutContent(...@@ -739,7 +741,10 @@ function AdaptiveWorkspaceLayoutContent(
739 />741 />
740 )}742 )}
741 >743 >
742 <AndroidWorkspaceSidebar onRequestVisibility={revealPrimarySidebar}>744 <AndroidWorkspaceSidebar
745 onRequestVisibility={revealPrimarySidebar}
746 selectedPullRequestKey={props.selectedPullRequestKey ?? null}
747 >
743 <AndroidHomeFabLayout sidebar onStartNewTask={handleStartNewTask}>748 <AndroidHomeFabLayout sidebar onStartNewTask={handleStartNewTask}>
744 <ThreadNavigationSidebar749 <ThreadNavigationSidebar
745 width={layout.listPaneWidth}750 width={layout.listPaneWidth}
apps/mobile/src/features/pull-requests/PullRequestListRow.tsx+147-105
...@@ -6,6 +6,11 @@ import { ControlPillMenu } from "../../components/ControlPillMenu";...@@ -6,6 +6,11 @@ import { ControlPillMenu } from "../../components/ControlPillMenu";
6import type { ControlPillMenuProps } from "../../components/ControlPillMenu.types";6import type { ControlPillMenuProps } from "../../components/ControlPillMenu.types";
7import { cn } from "../../lib/cn";7import { cn } from "../../lib/cn";
8import { relativeTime } from "../../lib/time";8import { relativeTime } from "../../lib/time";
9import {
10 selectedThreadRowColors,
11 THREAD_LIST_V2_SIDEBAR_ROW_STYLE,
12 THREAD_LIST_V2_SIDEBAR_ROW_CONTAINER_STYLE,
13} from "../threads/thread-list-v2-row-appearance";
9import { PullRequestAvatar, PullRequestDiffStats } from "./PullRequestPresentation";14import { PullRequestAvatar, PullRequestDiffStats } from "./PullRequestPresentation";
10import {15import {
11 pullRequestLabelColor,16 pullRequestLabelColor,
...@@ -20,11 +25,27 @@ export const PullRequestListRow = memo(...@@ -20,11 +25,27 @@ export const PullRequestListRow = memo(
20 readonly item: EnvironmentPullRequestListEntry;25 readonly item: EnvironmentPullRequestListEntry;
21 readonly onSelect: (item: EnvironmentPullRequestListEntry) => void;26 readonly onSelect: (item: EnvironmentPullRequestListEntry) => void;
22 readonly selected: boolean;27 readonly selected: boolean;
28 readonly sidebar: boolean;
23 readonly busy: boolean;29 readonly busy: boolean;
24 readonly getActions: (item: EnvironmentPullRequestListEntry) => ControlPillMenuProps["actions"];30 readonly getActions: (item: EnvironmentPullRequestListEntry) => ControlPillMenuProps["actions"];
25 readonly onAction: (item: EnvironmentPullRequestListEntry, action: string) => void;31 readonly onAction: (item: EnvironmentPullRequestListEntry, action: string) => void;
26 }) {32 }) {
27 const { item } = props;33 const { item } = props;
34 const foreground = props.selected
35 ? selectedThreadRowColors.foregroundClassName
36 : props.sidebar
37 ? "text-drawer-foreground"
38 : "text-foreground";
39 const mutedForeground = props.selected
40 ? selectedThreadRowColors.mutedForegroundClassName
41 : props.sidebar
42 ? "text-drawer-foreground-muted"
43 : "text-foreground-muted";
44 const tertiaryForeground = props.selected
45 ? selectedThreadRowColors.mutedForegroundClassName
46 : props.sidebar
47 ? "text-drawer-foreground-muted"
48 : "text-foreground-tertiary";
28 const status = pullRequestStatus(item);49 const status = pullRequestStatus(item);
29 const review = pullRequestListReviewStatus(item);50 const review = pullRequestListReviewStatus(item);
30 const checkTint =51 const checkTint =
...@@ -34,123 +55,144 @@ export const PullRequestListRow = memo(...@@ -34,123 +55,144 @@ export const PullRequestListRow = memo(
34 ? "accent-danger-foreground"55 ? "accent-danger-foreground"
35 : "accent-warning-foreground";56 : "accent-warning-foreground";
36 return (57 return (
37 <ControlPillMenu58 <View style={props.sidebar ? THREAD_LIST_V2_SIDEBAR_ROW_CONTAINER_STYLE : undefined}>
38 shouldOpenOnLongPress59 <ControlPillMenu
39 title={`#${item.number} · ${item.repository}`}60 shouldOpenOnLongPress
40 actions={props.getActions(item)}61 title={`#${item.number} · ${item.repository}`}
41 onPressAction={({ nativeEvent }) => props.onAction(item, nativeEvent.event)}62 actions={props.getActions(item)}
42 >63 onPressAction={({ nativeEvent }) => props.onAction(item, nativeEvent.event)}
43 <Pressable
44 accessibilityRole="button"
45 accessibilityState={{ selected: props.selected, busy: props.busy }}
46 accessibilityLabel={`Pull request ${item.number}, ${item.title}, ${status.label}, by ${item.author?.login ?? "unknown author"}${review ? `, ${review.accessibleLabel}` : ""}${item.checksState ? `, checks ${item.checksState}` : ""}${item.stack ? `, stack ${item.stack.position} of ${item.stack.size}` : ""}${item.labels.length ? `, ${item.labels.map((label) => label.name).join(", ")}` : ""}, ${item.additions} additions, ${item.deletions} deletions`}
47 accessibilityHint="Double tap to open. Touch and hold for quick actions."
48 onPress={() => props.onSelect(item)}
49 disabled={props.busy}
50 className={cn(
51 "flex-row gap-3 px-4 active:bg-row-hover",
52 props.selected && "bg-thread-selected",
53 props.busy && "opacity-50",
54 )}
55 >64 >
56 <View className="pt-2.5">65 <Pressable
57 <SymbolView name={status.icon} size={20} tintColorClassName={status.tint} />66 accessibilityRole="button"
58 </View>67 accessibilityState={{ selected: props.selected, busy: props.busy }}
59 <View className="min-w-0 flex-1 gap-1 border-b border-border-subtle py-2.5">68 accessibilityLabel={`Pull request ${item.number}, ${item.title}, ${status.label}, by ${item.author?.login ?? "unknown author"}${review ? `, ${review.accessibleLabel}` : ""}${item.checksState ? `, checks ${item.checksState}` : ""}${item.stack ? `, stack ${item.stack.position} of ${item.stack.size}` : ""}${item.labels.length ? `, ${item.labels.map((label) => label.name).join(", ")}` : ""}, ${item.additions} additions, ${item.deletions} deletions`}
60 <View className="flex-row items-center gap-1.5">69 accessibilityHint="Double tap to open. Touch and hold for quick actions."
61 <PullRequestAvatar actor={item.author} size={16} />70 onPress={() => props.onSelect(item)}
62 <Text className="min-w-0 flex-1 text-xs text-foreground-muted" numberOfLines={1}>71 disabled={props.busy}
63 {item.repository} <Text className="text-foreground-tertiary">#{item.number}</Text>72 style={props.sidebar ? THREAD_LIST_V2_SIDEBAR_ROW_STYLE : undefined}
64 </Text>73 className={cn(
65 {item.stack ? (74 "flex-row gap-3 overflow-hidden",
66 <View className="flex-row items-center gap-1">75 !props.sidebar && "px-4 active:bg-row-hover",
67 <SymbolView76 props.sidebar && "bg-drawer",
68 name="square.3.layers.3d"77 props.sidebar && !props.selected && "active:bg-thread-hover",
69 size={12}78 props.selected && "bg-thread-selected",
70 tintColorClassName="accent-icon-muted"79 props.busy && "opacity-50",
71 />80 )}
72 <Text className="text-2xs tabular-nums text-foreground-muted">81 >
73 {item.stack.position}/{item.stack.size}82 <View className={props.sidebar ? "pt-0.5" : "pt-2.5"}>
74 </Text>83 <SymbolView name={status.icon} size={20} tintColorClassName={status.tint} />
75 </View>
76 ) : null}
77 <Text className="text-2xs tabular-nums text-foreground-tertiary">
78 {relativeTime(item.updatedAt)}
79 </Text>
80 </View>84 </View>
81 <Text85 <View
82 className="text-base leading-5 font-t3-medium text-foreground"86 className={cn(
83 numberOfLines={2}87 "min-w-0 flex-1 gap-1",
84 ellipsizeMode="tail"88 !props.sidebar && "border-b border-border-subtle py-2.5",
89 )}
85 >90 >
86 {item.title}91 <View className="flex-row items-center gap-1.5">
87 </Text>92 <PullRequestAvatar actor={item.author} size={16} />
88 <View className="flex-row items-center gap-1.5">93 <Text className={cn("min-w-0 flex-1 text-xs", mutedForeground)} numberOfLines={1}>
89 <View className="min-w-0 flex-1 flex-row items-center gap-1">94 {item.repository} <Text className={tertiaryForeground}>#{item.number}</Text>
90 {item.labels.slice(0, 2).map((label) => {95 </Text>
91 const color = pullRequestLabelColor(label.color);96 {item.stack ? (
92 return (97 <View className="flex-row items-center gap-1">
93 <View98 <SymbolView
94 key={label.name}99 name="square.3.layers.3d"
95 className="min-w-0 shrink flex-row items-center gap-1 rounded-md bg-subtle px-1.5 py-0.5"100 size={12}
96 style={{ maxWidth: 160, ...(color ? { backgroundColor: `${color}20` } : {}) }}101 tintColorClassName={
97 >102 props.selected
98 {color ? (103 ? selectedThreadRowColors.mutedIconTintClassName
99 <View104 : props.sidebar
100 style={{ width: 5, height: 5, borderRadius: 3, backgroundColor: color }}105 ? "accent-drawer-foreground-muted"
101 />106 : "accent-foreground-muted"
102 ) : null}107 }
103 <Text108 />
104 className="shrink text-2xs font-t3-medium text-foreground-muted"109 <Text className={cn("text-2xs tabular-nums", mutedForeground)}>
105 numberOfLines={1}110 {item.stack.position}/{item.stack.size}
106 >111 </Text>
107 {label.name}112 </View>
108 </Text>
109 </View>
110 );
111 })}
112 {item.labels.length > 2 ? (
113 <Text className="shrink-0 text-2xs text-foreground-tertiary">
114 +{item.labels.length - 2}
115 </Text>
116 ) : null}113 ) : null}
114 <Text className={cn("text-2xs tabular-nums", tertiaryForeground)}>
115 {relativeTime(item.updatedAt)}
116 </Text>
117 </View>117 </View>
118 {item.checksState ? (118 <Text
119 <View className="shrink-0" accessibilityLabel={`Checks ${item.checksState}`}>119 className={cn("text-base leading-5 font-t3-medium", foreground)}
120 <SymbolView120 numberOfLines={2}
121 name={121 ellipsizeMode="tail"
122 item.checksState === "passing"122 >
123 ? "checkmark.circle"123 {item.title}
124 : item.checksState === "failing"124 </Text>
125 ? "xmark.circle.fill"125 <View className="flex-row items-center gap-1.5">
126 : "clock"126 <View className="min-w-0 flex-1 flex-row items-center gap-1">
127 }127 {item.labels.slice(0, 2).map((label) => {
128 size={13}128 const color = pullRequestLabelColor(label.color);
129 tintColorClassName={checkTint}129 return (
130 />130 <View
131 </View>131 key={label.name}
132 ) : null}132 className="min-w-0 shrink flex-row items-center gap-1 rounded-md bg-subtle px-1.5 py-0.5"
133 {review ? (133 style={{
134 <View className="shrink-0" accessibilityLabel={review.accessibleLabel}>134 maxWidth: 160,
135 <SymbolView name={review.icon} size={13} tintColorClassName={review.tint} />135 ...(color ? { backgroundColor: `${color}20` } : {}),
136 }}
137 >
138 {color ? (
139 <View
140 style={{ width: 5, height: 5, borderRadius: 3, backgroundColor: color }}
141 />
142 ) : null}
143 <Text
144 className={cn("shrink text-2xs font-t3-medium", mutedForeground)}
145 numberOfLines={1}
146 >
147 {label.name}
148 </Text>
149 </View>
150 );
151 })}
152 {item.labels.length > 2 ? (
153 <Text className={cn("shrink-0 text-2xs", tertiaryForeground)}>
154 +{item.labels.length - 2}
155 </Text>
156 ) : null}
136 </View>157 </View>
137 ) : null}158 {item.checksState ? (
138 {item.additions || item.deletions ? (159 <View className="shrink-0" accessibilityLabel={`Checks ${item.checksState}`}>
139 <PullRequestDiffStats160 <SymbolView
140 additions={item.additions}161 name={
141 deletions={item.deletions}162 item.checksState === "passing"
142 compact163 ? "checkmark.circle"
143 />164 : item.checksState === "failing"
144 ) : null}165 ? "xmark.circle.fill"
166 : "clock"
167 }
168 size={13}
169 tintColorClassName={checkTint}
170 />
171 </View>
172 ) : null}
173 {review ? (
174 <View className="shrink-0" accessibilityLabel={review.accessibleLabel}>
175 <SymbolView name={review.icon} size={13} tintColorClassName={review.tint} />
176 </View>
177 ) : null}
178 {item.additions || item.deletions ? (
179 <PullRequestDiffStats
180 additions={item.additions}
181 deletions={item.deletions}
182 compact
183 />
184 ) : null}
185 </View>
145 </View>186 </View>
146 </View>187 </Pressable>
147 </Pressable>188 </ControlPillMenu>
148 </ControlPillMenu>189 </View>
149 );190 );
150 },191 },
151 (previous, next) =>192 (previous, next) =>
152 pullRequestListRowsAreEqual(previous.item, next.item) &&193 pullRequestListRowsAreEqual(previous.item, next.item) &&
153 previous.selected === next.selected &&194 previous.selected === next.selected &&
195 previous.sidebar === next.sidebar &&
154 previous.busy === next.busy &&196 previous.busy === next.busy &&
155 previous.onSelect === next.onSelect &&197 previous.onSelect === next.onSelect &&
156 previous.onAction === next.onAction &&198 previous.onAction === next.onAction &&
apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx+12-5
...@@ -53,7 +53,12 @@ function PullRequestScrollView(props: ScrollViewProps) {...@@ -53,7 +53,12 @@ function PullRequestScrollView(props: ScrollViewProps) {
53 );53 );
54}54}
5555
56export function PullRequestsScreen() {56export function PullRequestsScreen(
57 props: {
58 readonly sidebar?: boolean;
59 readonly selectedPullRequestKey?: string | null;
60 } = {},
61) {
57 const navigation = useNavigation();62 const navigation = useNavigation();
58 const primaryColumn = use(NativePrimaryColumnContext);63 const primaryColumn = use(NativePrimaryColumnContext);
59 const workspace = useWorkspaceTabs()!;64 const workspace = useWorkspaceTabs()!;
...@@ -135,7 +140,8 @@ export function PullRequestsScreen() {...@@ -135,7 +140,8 @@ export function PullRequestsScreen() {
135 connectedTargets.map((target) => target.environmentId),140 connectedTargets.map((target) => target.environmentId),
136 result.updateEntry,141 result.updateEntry,
137 );142 );
138 const selectedKey = primaryColumn?.selectedPullRequestKey;143 const sidebar = props.sidebar === true || primaryColumn !== null;
144 const selectedKey = props.selectedPullRequestKey ?? primaryColumn?.selectedPullRequestKey;
139 const entries = useMemo(145 const entries = useMemo(
140 () =>146 () =>
141 filterPullRequestList(147 filterPullRequestList(
...@@ -179,9 +185,10 @@ export function PullRequestsScreen() {...@@ -179,9 +185,10 @@ export function PullRequestsScreen() {
179 onAction={select}185 onAction={select}
180 busy={busyKey === pullRequestKey(item)}186 busy={busyKey === pullRequestKey(item)}
181 selected={selectedKey === pullRequestKey(item)}187 selected={selectedKey === pullRequestKey(item)}
188 sidebar={sidebar}
182 />189 />
183 ),190 ),
184 [selectPullRequest, selectedKey, actions, select, busyKey],191 [selectPullRequest, selectedKey, actions, select, busyKey, sidebar],
185 );192 );
186 const previousSearchMode = useRef(tab.searchMode);193 const previousSearchMode = useRef(tab.searchMode);
187 useEffect(() => {194 useEffect(() => {
...@@ -199,11 +206,11 @@ export function PullRequestsScreen() {...@@ -199,11 +206,11 @@ export function PullRequestsScreen() {
199 : -1;206 : -1;
200207
201 return (208 return (
202 <View className={primaryColumn ? "flex-1 bg-drawer" : "flex-1 bg-screen"}>209 <View className={sidebar ? "flex-1 bg-drawer" : "flex-1 bg-screen"}>
203 <LegendList210 <LegendList
204 ref={listRef}211 ref={listRef}
205 data={entries}212 data={entries}
206 extraData={`${primaryColumn?.selectedPullRequestKey}:${busyKey}`}213 extraData={`${selectedKey}:${busyKey}`}
207 keyExtractor={pullRequestKey}214 keyExtractor={pullRequestKey}
208 itemsAreEqual={pullRequestListRowsAreEqual}215 itemsAreEqual={pullRequestListRowsAreEqual}
209 // Default-size rows measure about 78pt with one title line and 98pt with two.216 // Default-size rows measure about 78pt with one title line and 98pt with two.
apps/mobile/src/features/pull-requests/pullRequestSelection.test.ts created+80
...@@ -0,0 +1,80 @@
1import { describe, expect, it } from "vite-plus/test";
2import type { NavigationState } from "@react-navigation/native";
3import { selectedPullRequestKey } from "./pullRequestSelection";
4
5const params = {
6 environmentId: "environment-one",
7 projectId: "project-one",
8 host: "GitHub.COM",
9 repository: "Org/Repo",
10 number: 42,
11};
12const home = { key: "home", name: "Home" };
13const pullRequest = { key: "pr", name: "PullRequest", params };
14function selection(routes: NavigationState["routes"], index = routes.length - 1) {
15 return selectedPullRequestKey({ routes, index });
16}
17
18describe("displayed pull request selection", () => {
19 it("normalizes repository and host while retaining the environment scope", () => {
20 expect(selection([home, pullRequest])).toBe("environment-one:github.com:org/repo:42");
21 expect(
22 selection([
23 home,
24 { ...pullRequest, params: { ...params, environmentId: "environment-two" } },
25 ]),
26 ).toBe("environment-two:github.com:org/repo:42");
27 });
28
29 it.each([
30 "PullRequestFile",
31 "PullRequestFiles",
32 "PullRequestGuideStop",
33 "PullRequestDiscussion",
34 "PullRequestReviewers",
35 "PullRequestReview",
36 "PullRequestInlineComment",
37 ])("keeps the owning selection on %s", (name) => {
38 expect(selection([home, pullRequest, { key: name, name, params }])).toBe(
39 "environment-one:github.com:org/repo:42",
40 );
41 });
42
43 it("uses the most recently opened PR, including a different hosting service", () => {
44 expect(
45 selection([
46 home,
47 pullRequest,
48 {
49 key: "other-pr",
50 name: "PullRequest",
51 params: { ...params, host: "github.enterprise", repository: "Other/Repo", number: 7 },
52 },
53 ]),
54 ).toBe("environment-one:github.enterprise:other/repo:7");
55 });
56
57 it.each(["Home", "Thread", "ThreadFiles"])("clears an older selection on %s", (name) => {
58 expect(selection([home, pullRequest, { key: "current", name }])).toBeNull();
59 });
60
61 it("ignores inactive routes after the navigation index", () => {
62 expect(selection([home, pullRequest], 0)).toBeNull();
63 });
64
65 it("retains the owning PR behind an unrelated overlay", () => {
66 expect(selection([home, pullRequest, { key: "settings", name: "SettingsSheet" }])).toBe(
67 "environment-one:github.com:org/repo:42",
68 );
69 });
70
71 it("does not highlight an older PR when the current PR lacks its hosting identity", () => {
72 expect(
73 selection([
74 home,
75 pullRequest,
76 { key: "unresolved", name: "PullRequest", params: { environmentId: "environment-one" } },
77 ]),
78 ).toBeNull();
79 });
80});
apps/mobile/src/features/pull-requests/pullRequestSelection.ts created+32
...@@ -0,0 +1,32 @@
1import type { NavigationState } from "@react-navigation/native";
2import { pullRequestListKey } from "./pullRequestList";
3
4/** Follow the displayed PR through its detail routes and sheets, not an older stack entry. */
5export function selectedPullRequestKey(state: Pick<NavigationState, "routes" | "index">) {
6 for (let index = state.index; index >= 0; index--) {
7 const route = state.routes[index];
8 if (!route) continue;
9 if (route.name === "Home" || route.name.startsWith("Thread")) return null;
10 if (!route.name.startsWith("PullRequest")) continue;
11 const params = route.params;
12 if (
13 params &&
14 "environmentId" in params &&
15 "host" in params &&
16 "repository" in params &&
17 "number" in params &&
18 typeof params.environmentId === "string" &&
19 typeof params.host === "string" &&
20 typeof params.repository === "string" &&
21 typeof params.number === "number"
22 ) {
23 return `${params.environmentId}:${pullRequestListKey({
24 host: params.host,
25 repository: params.repository,
26 number: params.number,
27 })}`;
28 }
29 if (route.name === "PullRequest") return null;
30 }
31 return null;
32}
apps/mobile/src/features/threads/thread-list-v2-row-appearance.android.ts+14-8
...@@ -4,6 +4,17 @@ import type { MobileThemeVariables } from "../../lib/mobileTheme";...@@ -4,6 +4,17 @@ import type { MobileThemeVariables } from "../../lib/mobileTheme";
4export const THREAD_LIST_V2_MONO_FONT = "monospace";4export const THREAD_LIST_V2_MONO_FONT = "monospace";
5export const THREAD_LIST_V2_ROW_CONTENT_CLASS_NAME = "px-3 py-2.5";5export const THREAD_LIST_V2_ROW_CONTENT_CLASS_NAME = "px-3 py-2.5";
6export const THREAD_LIST_V2_ROW_DIVIDERS = false;6export const THREAD_LIST_V2_ROW_DIVIDERS = false;
7export const THREAD_LIST_V2_SIDEBAR_ROW_STYLE = {
8 borderRadius: 20,
9 paddingHorizontal: 12,
10 paddingVertical: 10,
11} satisfies ViewStyle;
12export const THREAD_LIST_V2_SIDEBAR_ROW_CONTAINER_STYLE = {
13 borderRadius: 20,
14 overflow: "hidden",
15 marginHorizontal: 8,
16 marginVertical: 2,
17} satisfies ViewStyle;
718
8export const selectedThreadRowColors = {19export const selectedThreadRowColors = {
9 foregroundClassName: "text-thread-selected-foreground",20 foregroundClassName: "text-thread-selected-foreground",
...@@ -25,14 +36,9 @@ export function getThreadListV2RowAppearance(...@@ -25,14 +36,9 @@ export function getThreadListV2RowAppearance(
25 const backgroundColor = theme[sidebarPane ? "--color-drawer" : "--color-screen"];36 const backgroundColor = theme[sidebarPane ? "--color-drawer" : "--color-screen"];
26 const style: ViewStyle = {37 const style: ViewStyle = {
27 backgroundColor: selected ? selectedBackgroundColor : backgroundColor,38 backgroundColor: selected ? selectedBackgroundColor : backgroundColor,
28 borderRadius: 20,39 borderRadius: THREAD_LIST_V2_SIDEBAR_ROW_STYLE.borderRadius,
29 };
30 const swipeContainerStyle: ViewStyle = {
31 borderRadius: 20,
32 overflow: "hidden",
33 marginHorizontal: 8,
34 marginVertical: 2,
35 };40 };
41 const swipeContainerStyle = THREAD_LIST_V2_SIDEBAR_ROW_CONTAINER_STYLE;
3642
37 return {43 return {
38 className: undefined,44 className: undefined,
...@@ -52,7 +58,7 @@ export function getThreadListV2RowAppearance(...@@ -52,7 +58,7 @@ export function getThreadListV2RowAppearance(
52 ? "accent-drawer-foreground-muted"58 ? "accent-drawer-foreground-muted"
53 : "accent-foreground-tertiary",59 : "accent-foreground-tertiary",
54 style,60 style,
55 cardStyle: sidebarPane ? { ...style, paddingHorizontal: 12, paddingVertical: 10 } : style,61 cardStyle: sidebarPane ? { ...style, ...THREAD_LIST_V2_SIDEBAR_ROW_STYLE } : style,
56 swipeContainerStyle,62 swipeContainerStyle,
57 swipeBackgroundColor: backgroundColor,63 swipeBackgroundColor: backgroundColor,
58 providerIconSurfaceColor: selected ? selectedBackgroundColor : backgroundColor,64 providerIconSurfaceColor: selected ? selectedBackgroundColor : backgroundColor,
apps/mobile/src/features/threads/thread-list-v2-row-appearance.ts+12-3
...@@ -4,6 +4,15 @@ import type { MobileThemeVariables } from "../../lib/mobileTheme";...@@ -4,6 +4,15 @@ import type { MobileThemeVariables } from "../../lib/mobileTheme";
4export const THREAD_LIST_V2_MONO_FONT = "Menlo";4export const THREAD_LIST_V2_MONO_FONT = "Menlo";
5export const THREAD_LIST_V2_ROW_CONTENT_CLASS_NAME = "px-5 py-2.5";5export const THREAD_LIST_V2_ROW_CONTENT_CLASS_NAME = "px-5 py-2.5";
6export const THREAD_LIST_V2_ROW_DIVIDERS = true;6export const THREAD_LIST_V2_ROW_DIVIDERS = true;
7export const THREAD_LIST_V2_SIDEBAR_ROW_STYLE = {
8 borderRadius: 12,
9 paddingHorizontal: 12,
10 paddingVertical: 10,
11} satisfies ViewStyle;
12export const THREAD_LIST_V2_SIDEBAR_ROW_CONTAINER_STYLE = {
13 borderRadius: 12,
14 overflow: "hidden",
15} satisfies ViewStyle;
716
8export const selectedThreadRowColors = {17export const selectedThreadRowColors = {
9 foregroundClassName: "text-thread-selected-foreground",18 foregroundClassName: "text-thread-selected-foreground",
...@@ -25,11 +34,11 @@ export function getThreadListV2RowAppearance(...@@ -25,11 +34,11 @@ export function getThreadListV2RowAppearance(
25 const style: ViewStyle | undefined = sidebarPane34 const style: ViewStyle | undefined = sidebarPane
26 ? {35 ? {
27 backgroundColor: selected ? selectedBackgroundColor : theme["--color-drawer"],36 backgroundColor: selected ? selectedBackgroundColor : theme["--color-drawer"],
28 borderRadius: 12,37 borderRadius: THREAD_LIST_V2_SIDEBAR_ROW_STYLE.borderRadius,
29 }38 }
30 : undefined;39 : undefined;
31 const swipeContainerStyle: ViewStyle | undefined = sidebarPane40 const swipeContainerStyle: ViewStyle | undefined = sidebarPane
32 ? { borderRadius: 12, overflow: "hidden" }41 ? THREAD_LIST_V2_SIDEBAR_ROW_CONTAINER_STYLE
33 : undefined;42 : undefined;
3443
35 return {44 return {
...@@ -50,7 +59,7 @@ export function getThreadListV2RowAppearance(...@@ -50,7 +59,7 @@ export function getThreadListV2RowAppearance(
50 ? "accent-drawer-foreground-muted"59 ? "accent-drawer-foreground-muted"
51 : "accent-foreground-tertiary",60 : "accent-foreground-tertiary",
52 style,61 style,
53 cardStyle: sidebarPane ? { ...style, paddingHorizontal: 12, paddingVertical: 10 } : undefined,62 cardStyle: sidebarPane ? { ...style, ...THREAD_LIST_V2_SIDEBAR_ROW_STYLE } : undefined,
54 swipeContainerStyle,63 swipeContainerStyle,
55 swipeBackgroundColor: theme[sidebarPane ? "--color-drawer" : "--color-screen"],64 swipeBackgroundColor: theme[sidebarPane ? "--color-drawer" : "--color-screen"],
56 // Provider badges blend into the surface beneath them.65 // Provider badges blend into the surface beneath them.
apps/mobile/src/features/workspace/AndroidWorkspaceSidebar.android.tsx+2-1
...@@ -8,6 +8,7 @@ import { WorkspaceTabsContext, useWorkspaceTabs } from "./WorkspaceTabsContext";...@@ -8,6 +8,7 @@ import { WorkspaceTabsContext, useWorkspaceTabs } from "./WorkspaceTabsContext";
8export function AndroidWorkspaceSidebar(props: {8export function AndroidWorkspaceSidebar(props: {
9 readonly children: ReactNode;9 readonly children: ReactNode;
10 readonly onRequestVisibility: () => void;10 readonly onRequestVisibility: () => void;
11 readonly selectedPullRequestKey: string | null;
11}) {12}) {
12 const workspace = useWorkspaceTabs()!;13 const workspace = useWorkspaceTabs()!;
13 const context = useMemo(() => ({ ...workspace, bottomClearance: 16 }), [workspace]);14 const context = useMemo(() => ({ ...workspace, bottomClearance: 16 }), [workspace]);
...@@ -18,7 +19,7 @@ export function AndroidWorkspaceSidebar(props: {...@@ -18,7 +19,7 @@ export function AndroidWorkspaceSidebar(props: {
18 <View className="flex-1 bg-header">19 <View className="flex-1 bg-header">
19 <MaterialPullRequestToolbar onRequestVisibility={props.onRequestVisibility} />20 <MaterialPullRequestToolbar onRequestVisibility={props.onRequestVisibility} />
20 <MaterialScreenContent insetHorizontal>21 <MaterialScreenContent insetHorizontal>
21 <PullRequestsScreen />22 <PullRequestsScreen sidebar selectedPullRequestKey={props.selectedPullRequestKey} />
22 </MaterialScreenContent>23 </MaterialScreenContent>
23 </View>24 </View>
24 </WorkspaceTabsContext>25 </WorkspaceTabsContext>
apps/mobile/src/features/workspace/AndroidWorkspaceSidebar.tsx+1
...@@ -3,6 +3,7 @@ import type { ReactNode } from "react";...@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
3export function AndroidWorkspaceSidebar(props: {3export function AndroidWorkspaceSidebar(props: {
4 readonly children: ReactNode;4 readonly children: ReactNode;
5 readonly onRequestVisibility: () => void;5 readonly onRequestVisibility: () => void;
6 readonly selectedPullRequestKey: string | null;
6}) {7}) {
7 return props.children;8 return props.children;
8}9}