From dff812bcd8316e0233a22d505b9ceab1c1c5be57 Mon Sep 17 00:00:00 2001 From: wSedlacek Date: Fri, 6 Nov 2026 18:06:21 -0800 Subject: [PATCH 1/2] fix(mobile): PR details use one native section tab control --- .../src/components/ui/SegmentedControl.tsx | 2 +- .../pull-requests/PullRequestRouteScreen.tsx | 105 +++++++++++++----- 2 files changed, 79 insertions(+), 28 deletions(-) diff --git a/apps/mobile/src/components/ui/SegmentedControl.tsx b/apps/mobile/src/components/ui/SegmentedControl.tsx index 15883d300da41..7064ad95fc143 100644 --- a/apps/mobile/src/components/ui/SegmentedControl.tsx +++ b/apps/mobile/src/components/ui/SegmentedControl.tsx @@ -48,7 +48,7 @@ export function SegmentedControl( const option = props.options[nativeEvent.index]; if (option) props.onSelect(option.value); }} - style={{ height: props.size === "compact" ? 32 : 36 }} + style={{ height: props.size === "compact" ? 32 : 36, width: "100%" }} /> ); diff --git a/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx b/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx index f78cd9caa58cd..dd36fa58f01e5 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx @@ -12,7 +12,7 @@ import { import { PullRequestActivityFilters } from "./PullRequestActivityFilters"; import type { LegendListRef } from "@legendapp/list/react-native"; import { AnimatedLegendList } from "@legendapp/list/reanimated"; -import Animated from "react-native-reanimated"; +import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated"; import { PR_ROW_LAYOUT, PR_ROW_ENTER, @@ -124,6 +124,9 @@ import { } from "@t3tools/client-runtime/source-control-clients"; const EMPTY_IDS: readonly string[] = []; +// Matches SegmentedControl's default height so the list spacer and overlay stay aligned. +const PR_SECTION_CONTROL_HEIGHT = Platform.OS === "android" ? 40 : 36; +const PR_LIST_CONTENT_TOP_PADDING = 8; function usePullRequestActivityRows(params: PullRequestParams, filter: ActivityFilter) { const navigation = useNavigation(); @@ -355,9 +358,27 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps>>({}); + // One native control stays mounted outside the opacity-hidden tabs (Usage-style), so Liquid + // Glass selection never remounts mid-gesture. Its top tracks the active tab's scroll. + const sectionControlTop = useSharedValue(0); + const sectionControlStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: sectionControlTop.value }], + })); + const syncSectionControlTop = (scrollY: number) => { + sectionControlTop.set( + PR_LIST_CONTENT_TOP_PADDING + + sectionControlReadStatusHeight.current + + sectionControlYInHeader.current - + scrollY, + ); + }; // AnimatedLegendList re-attaches its handle and sets state whenever its ref changes, // so an inline ref in the tab map loops the screen into "Maximum update depth exceeded". const [listRefs] = useState(() => { @@ -383,6 +404,7 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps { const offset = event.nativeEvent.contentOffset.y; scrollOffsets.current[tab] = offset; - if (shown(requestedSection.current) === tab) + if (shown(requestedSection.current) === tab) { currentScrollOffset.current = { tab, offset }; + syncSectionControlTop(offset); + } }} + scrollEventThrottle={16} className="flex-1" renderScrollComponent={Platform.OS === "ios" ? NativeScrollView : undefined} contentInsetAdjustmentBehavior="never" @@ -636,7 +661,7 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps - + { + sectionControlReadStatusHeight.current = event.nativeEvent.layout.height; + syncSectionControlTop(currentScrollOffset.current.offset); + }} + > + + {identity ? ( - {/* UIKit draws the tapped control's glass lens outside the hidden - tab's opacity, so only the visible tab mounts the control. */} - {Platform.OS !== "ios" || section === tab ? ( - - ) : ( - - )} + {/* Spacer only: the real control is overlaid outside opacity:0 tabs so + UIKit's glass lens stays on one always-mounted native control. */} + { + sectionControlYInHeader.current = event.nativeEvent.layout.y; + syncSectionControlTop(currentScrollOffset.current.offset); + setSectionControlReady(true); + }} + /> ) : null} @@ -801,6 +824,34 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps ))} + {identity && sectionControlReady ? ( + + + + + + ) : null} Date: Fri, 6 Nov 2026 18:38:30 -0800 Subject: [PATCH 2/2] fix(mobile): keep PR section control aligned when guides remap --- .../pull-requests/PullRequestRouteScreen.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx b/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx index dd36fa58f01e5..437f08d352843 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx @@ -379,6 +379,20 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps { + const offset = scrollOffsets.current[section] ?? -contentTopInset; + currentScrollOffset.current = { tab: section, offset }; + sectionControlTop.set( + PR_LIST_CONTENT_TOP_PADDING + + sectionControlReadStatusHeight.current + + sectionControlYInHeader.current - + offset, + ); + }, [section, contentTopInset, sectionControlTop]); // AnimatedLegendList re-attaches its handle and sets state whenever its ref changes, // so an inline ref in the tab map loops the screen into "Maximum update depth exceeded". const [listRefs] = useState(() => { @@ -825,7 +839,10 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps ))} {identity && sectionControlReady ? ( - +