authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-11 22:09:05-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-11 22:09:05-07:00
log6f6197c4657f9e2e44a7e8f2fa759d00a15c1cbd
treef0596f4ddaa8b8d85e106627a00f4c38982370d5
parentd514a8c6176ace9057dc8c7619f0b5f0d81d1bd0
signature Signed by PGP key B5690EEEBB952194

fix(mobile): restore native scroll-edge effects on forms and PRs (#18507)


8 files changed, 47 insertions(+), 9 deletions(-)

apps/mobile/src/components/ScreenScrollView.tsx+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import type { ComponentProps } from "react";1import type { ComponentProps } from "react";
2import { Platform, ScrollView } from "react-native";2import { Platform } from "react-native";
3import { NativeScrollView as ScrollView } from "../native/NativeScrollViewMarker";
34
4/** Keeps forms and settings readable inside a wide pane while its surface fills the screen. */5/** Keeps forms and settings readable inside a wide pane while its surface fills the screen. */
5export function ScreenScrollView(props: ComponentProps<typeof ScrollView>) {6export function ScreenScrollView(props: ComponentProps<typeof ScrollView>) {
apps/mobile/src/features/pull-requests/PullRequestDiscussionScreen.tsx+2-2
...@@ -5,7 +5,7 @@ import type { PullRequestThreadComment } from "@t3tools/contracts";...@@ -5,7 +5,7 @@ import type { PullRequestThreadComment } from "@t3tools/contracts";
5import { useRef, useState } from "react";5import { useRef, useState } from "react";
6import { ActivityIndicator, Platform, ScrollView, View } from "react-native";6import { ActivityIndicator, Platform, ScrollView, View } from "react-native";
7import { KeyboardAvoidingView, useKeyboardState } from "react-native-keyboard-controller";7import { KeyboardAvoidingView, useKeyboardState } from "react-native-keyboard-controller";
8import { ScrollViewMarker } from "react-native-screens";8import { NativeScrollViewMarker as ScrollViewMarker } from "../../native/NativeScrollViewMarker";
9import { useSafeAreaInsets } from "react-native-safe-area-context";9import { useSafeAreaInsets } from "react-native-safe-area-context";
10import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";10import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
11import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";11import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
...@@ -153,7 +153,7 @@ function Discussion({ params }: { params: PullRequestParams & { readonly discuss...@@ -153,7 +153,7 @@ function Discussion({ params }: { params: PullRequestParams & { readonly discuss
153 />153 />
154 </NativeHeaderToolbar>154 </NativeHeaderToolbar>
155 ) : null}155 ) : null}
156 <ScrollViewMarker style={{ flex: 1 }} scrollEdgeEffects={{ top: "soft", bottom: "soft" }}>156 <ScrollViewMarker style={{ flex: 1 }}>
157 <ScrollView157 <ScrollView
158 className="flex-1"158 className="flex-1"
159 contentInsetAdjustmentBehavior="automatic"159 contentInsetAdjustmentBehavior="automatic"
apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx+2-2
...@@ -52,7 +52,7 @@ import {...@@ -52,7 +52,7 @@ import {
52 StyleSheet,52 StyleSheet,
53 View,53 View,
54} from "react-native";54} from "react-native";
55import { ScrollViewMarker } from "react-native-screens";55import { NativeScrollViewMarker as ScrollViewMarker } from "../../native/NativeScrollViewMarker";
56import { useSafeAreaInsets } from "react-native-safe-area-context";56import { useSafeAreaInsets } from "react-native-safe-area-context";
5757
58import { AppText as Text } from "../../components/AppText";58import { AppText as Text } from "../../components/AppText";
...@@ -115,7 +115,7 @@ const EMPTY_IDS: readonly string[] = [];...@@ -115,7 +115,7 @@ const EMPTY_IDS: readonly string[] = [];
115function PullRequestTabScrollView(props: ScrollViewProps) {115function PullRequestTabScrollView(props: ScrollViewProps) {
116 const insets = useSafeAreaInsets();116 const insets = useSafeAreaInsets();
117 return (117 return (
118 <ScrollViewMarker style={{ flex: 1 }} scrollEdgeEffects={{ top: "soft", bottom: "soft" }}>118 <ScrollViewMarker style={{ flex: 1 }}>
119 <ScrollView119 <ScrollView
120 {...props}120 {...props}
121 automaticallyAdjustContentInsets={false}121 automaticallyAdjustContentInsets={false}
apps/mobile/src/features/settings/SettingsFollowUpRouteScreen.tsx+2-1
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
1import { useAtomSet, useAtomValue } from "@effect/atom-react";2import { useAtomSet, useAtomValue } from "@effect/atom-react";
2import { useNavigation } from "@react-navigation/native";3import { useNavigation } from "@react-navigation/native";
3import { AsyncResult } from "effect/reactivity";4import { AsyncResult } from "effect/reactivity";
4import { Platform, ScrollView, View } from "react-native";5import { Platform, View } from "react-native";
5import { useSafeAreaInsets } from "react-native-safe-area-context";6import { useSafeAreaInsets } from "react-native-safe-area-context";
67
7import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";8import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
apps/mobile/src/features/settings/SettingsKeyboardRouteScreen.tsx+2-1
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
1import { useAtomSet, useAtomValue } from "@effect/atom-react";2import { useAtomSet, useAtomValue } from "@effect/atom-react";
2import { useNavigation } from "@react-navigation/native";3import { useNavigation } from "@react-navigation/native";
3import { AsyncResult } from "effect/reactivity";4import { AsyncResult } from "effect/reactivity";
4import { Platform, ScrollView, View } from "react-native";5import { Platform, View } from "react-native";
5import { useSafeAreaInsets } from "react-native-safe-area-context";6import { useSafeAreaInsets } from "react-native-safe-area-context";
67
7import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";8import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
apps/mobile/src/features/settings/SettingsMicrophoneRouteScreen.tsx+2-1
...@@ -1,8 +1,9 @@...@@ -1,8 +1,9 @@
1import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
1import { useAtomSet, useAtomValue } from "@effect/atom-react";2import { useAtomSet, useAtomValue } from "@effect/atom-react";
2import { AsyncResult } from "effect/reactivity";3import { AsyncResult } from "effect/reactivity";
3import { getRecordingPermissionsAsync, requestRecordingPermissionsAsync } from "expo-audio";4import { getRecordingPermissionsAsync, requestRecordingPermissionsAsync } from "expo-audio";
4import { useCallback, useEffect, useState } from "react";5import { useCallback, useEffect, useState } from "react";
5import { AppState, Linking, Pressable, ScrollView, View } from "react-native";6import { AppState, Linking, Pressable, View } from "react-native";
6import Reanimated, { ReduceMotion, useAnimatedStyle, withTiming } from "react-native-reanimated";7import Reanimated, { ReduceMotion, useAnimatedStyle, withTiming } from "react-native-reanimated";
7import { useSafeAreaInsets } from "react-native-safe-area-context";8import { useSafeAreaInsets } from "react-native-safe-area-context";
89
apps/mobile/src/native/NativeScrollViewMarker.tsx created+31
...@@ -0,0 +1,31 @@
1import { createContext, use, type ComponentProps } from "react";
2import { Platform, ScrollView } from "react-native";
3import { ScrollViewMarker } from "react-native-screens";
4import { nativeHeaderScrollEdgeEffects } from "./scrollEdgeEffects";
5
6type ScrollEdgeEffects = ComponentProps<typeof ScrollViewMarker>["scrollEdgeEffects"];
7
8const DEFAULT_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
9
10export const NativeScrollEdgeEffectsContext = createContext<ScrollEdgeEffects>(undefined);
11
12/** Register the content scroll view and apply its route's native edge settings. */
13export function NativeScrollViewMarker(props: ComponentProps<typeof ScrollViewMarker>) {
14 const effects = use(NativeScrollEdgeEffectsContext);
15 return (
16 <ScrollViewMarker
17 {...props}
18 scrollEdgeEffects={props.scrollEdgeEffects ?? effects ?? DEFAULT_SCROLL_EDGE_EFFECTS}
19 />
20 );
21}
22
23/** Primary screen scrolling; Android keeps its existing native view hierarchy. */
24export function NativeScrollView(props: ComponentProps<typeof ScrollView>) {
25 const scrollView = <ScrollView {...props} />;
26 return Platform.OS === "ios" ? (
27 <NativeScrollViewMarker style={{ flex: 1 }}>{scrollView}</NativeScrollViewMarker>
28 ) : (
29 scrollView
30 );
31}
apps/mobile/src/native/createV5StackNavigator.ios.tsx+4-1
...@@ -23,6 +23,7 @@ import {...@@ -23,6 +23,7 @@ import {
23import { useCallback, useRef, useState, type ComponentProps } from "react";23import { useCallback, useRef, useState, type ComponentProps } from "react";
24import { StyleSheet, View } from "react-native";24import { StyleSheet, View } from "react-native";
25import { FormSheet, Stack } from "react-native-screens";25import { FormSheet, Stack } from "react-native-screens";
26import { NativeScrollEdgeEffectsContext } from "./NativeScrollViewMarker";
26import { V5StackHeader } from "./V5StackHeader.ios";27import { V5StackHeader } from "./V5StackHeader.ios";
27import type { AppNativeStackNavigationOptions } from "./StackHeader";28import type { AppNativeStackNavigationOptions } from "./StackHeader";
28import { NativeColumnContent } from "./NativeColumnContent.ios";29import { NativeColumnContent } from "./NativeColumnContent.ios";
...@@ -243,7 +244,9 @@ export function V5CardStackView(...@@ -243,7 +244,9 @@ export function V5CardStackView(
243 .nativeContentInsetHorizontally244 .nativeContentInsetHorizontally
244 }245 }
245 >246 >
246 {descriptor.render()}247 <NativeScrollEdgeEffectsContext value={descriptor.options.scrollEdgeEffects}>
248 {descriptor.render()}
249 </NativeScrollEdgeEffectsContext>
247 </NativeColumnContent>250 </NativeColumnContent>
248 </NavigationRouteContext>251 </NavigationRouteContext>
249 </NavigationContext>252 </NavigationContext>