apps/mobile/src/native/NativeScrollViewMarker.tsx
import { requireNativeView, requireOptionalNativeModule } from "expo";
import { createContext, use, type ComponentProps } from "react";
import { Platform, ScrollView, type ViewProps } from "react-native";
// oxlint-disable-next-line eslint/no-restricted-imports -- The wrapper every screen must use.
import { ScrollViewMarker } from "react-native-screens";
import { nativeHeaderScrollEdgeEffects } from "./scrollEdgeEffects";
type ScrollEdgeEffects = ComponentProps<typeof ScrollViewMarker>["scrollEdgeEffects"];
const DEFAULT_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
export const NativeScrollEdgeEffectsContext = createContext<ScrollEdgeEffects>(undefined);
function resolveScrollEdgeBleed() {
try {
const module = requireOptionalNativeModule<{
readonly ViewPrototypes?: { readonly T3NativeControls_ScrollEdgeBleed?: unknown };
}>("T3NativeControls");
if (!module?.ViewPrototypes?.T3NativeControls_ScrollEdgeBleed) return null;
return requireNativeView<ViewProps>("T3NativeControls", "ScrollEdgeBleed");
} catch {
return null;
}
}
// Widens the scroll view under the side safe areas its column pads away (the Duo's vertical bar),
// so the header's scroll-edge blur, which UIKit draws inside the scroll view, reaches the bar
// while the content stays in the safe area. Every header scroll view comes through here.
const ScrollEdgeBleed = Platform.OS === "ios" ? resolveScrollEdgeBleed() : null;
/** Registers a screen's scroll view with its header. The only way to do so: see the lint rule. */
export function NativeScrollViewMarker(props: ComponentProps<typeof ScrollViewMarker>) {
const effects = use(NativeScrollEdgeEffectsContext);
const marker = (
<ScrollViewMarker
{...props}
{...(ScrollEdgeBleed ? { style: { flex: 1 } } : {})}
scrollEdgeEffects={props.scrollEdgeEffects ?? effects ?? DEFAULT_SCROLL_EDGE_EFFECTS}
/>
);
return ScrollEdgeBleed ? <ScrollEdgeBleed style={props.style}>{marker}</ScrollEdgeBleed> : marker;
}
export function NativeScrollView(props: ComponentProps<typeof ScrollView>) {
const scrollView = <ScrollView {...props} />;
return Platform.OS === "ios" ? (
<NativeScrollViewMarker style={{ flex: 1 }}>{scrollView}</NativeScrollViewMarker>
) : (
scrollView
);
}