authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-11 22:30:20-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-12 05:30:20+00:00
log462e7f4de4fd4ae14d2b4439fd983fa9ced2790e
treee0c39b9fb454859797f5b150b276086777848c06
parent6f6197c4657f9e2e44a7e8f2fa759d00a15c1cbd
signature Signed by PGP key B5690EEEBB952194

feat(mobile): native-sized pull request action bar (#18527)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

8 files changed, 29 insertions(+), 63 deletions(-)

apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz deleted
Binary files a/apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz and /dev/null differ
apps/mobile/deps/react-native-screens-5.0.0-t3.27.tgz created
Binary files /dev/null and b/apps/mobile/deps/react-native-screens-5.0.0-t3.27.tgz differ
apps/mobile/package.json+1-1
......@@ -126,7 +126,7 @@
126126 "react-native-keyboard-controller": "1.22.6",
127127 "react-native-reanimated": "4.7.0",
128128 "react-native-safe-area-context": "~5.9.1",
129 "react-native-screens": "file:deps/react-native-screens-5.0.0-t3.26.tgz",
129 "react-native-screens": "file:deps/react-native-screens-5.0.0-t3.27.tgz",
130130 "react-native-shiki-engine": "^0.3.12",
131131 "react-native-svg": "15.15.5",
132132 "react-native-webview": "14.0.1",
apps/mobile/src/features/pull-requests/PullRequestControls.tsx+15-32
......@@ -4,7 +4,7 @@ import { useNavigation } from "@react-navigation/native";
44import type { PullRequestDetail } from "@t3tools/contracts";
55import type { ReactNode } from "react";
66import { Platform, Pressable, View } from "react-native";
7import { useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-context";
7import { useSafeAreaInsets } from "react-native-safe-area-context";
88import { AppText as Text } from "../../components/AppText";
99import { SymbolView, type AppSymbolName } from "../../components/AppSymbol";
1010import { MaterialButton } from "../../components/MaterialButton";
......@@ -12,10 +12,7 @@ import { MaterialIconButton } from "../../components/MaterialIconButton";
1212import { MaterialFloatingToolbar } from "./MaterialFloatingToolbar";
1313import { ControlPillMenu } from "../../components/ControlPillMenu";
1414import { NativeHeaderToolbar } from "../../native/StackHeader";
15import {
16 useNativeColumnLayoutMetrics,
17 useNativeLayoutMetrics,
18} from "../../native/native-layout-metrics";
15import { useNativeLayoutMetrics } from "../../native/native-layout-metrics";
1916import { type PullRequestParams } from "./usePullRequestDiff";
2017import { usePullRequestPermissions } from "./usePullRequestPermissions";
2118import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
......@@ -117,8 +114,6 @@ export function PullRequestActionToolbar(props: {
117114 const { draft } = usePullRequestReviewDraft(props.params);
118115 const { themeVariables: colors } = useAppearancePreferences();
119116 const insets = useSafeAreaInsets();
120 const frame = useSafeAreaFrame();
121 const columnMetrics = useNativeColumnLayoutMetrics();
122117 const nativeMetrics = useNativeLayoutMetrics();
123118 const verticalControls = nativeMetrics !== null && nativeMetrics.verticalBarEdge !== "none";
124119 const permissions = usePullRequestPermissions(props.params, props.pr);
......@@ -140,20 +135,6 @@ export function PullRequestActionToolbar(props: {
140135 if (!props.pr) return null;
141136 const primaryDisabled = pendingReview ? false : agent.disabled;
142137 const showPrimary = pendingReview || canOpenThread;
143 // Match the native phone tab controls; the side rail keeps intrinsic sizing.
144 const controlSize = verticalControls ? undefined : 60;
145 // Reserve the native outer margins, custom-button padding, and item gaps.
146 // UIKit needs this space to keep every action out of its overflow menu.
147 // Duo's side rail uses an intrinsic icon item instead.
148 const sideControls = Number(showAgentControls) + 1;
149 const primaryWidth = verticalControls
150 ? undefined
151 : Math.max(
152 0,
153 (columnMetrics?.width ?? frame.width - insets.left - insets.right) -
154 58 -
155 sideControls * (60 + 10 + 16),
156 );
157138 if (Platform.OS === "android") {
158139 const primary = (
159140 <MaterialButton
......@@ -230,8 +211,6 @@ export function PullRequestActionToolbar(props: {
230211 title={primaryLabel}
231212 accessibilityLabel={primaryLabel}
232213 variant="prominent"
233 width={primaryWidth}
234 height={controlSize}
235214 icon={
236215 verticalControls
237216 ? pendingReview
......@@ -251,8 +230,6 @@ export function PullRequestActionToolbar(props: {
251230 label={primaryLabel}
252231 accessibilityLabel={primaryLabel}
253232 variant="prominent"
254 width={primaryWidth}
255 height={controlSize}
256233 icon={
257234 verticalControls
258235 ? pendingReview
......@@ -270,8 +247,6 @@ export function PullRequestActionToolbar(props: {
270247 const agentControl = showAgentControls ? (
271248 <NativeHeaderToolbar.Menu
272249 identifier="pull-request-agent"
273 width={controlSize}
274 height={controlSize}
275250 icon="sparkles"
276251 title="AI helpers"
277252 accessibilityLabel="PR agent actions"
......@@ -310,19 +285,27 @@ export function PullRequestActionToolbar(props: {
310285 const filesControl = (
311286 <NativeHeaderToolbar.Button
312287 identifier="workspace-trailing-action"
313 width={controlSize}
314 height={controlSize}
315288 icon={{ type: "image", source: require("../../../assets/icons/file-diff.png") }}
316289 accessibilityLabel="Changed files"
317290 onPress={openFiles}
318291 separateBackground
319292 />
320293 );
294 if (verticalControls)
295 return (
296 <NativeHeaderToolbar placement="bottom">
297 {filesControl}
298 {agentControl}
299 {primaryControl}
300 </NativeHeaderToolbar>
301 );
321302 return (
322303 <NativeHeaderToolbar placement="bottom">
323 {verticalControls ? filesControl : agentControl}
324 {verticalControls ? agentControl : primaryControl}
325 {verticalControls ? primaryControl : filesControl}
304 {agentControl}
305 <NativeHeaderToolbar.Spacer flexible />
306 {primaryControl}
307 <NativeHeaderToolbar.Spacer flexible />
308 {filesControl}
326309 </NativeHeaderToolbar>
327310 );
328311}
apps/mobile/src/native/NativeHeaderToolbar.tsx+1-11
......@@ -41,9 +41,7 @@ type NativeStackHeaderAxisBehavior = Extract<
4141 NativeStackHeaderItem,
4242 { type: "button" }
4343>["axisBehavior"];
44type NativeToolbarItem =
45 | (NativeStackHeaderItem & { readonly height?: number })
46 | HeaderBarButtonSearchBarPlacementItem;
44type NativeToolbarItem = NativeStackHeaderItem | HeaderBarButtonSearchBarPlacementItem;
4745type NativeStackOptionsWithToolbar = NativeStackNavigationOptions & {
4846 unstable_headerToolbarItems?: () => NativeToolbarItem[];
4947};
......@@ -162,8 +160,6 @@ function convertToolbarChild(child: ReactNode): NativeToolbarItem | null {
162160 : () => undefined,
163161 sharesBackground: !child.props.separateBackground,
164162 tintColor: child.props.tintColor as ColorValue | undefined,
165 width: typeof child.props.width === "number" ? child.props.width : undefined,
166 height: typeof child.props.height === "number" ? child.props.height : undefined,
167163 variant:
168164 child.props.variant === "done" || child.props.variant === "prominent"
169165 ? child.props.variant
......@@ -189,8 +185,6 @@ function convertToolbarChild(child: ReactNode): NativeToolbarItem | null {
189185 },
190186 sharesBackground: !child.props.separateBackground,
191187 tintColor: child.props.tintColor as ColorValue | undefined,
192 width: typeof child.props.width === "number" ? child.props.width : undefined,
193 height: typeof child.props.height === "number" ? child.props.height : undefined,
194188 variant:
195189 child.props.variant === "done" || child.props.variant === "prominent"
196190 ? child.props.variant
......@@ -289,8 +283,6 @@ function NativeHeaderToolbarButton(_props: {
289283 readonly tintColor?: ColorValue;
290284 readonly variant?: "plain" | "done" | "prominent";
291285 readonly identifier?: string;
292 readonly width?: number;
293 readonly height?: number;
294286}) {
295287 return null;
296288}
......@@ -308,8 +300,6 @@ function NativeHeaderToolbarMenu(_props: {
308300 readonly tintColor?: ColorValue;
309301 readonly title?: string;
310302 readonly variant?: "plain" | "done" | "prominent";
311 readonly width?: number;
312 readonly height?: number;
313303}) {
314304 return null;
315305}
apps/mobile/src/native/V5StackHeader.ios.tsx+2-9
......@@ -21,7 +21,7 @@ import {
2121
2222type HeaderItem = NonNullable<NonNullable<StackHeaderConfigProps["ios"]>["leadingItems"]>[number];
2323type ToolbarItem =
24 | (NativeStackHeaderItem & { readonly height?: number })
24 | NativeStackHeaderItem
2525 | HeaderBarButtonMailSearchToolbarItem
2626 | HeaderBarButtonSearchBarPlacementItem;
2727type Options = NativeStackNavigationOptions & {
......@@ -91,10 +91,7 @@ function convertMenu(menu: MenuInput, id: string): StackHeaderMenuIOS {
9191 return { id, type: "menu", title: menu.title, children };
9292}
9393
94function convertItems(
95 items: (NativeStackHeaderItem & { readonly height?: number })[],
96 prefix: string,
97): HeaderItem[] {
94function convertItems(items: NativeStackHeaderItem[], prefix: string): HeaderItem[] {
9895 return items.map((item, index): HeaderItem => {
9996 const id =
10097 item.type === "spacing" ? `${prefix}:${index}` : (item.identifier ?? `${prefix}:${index}`);
......@@ -121,8 +118,6 @@ function convertItems(
121118 menu: item.disabled ? undefined : convertMenu(item.menu, `${id}:menu`),
122119 variant: item.variant,
123120 tintColor: item.tintColor,
124 width: item.width,
125 height: item.height,
126121 hidesSharedBackground: item.hidesSharedBackground,
127122 };
128123 case "button":
......@@ -136,8 +131,6 @@ function convertItems(
136131 onPress: item.disabled ? undefined : item.onPress,
137132 variant: item.variant,
138133 tintColor: item.tintColor,
139 width: item.width,
140 height: item.height,
141134 hidesSharedBackground: item.hidesSharedBackground,
142135 };
143136 }
docs/operations/mobile-screens-fork.md+1-1
......@@ -1,7 +1,7 @@
11# Building the mobile Screens dependency
22
33The mobile v5 stack uses a committed tarball from the
4[T3 Screens fork](https://github.com/pingdotgg/react-native-screens/tree/t3-v5.0.0-t3.26).
4[T3 Screens fork](https://github.com/pingdotgg/react-native-screens/tree/t3-v5.0.0-t3.27).
55The fork owns the integration changes; the app does not patch this package.
66Each archive includes compiled JavaScript, declarations, native source and
77`t3-fork.json` recording its version, source commit and upstream base.
pnpm-lock.yaml+9-9
......@@ -348,7 +348,7 @@ importers:
348348 version: 7.3.4(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)
349349 '@react-navigation/native-stack':
350350 specifier: 7.17.6
351 version: 7.17.6(patch_hash=d2d52f3776e30765519748fd5e177592dc3b9cc778c52fb00e02674dfa9042c5)(02486f76aa3d9c7d88119682fcbddcde)
351 version: 7.17.6(patch_hash=d2d52f3776e30765519748fd5e177592dc3b9cc778c52fb00e02674dfa9042c5)(6b53c4f789a319cd2c00cd296dcea732)
352352 '@shikijs/core':
353353 specifier: 4.2.0
354354 version: 4.2.0
......@@ -548,8 +548,8 @@ importers:
548548 specifier: ~5.9.1
549549 version: 5.9.1(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)
550550 react-native-screens:
551 specifier: file:deps/react-native-screens-5.0.0-t3.26.tgz
552 version: file:apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)
551 specifier: file:deps/react-native-screens-5.0.0-t3.27.tgz
552 version: file:apps/mobile/deps/react-native-screens-5.0.0-t3.27.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)
553553 react-native-shiki-engine:
554554 specifier: ^0.3.12
555555 version: 0.3.12(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)
......@@ -10891,9 +10891,9 @@ packages:
1089110891 react: '*'
1089210892 react-native: '*'
1089310893
10894 react-native-screens@file:apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz:
10895 resolution: {integrity: sha512-OD1yeZu1HKuvBaroReihFROaofzuF2w4ORtPJXJrpAGk8AgQNusy3KaRRpeiCwIzH2KQU52SnWxrtKaYMzzQ+A==, tarball: file:apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz}
10896 version: 5.0.0-t3.26
10894 react-native-screens@file:apps/mobile/deps/react-native-screens-5.0.0-t3.27.tgz:
10895 resolution: {integrity: sha512-krNMVmduM2fXeJYveadSz6NkYI/AFN3hAAirjk42Z9wkBrPtuhmcp3E2+Me95Xdj5LBL3G3dgltUjo5vYeEtLw==, tarball: file:apps/mobile/deps/react-native-screens-5.0.0-t3.27.tgz}
10896 version: 5.0.0-t3.27
1089710897 peerDependencies:
1089810898 react: '*'
1089910899 react-native: '*'
......@@ -16096,7 +16096,7 @@ snapshots:
1609616096 optionalDependencies:
1609716097 '@react-native-masked-view/masked-view': 0.3.2(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)
1609816098
16099 '@react-navigation/native-stack@7.17.6(patch_hash=d2d52f3776e30765519748fd5e177592dc3b9cc778c52fb00e02674dfa9042c5)(02486f76aa3d9c7d88119682fcbddcde)':
16099 '@react-navigation/native-stack@7.17.6(patch_hash=d2d52f3776e30765519748fd5e177592dc3b9cc778c52fb00e02674dfa9042c5)(6b53c4f789a319cd2c00cd296dcea732)':
1610016100 dependencies:
1610116101 '@react-navigation/elements': 2.9.26(83de68753219ecf7f3321e065bd378be)
1610216102 '@react-navigation/native': 7.3.4(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)
......@@ -16104,7 +16104,7 @@ snapshots:
1610416104 react: 19.3.0
1610516105 react-native: 0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6)
1610616106 react-native-safe-area-context: 5.9.1(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)
16107 react-native-screens: file:apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)
16107 react-native-screens: file:apps/mobile/deps/react-native-screens-5.0.0-t3.27.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)
1610816108 sf-symbols-typescript: 2.2.0
1610916109 warn-once: 0.1.1
1611016110 transitivePeerDependencies:
......@@ -22097,7 +22097,7 @@ snapshots:
2209722097 react: 19.3.0
2209822098 react-native: 0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6)
2209922099
22100 react-native-screens@file:apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0):
22100 react-native-screens@file:apps/mobile/deps/react-native-screens-5.0.0-t3.27.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0):
2210122101 dependencies:
2210222102 react: 19.3.0
2210322103 react-freeze: 1.0.4(react@19.3.0)