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 @@
11import type { ComponentProps } from "react";
2import { Platform, ScrollView } from "react-native";
2import { Platform } from "react-native";
3import { NativeScrollView as ScrollView } from "../native/NativeScrollViewMarker";
34
45/** Keeps forms and settings readable inside a wide pane while its surface fills the screen. */
56export 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";
55import { useRef, useState } from "react";
66import { ActivityIndicator, Platform, ScrollView, View } from "react-native";
77import { KeyboardAvoidingView, useKeyboardState } from "react-native-keyboard-controller";
8import { ScrollViewMarker } from "react-native-screens";
8import { NativeScrollViewMarker as ScrollViewMarker } from "../../native/NativeScrollViewMarker";
99import { useSafeAreaInsets } from "react-native-safe-area-context";
1010import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
1111import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
......@@ -153,7 +153,7 @@ function Discussion({ params }: { params: PullRequestParams & { readonly discuss
153153 />
154154 </NativeHeaderToolbar>
155155 ) : null}
156 <ScrollViewMarker style={{ flex: 1 }} scrollEdgeEffects={{ top: "soft", bottom: "soft" }}>
156 <ScrollViewMarker style={{ flex: 1 }}>
157157 <ScrollView
158158 className="flex-1"
159159 contentInsetAdjustmentBehavior="automatic"
apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx+2-2
......@@ -52,7 +52,7 @@ import {
5252 StyleSheet,
5353 View,
5454} from "react-native";
55import { ScrollViewMarker } from "react-native-screens";
55import { NativeScrollViewMarker as ScrollViewMarker } from "../../native/NativeScrollViewMarker";
5656import { useSafeAreaInsets } from "react-native-safe-area-context";
5757
5858import { AppText as Text } from "../../components/AppText";
......@@ -115,7 +115,7 @@ const EMPTY_IDS: readonly string[] = [];
115115function PullRequestTabScrollView(props: ScrollViewProps) {
116116 const insets = useSafeAreaInsets();
117117 return (
118 <ScrollViewMarker style={{ flex: 1 }} scrollEdgeEffects={{ top: "soft", bottom: "soft" }}>
118 <ScrollViewMarker style={{ flex: 1 }}>
119119 <ScrollView
120120 {...props}
121121 automaticallyAdjustContentInsets={false}
apps/mobile/src/features/settings/SettingsFollowUpRouteScreen.tsx+2-1
......@@ -1,7 +1,8 @@
1import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
12import { useAtomSet, useAtomValue } from "@effect/atom-react";
23import { useNavigation } from "@react-navigation/native";
34import { AsyncResult } from "effect/reactivity";
4import { Platform, ScrollView, View } from "react-native";
5import { Platform, View } from "react-native";
56import { useSafeAreaInsets } from "react-native-safe-area-context";
67
78import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
apps/mobile/src/features/settings/SettingsKeyboardRouteScreen.tsx+2-1
......@@ -1,7 +1,8 @@
1import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
12import { useAtomSet, useAtomValue } from "@effect/atom-react";
23import { useNavigation } from "@react-navigation/native";
34import { AsyncResult } from "effect/reactivity";
4import { Platform, ScrollView, View } from "react-native";
5import { Platform, View } from "react-native";
56import { useSafeAreaInsets } from "react-native-safe-area-context";
67
78import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
apps/mobile/src/features/settings/SettingsMicrophoneRouteScreen.tsx+2-1
......@@ -1,8 +1,9 @@
1import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
12import { useAtomSet, useAtomValue } from "@effect/atom-react";
23import { AsyncResult } from "effect/reactivity";
34import { getRecordingPermissionsAsync, requestRecordingPermissionsAsync } from "expo-audio";
45import { useCallback, useEffect, useState } from "react";
5import { AppState, Linking, Pressable, ScrollView, View } from "react-native";
6import { AppState, Linking, Pressable, View } from "react-native";
67import Reanimated, { ReduceMotion, useAnimatedStyle, withTiming } from "react-native-reanimated";
78import { 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 {
2323import { useCallback, useRef, useState, type ComponentProps } from "react";
2424import { StyleSheet, View } from "react-native";
2525import { FormSheet, Stack } from "react-native-screens";
26import { NativeScrollEdgeEffectsContext } from "./NativeScrollViewMarker";
2627import { V5StackHeader } from "./V5StackHeader.ios";
2728import type { AppNativeStackNavigationOptions } from "./StackHeader";
2829import { NativeColumnContent } from "./NativeColumnContent.ios";
......@@ -243,7 +244,9 @@ export function V5CardStackView(
243244 .nativeContentInsetHorizontally
244245 }
245246 >
246 {descriptor.render()}
247 <NativeScrollEdgeEffectsContext value={descriptor.options.scrollEdgeEffects}>
248 {descriptor.render()}
249 </NativeScrollEdgeEffectsContext>
247250 </NativeColumnContent>
248251 </NavigationRouteContext>
249252 </NavigationContext>