replace react-nav header hack with scroll edge interaction module
- new expo-scroll-edge-interaction native module (iOS-only) that sets up UIScrollEdgeElementContainerInteraction on iOS 26+ - new ScrollEdgeInteractionProvider context and hooks - Layout.Screen gains `transparentHeader` prop - Layout.Header.Outer auto-detects transparent mode from context - List auto-registers scroll view and applies content inset Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"platforms": ["ios"],
|
||||||
|
"ios": {
|
||||||
|
"modules": ["ExpoScrollEdgeInteractionModule"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export type {ScrollEdgeInteractionEdge} from './src/ExpoScrollEdgeInteraction.types'
|
||||||
|
export {ExpoScrollEdgeInteractionView} from './src/ExpoScrollEdgeInteractionView'
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'ExpoScrollEdgeInteraction'
|
||||||
|
s.version = '1.0.0'
|
||||||
|
s.summary = 'UIScrollEdgeElementContainerInteraction for React Native views'
|
||||||
|
s.description = 'UIScrollEdgeElementContainerInteraction for React Native views'
|
||||||
|
s.author = 'bluesky-social'
|
||||||
|
s.homepage = 'https://github.com/bluesky-social/social-app'
|
||||||
|
s.platforms = { :ios => '13.4', :tvos => '13.4' }
|
||||||
|
s.source = { git: '' }
|
||||||
|
s.static_framework = true
|
||||||
|
|
||||||
|
s.dependency 'ExpoModulesCore'
|
||||||
|
|
||||||
|
# Swift/Objective-C compatibility
|
||||||
|
s.pod_target_xcconfig = {
|
||||||
|
'DEFINES_MODULE' => 'YES',
|
||||||
|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||||
|
}
|
||||||
|
|
||||||
|
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
||||||
|
end
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import ExpoModulesCore
|
||||||
|
|
||||||
|
public class ExpoScrollEdgeInteractionModule: Module {
|
||||||
|
public func definition() -> ModuleDefinition {
|
||||||
|
Name("ExpoScrollEdgeInteraction")
|
||||||
|
|
||||||
|
View(ExpoScrollEdgeInteractionView.self) {
|
||||||
|
Prop("nodeHandle") { (view: ExpoScrollEdgeInteractionView, prop: Int?) in
|
||||||
|
view.nodeHandle = prop
|
||||||
|
}
|
||||||
|
Prop("scrollViewTag") { (view: ExpoScrollEdgeInteractionView, prop: Int?) in
|
||||||
|
view.scrollViewTag = prop
|
||||||
|
}
|
||||||
|
Prop("edge") { (view: ExpoScrollEdgeInteractionView, prop: String?) in
|
||||||
|
view.edge = prop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import ExpoModulesCore
|
||||||
|
import React
|
||||||
|
|
||||||
|
class ExpoScrollEdgeInteractionView: ExpoView {
|
||||||
|
var nodeHandle: Int? {
|
||||||
|
didSet {
|
||||||
|
setupInteraction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollViewTag: Int? {
|
||||||
|
didSet {
|
||||||
|
setupInteraction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var edge: String? {
|
||||||
|
didSet {
|
||||||
|
setupInteraction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentInteraction: NSObject?
|
||||||
|
|
||||||
|
private func setupInteraction() {
|
||||||
|
guard #available(iOS 26.0, *) else { return }
|
||||||
|
|
||||||
|
// Clean up old interaction
|
||||||
|
if let interaction = currentInteraction as? UIScrollEdgeElementContainerInteraction,
|
||||||
|
let headerView = findHeaderView() {
|
||||||
|
headerView.removeInteraction(interaction)
|
||||||
|
currentInteraction = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let nodeHandle = nodeHandle,
|
||||||
|
let scrollViewTag = scrollViewTag
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
guard let headerView = appContext?.findView(withTag: nodeHandle, ofType: UIView.self) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RCTScrollView wraps a UIScrollView — get the inner one
|
||||||
|
let scrollView: UIScrollView?
|
||||||
|
if let rctScrollView = appContext?.findView(withTag: scrollViewTag, ofType: RCTScrollView.self) {
|
||||||
|
scrollView = rctScrollView.scrollView
|
||||||
|
} else {
|
||||||
|
scrollView = appContext?.findView(withTag: scrollViewTag, ofType: UIScrollView.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let scrollView = scrollView else { return }
|
||||||
|
|
||||||
|
let interaction = UIScrollEdgeElementContainerInteraction()
|
||||||
|
interaction.edge = resolveEdge()
|
||||||
|
interaction.scrollView = scrollView
|
||||||
|
headerView.addInteraction(interaction)
|
||||||
|
currentInteraction = interaction
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolveEdge() -> NSDirectionalRectEdge {
|
||||||
|
switch edge {
|
||||||
|
case "bottom": return .bottom
|
||||||
|
case "left": return .leading
|
||||||
|
case "right": return .trailing
|
||||||
|
default: return .top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findHeaderView() -> UIView? {
|
||||||
|
guard let nodeHandle = nodeHandle else { return nil }
|
||||||
|
return appContext?.findView(withTag: nodeHandle, ofType: UIView.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import {type ViewStyle} from 'react-native'
|
||||||
|
|
||||||
|
export type ScrollEdgeInteractionEdge = 'top' | 'bottom' | 'left' | 'right'
|
||||||
|
|
||||||
|
export interface ExpoScrollEdgeInteractionViewProps {
|
||||||
|
nodeHandle: number | null
|
||||||
|
scrollViewTag: number | null
|
||||||
|
edge?: ScrollEdgeInteractionEdge
|
||||||
|
children: React.ReactNode
|
||||||
|
style?: ViewStyle
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import {requireNativeViewManager} from 'expo-modules-core'
|
||||||
|
|
||||||
|
import {type ExpoScrollEdgeInteractionViewProps} from './ExpoScrollEdgeInteraction.types'
|
||||||
|
|
||||||
|
const NativeView: React.ComponentType<ExpoScrollEdgeInteractionViewProps> =
|
||||||
|
requireNativeViewManager('ExpoScrollEdgeInteraction')
|
||||||
|
|
||||||
|
export function ExpoScrollEdgeInteractionView({
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: ExpoScrollEdgeInteractionViewProps) {
|
||||||
|
return <NativeView {...rest}>{children}</NativeView>
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import {type ExpoScrollEdgeInteractionViewProps} from './ExpoScrollEdgeInteraction.types'
|
||||||
|
|
||||||
|
export function ExpoScrollEdgeInteractionView({
|
||||||
|
children,
|
||||||
|
}: React.PropsWithChildren<ExpoScrollEdgeInteractionViewProps>) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import {useLayoutEffect, useMemo, useState} from 'react'
|
import {View} from 'react-native'
|
||||||
import {useWindowDimensions, View} from 'react-native'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
|
||||||
|
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {atoms as a, useGutters} from '#/alf'
|
||||||
import {atoms as a} from '#/alf'
|
import {useTransparentHeaderProps} from '#/components/Layout/ScrollEdgeInteraction'
|
||||||
import {IS_LIQUID_GLASS} from '#/env'
|
|
||||||
import {Outer as DefaultOuter, type OuterProps} from './index.shared'
|
import {Outer as DefaultOuter, type OuterProps} from './index.shared'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -18,68 +16,46 @@ export {
|
|||||||
} from './index.shared'
|
} from './index.shared'
|
||||||
|
|
||||||
export function Outer(props: OuterProps) {
|
export function Outer(props: OuterProps) {
|
||||||
if (IS_LIQUID_GLASS && props.transparent) {
|
const transparentHeaderProps = useTransparentHeaderProps()
|
||||||
return <TransparentOuter {...props} />
|
if (transparentHeaderProps) {
|
||||||
|
return (
|
||||||
|
<TransparentOuter
|
||||||
|
transparentHeaderProps={transparentHeaderProps}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <DefaultOuter {...props} />
|
return <DefaultOuter {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function TransparentOuter({children, headerRef}: OuterProps) {
|
function TransparentOuter({
|
||||||
const [headerWidth, setHeaderWidth] = useState(0)
|
children,
|
||||||
const {width: screenWidth} = useWindowDimensions()
|
transparentHeaderProps,
|
||||||
|
}: OuterProps & {
|
||||||
|
transparentHeaderProps: {ref: (node: View | null) => void; onLayout: any}
|
||||||
|
}) {
|
||||||
|
const {top} = useSafeAreaInsets()
|
||||||
|
const gutters = useGutters([0, 'base'])
|
||||||
|
|
||||||
// bit of a hack - react-native-screens initially renders the header too wide
|
|
||||||
// so let's delay showing it until the padding has been applied -sfn
|
|
||||||
const isInitialRender = headerWidth === 0 || headerWidth === screenWidth
|
|
||||||
const headerElement = useMemo(() => {
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
ref={headerRef}
|
collapsable={false}
|
||||||
onLayout={evt => setHeaderWidth(evt.nativeEvent.layout.width)}
|
ref={transparentHeaderProps.ref}
|
||||||
|
onLayout={transparentHeaderProps.onLayout}
|
||||||
style={[
|
style={[
|
||||||
a.flex_1,
|
a.absolute,
|
||||||
|
a.top_0,
|
||||||
|
a.left_0,
|
||||||
|
a.right_0,
|
||||||
|
a.z_10,
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.gap_sm,
|
a.gap_sm,
|
||||||
// built-in padding, but slightly more on right than left,
|
gutters,
|
||||||
// so compensate for that. this is because we're using
|
{paddingTop: top, minHeight: 48 + top},
|
||||||
// headerRightItems
|
a.pb_xs,
|
||||||
a.pl_xs,
|
|
||||||
isInitialRender && {opacity: 0},
|
|
||||||
]}>
|
]}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}, [children, headerRef, isInitialRender])
|
|
||||||
|
|
||||||
// this is how expo-router handles it
|
|
||||||
// https://github.com/expo/expo/blob/main/packages/expo-router/src/views/Screen.tsx#L34
|
|
||||||
// note: I'm skipping handling preloading for now -sfn
|
|
||||||
const navigation = useNavigation<NavigationProp>()
|
|
||||||
const isFocused = useIsFocused()
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (isFocused) {
|
|
||||||
navigation.setOptions({
|
|
||||||
headerShown: true,
|
|
||||||
// abuse the headerItems API so we get
|
|
||||||
// the sweet sweet progressive blur header.
|
|
||||||
// unclear why just `header: () => elem` doesn't work -sfn
|
|
||||||
unstable_headerRightItems: () => [
|
|
||||||
{
|
|
||||||
type: 'custom',
|
|
||||||
element: headerElement,
|
|
||||||
hidesSharedBackground: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
headerTransparent: true,
|
|
||||||
headerBackVisible: false,
|
|
||||||
scrollEdgeEffects: {
|
|
||||||
top: 'soft',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [isFocused, navigation, headerElement])
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,22 +35,6 @@ export type OuterProps = {
|
|||||||
noBottomBorder?: boolean
|
noBottomBorder?: boolean
|
||||||
headerRef?: React.RefObject<View | null>
|
headerRef?: React.RefObject<View | null>
|
||||||
sticky?: boolean
|
sticky?: boolean
|
||||||
/**
|
|
||||||
* Use native transparent blurred header on iOS 26 (looks very nice)
|
|
||||||
*
|
|
||||||
* When using this, make sure to enable these props on the scrollview for the screen:
|
|
||||||
* ```tsx
|
|
||||||
* <Layout.Content
|
|
||||||
* contentInsetAdjustmentBehavior="automatic"
|
|
||||||
* automaticallyAdjustsScrollIndicatorInsets
|
|
||||||
* // everything else
|
|
||||||
* />
|
|
||||||
* ```
|
|
||||||
* and also set `noInsetTop={IS_LIQUID_GLASS}` on `Layout.Screen`
|
|
||||||
*
|
|
||||||
* @platform ios
|
|
||||||
* */
|
|
||||||
transparent?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Outer({
|
export function Outer({
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import {createContext, useContext, useRef, useState} from 'react'
|
||||||
|
import {findNodeHandle, type LayoutChangeEvent, type View} from 'react-native'
|
||||||
|
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {IS_LIQUID_GLASS} from '#/env'
|
||||||
|
import {
|
||||||
|
ExpoScrollEdgeInteractionView,
|
||||||
|
type ScrollEdgeInteractionEdge,
|
||||||
|
} from '../../../modules/expo-scroll-edge-interaction'
|
||||||
|
|
||||||
|
interface ScrollEdgeContextValue {
|
||||||
|
setNodeHandle: (update: React.SetStateAction<number | null>) => void
|
||||||
|
setScrollViewTag: (update: React.SetStateAction<number | null>) => void
|
||||||
|
setHeaderHeight: (height: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScrollEdgeContext = createContext<ScrollEdgeContextValue | null>(null)
|
||||||
|
const ScrollEdgeHeightContext = createContext<number | null>(null)
|
||||||
|
|
||||||
|
export function ScrollEdgeInteractionProvider({
|
||||||
|
children,
|
||||||
|
edge = 'top',
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
edge?: ScrollEdgeInteractionEdge
|
||||||
|
}) {
|
||||||
|
const [nodeHandle, setNodeHandle] = useState<number | null>(null)
|
||||||
|
const [scrollViewTag, setScrollViewTag] = useState<number | null>(null)
|
||||||
|
const [headerHeight, setHeaderHeight] = useState(0)
|
||||||
|
|
||||||
|
if (!IS_LIQUID_GLASS) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollEdgeContext.Provider
|
||||||
|
value={{setNodeHandle, setScrollViewTag, setHeaderHeight}}>
|
||||||
|
<ScrollEdgeHeightContext.Provider value={headerHeight}>
|
||||||
|
<ExpoScrollEdgeInteractionView
|
||||||
|
nodeHandle={nodeHandle}
|
||||||
|
scrollViewTag={scrollViewTag}
|
||||||
|
edge={edge}
|
||||||
|
style={a.flex_1}>
|
||||||
|
{children}
|
||||||
|
</ExpoScrollEdgeInteractionView>
|
||||||
|
</ScrollEdgeHeightContext.Provider>
|
||||||
|
</ScrollEdgeContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a ref callback to attach to the header view. The native module
|
||||||
|
* will add a UIScrollEdgeElementContainerInteraction to this view.
|
||||||
|
* Returns undefined when there's no provider (non-Liquid Glass).
|
||||||
|
*/
|
||||||
|
export function useTransparentHeaderProps() {
|
||||||
|
const ctx = useContext(ScrollEdgeContext)
|
||||||
|
// Track which handle this specific hook instance set, so that
|
||||||
|
// unmounting one header doesn't clobber another's handle.
|
||||||
|
const ourHandle = useRef<number | null>(null)
|
||||||
|
|
||||||
|
if (!ctx) return undefined
|
||||||
|
|
||||||
|
const {setNodeHandle, setHeaderHeight} = ctx
|
||||||
|
|
||||||
|
const refCallback = (node: View | null) => {
|
||||||
|
const handle = node ? findNodeHandle(node) : null
|
||||||
|
if (handle !== null) {
|
||||||
|
ourHandle.current = handle
|
||||||
|
setNodeHandle(handle)
|
||||||
|
} else {
|
||||||
|
// Only clear if we were the last one to set it
|
||||||
|
const prev = ourHandle.current
|
||||||
|
ourHandle.current = null
|
||||||
|
setNodeHandle(cur => (cur === prev ? null : cur))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLayout = (e: LayoutChangeEvent) => {
|
||||||
|
setHeaderHeight(e.nativeEvent.layout.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {ref: refCallback, onLayout}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a scroll view with the scroll edge interaction provider.
|
||||||
|
* Called automatically by List.
|
||||||
|
* Returns a ref callback, or undefined if no provider.
|
||||||
|
*/
|
||||||
|
export function useScrollEdgeScrollView() {
|
||||||
|
const ctx = useContext(ScrollEdgeContext)
|
||||||
|
const ourTag = useRef<number | null>(null)
|
||||||
|
|
||||||
|
if (!ctx) return undefined
|
||||||
|
|
||||||
|
const {setScrollViewTag} = ctx
|
||||||
|
|
||||||
|
return (node: any) => {
|
||||||
|
const tag = node ? findNodeHandle(node) : null
|
||||||
|
if (tag !== null) {
|
||||||
|
ourTag.current = tag
|
||||||
|
setScrollViewTag(tag)
|
||||||
|
} else {
|
||||||
|
const prev = ourTag.current
|
||||||
|
ourTag.current = null
|
||||||
|
setScrollViewTag(cur => (cur === prev ? null : cur))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the measured header height, or null if not inside a
|
||||||
|
* transparent header provider.
|
||||||
|
*/
|
||||||
|
export function useTransparentHeaderHeight(): number | null {
|
||||||
|
return useContext(ScrollEdgeHeightContext)
|
||||||
|
}
|
||||||
@@ -22,32 +22,63 @@ import {
|
|||||||
import {useDialogContext} from '#/components/Dialog'
|
import {useDialogContext} from '#/components/Dialog'
|
||||||
import {CENTER_COLUMN_OFFSET, SCROLLBAR_OFFSET} from '#/components/Layout/const'
|
import {CENTER_COLUMN_OFFSET, SCROLLBAR_OFFSET} from '#/components/Layout/const'
|
||||||
import {ScrollbarOffsetContext} from '#/components/Layout/context'
|
import {ScrollbarOffsetContext} from '#/components/Layout/context'
|
||||||
|
import {ScrollEdgeInteractionProvider} from '#/components/Layout/ScrollEdgeInteraction'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
|
|
||||||
export * from '#/components/Layout/const'
|
export * from '#/components/Layout/const'
|
||||||
export * as Header from '#/components/Layout/Header'
|
export * as Header from '#/components/Layout/Header'
|
||||||
|
export {useTransparentHeaderHeight} from '#/components/Layout/ScrollEdgeInteraction'
|
||||||
|
|
||||||
export type ScreenProps = React.ComponentProps<typeof View> & {
|
export type ScreenProps = React.ComponentProps<typeof View> & {
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
noInsetTop?: boolean
|
noInsetTop?: boolean
|
||||||
|
/**
|
||||||
|
* Enables a transparent, absolutely-positioned header with the iOS 26
|
||||||
|
* Liquid Glass scroll-edge blur effect. When set, wraps screen children
|
||||||
|
* in a `ScrollEdgeInteractionProvider`, implies `noInsetTop`, and causes
|
||||||
|
* `Layout.Header.Outer` to render as a transparent overlay.
|
||||||
|
*
|
||||||
|
* The blur effect only works with `List` (native FlatList), not with
|
||||||
|
* `Layout.Content` (Animated.ScrollView). `List` automatically picks up
|
||||||
|
* the header height and applies `contentInset`.
|
||||||
|
*
|
||||||
|
* On non-Liquid-Glass devices this is a no-op.
|
||||||
|
*
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
transparentHeader?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outermost component of every screen
|
* Outermost component of every screen
|
||||||
*/
|
*/
|
||||||
export const Screen = memo(function Screen({
|
export const Screen = memo(function Screen({
|
||||||
|
children,
|
||||||
style,
|
style,
|
||||||
noInsetTop,
|
noInsetTop,
|
||||||
|
transparentHeader,
|
||||||
...props
|
...props
|
||||||
}: ScreenProps) {
|
}: ScreenProps) {
|
||||||
const {top} = useSafeAreaInsets()
|
const {top} = useSafeAreaInsets()
|
||||||
|
const skipInsetTop = noInsetTop || transparentHeader
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{IS_WEB && <WebCenterBorders />}
|
{IS_WEB && <WebCenterBorders />}
|
||||||
<View
|
<View
|
||||||
style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
|
style={[
|
||||||
{...props}
|
a.util_screen_outer,
|
||||||
/>
|
{paddingTop: skipInsetTop ? 0 : top},
|
||||||
|
style,
|
||||||
|
]}
|
||||||
|
{...props}>
|
||||||
|
{transparentHeader ? (
|
||||||
|
<ScrollEdgeInteractionProvider>
|
||||||
|
{children}
|
||||||
|
</ScrollEdgeInteractionProvider>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function MessagesListHeader({
|
|||||||
}, [moderation])
|
}, [moderation])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Header.Outer transparent>
|
<Layout.Header.Outer>
|
||||||
<View style={[a.flex_1, a.flex_row, a.gap_xs, a.align_start]}>
|
<View style={[a.flex_1, a.flex_row, a.gap_xs, a.align_start]}>
|
||||||
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
|
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
|
||||||
<Layout.Header.BackButton />
|
<Layout.Header.BackButton />
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function MessagesConversationScreenInner({route}: Props) {
|
|||||||
<Layout.Screen
|
<Layout.Screen
|
||||||
testID="convoScreen"
|
testID="convoScreen"
|
||||||
style={web([{minHeight: 0}, a.flex_1])}
|
style={web([{minHeight: 0}, a.flex_1])}
|
||||||
noInsetTop={IS_LIQUID_GLASS}>
|
transparentHeader={IS_LIQUID_GLASS}>
|
||||||
<ConvoProvider key={convoId} convoId={convoId}>
|
<ConvoProvider key={convoId} convoId={convoId}>
|
||||||
<Inner />
|
<Inner />
|
||||||
</ConvoProvider>
|
</ConvoProvider>
|
||||||
|
|||||||
@@ -6,14 +6,20 @@ import {
|
|||||||
useAnimatedScrollHandler,
|
useAnimatedScrollHandler,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video'
|
import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video'
|
||||||
|
|
||||||
import {useDedupe} from '#/lib/hooks/useDedupe'
|
import {useDedupe} from '#/lib/hooks/useDedupe'
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
|
import {mergeRefs} from '#/lib/merge-refs'
|
||||||
import {useScrollHandlers} from '#/lib/ScrollContext'
|
import {useScrollHandlers} from '#/lib/ScrollContext'
|
||||||
import {addStyle} from '#/lib/styles'
|
import {addStyle} from '#/lib/styles'
|
||||||
import {useLightbox} from '#/state/lightbox'
|
import {useLightbox} from '#/state/lightbox'
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
|
import {
|
||||||
|
useScrollEdgeScrollView,
|
||||||
|
useTransparentHeaderHeight,
|
||||||
|
} from '#/components/Layout/ScrollEdgeInteraction'
|
||||||
import {IS_IOS} from '#/env'
|
import {IS_IOS} from '#/env'
|
||||||
import {FlatList_INTERNAL} from './Views'
|
import {FlatList_INTERNAL} from './Views'
|
||||||
|
|
||||||
@@ -54,6 +60,7 @@ let List = forwardRef<ListMethods, ListProps>(
|
|||||||
headerOffset,
|
headerOffset,
|
||||||
style,
|
style,
|
||||||
progressViewOffset,
|
progressViewOffset,
|
||||||
|
contentInset,
|
||||||
automaticallyAdjustsScrollIndicatorInsets = false,
|
automaticallyAdjustsScrollIndicatorInsets = false,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
@@ -63,6 +70,13 @@ let List = forwardRef<ListMethods, ListProps>(
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const dedupe = useDedupe(400)
|
const dedupe = useDedupe(400)
|
||||||
const scrollsToTop = useAllowScrollToTop()
|
const scrollsToTop = useAllowScrollToTop()
|
||||||
|
const transparentHeaderRef = useScrollEdgeScrollView()
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const transparentHeaderHeight = useTransparentHeaderHeight()
|
||||||
|
const isHeaderTransparent = transparentHeaderHeight != null
|
||||||
|
const headerHeight = isHeaderTransparent
|
||||||
|
? transparentHeaderHeight - insets.top
|
||||||
|
: undefined
|
||||||
|
|
||||||
const handleScrolledDownChange = useNonReactiveCallback(
|
const handleScrolledDownChange = useNonReactiveCallback(
|
||||||
(didScrollDown: boolean) => {
|
(didScrollDown: boolean) => {
|
||||||
@@ -163,10 +177,18 @@ let List = forwardRef<ListMethods, ListProps>(
|
|||||||
automaticallyAdjustsScrollIndicatorInsets
|
automaticallyAdjustsScrollIndicatorInsets
|
||||||
}
|
}
|
||||||
scrollIndicatorInsets={{
|
scrollIndicatorInsets={{
|
||||||
top: headerOffset,
|
top: headerHeight || headerOffset,
|
||||||
right: 1,
|
right: 1,
|
||||||
...props.scrollIndicatorInsets,
|
...props.scrollIndicatorInsets,
|
||||||
}}
|
}}
|
||||||
|
contentInset={
|
||||||
|
headerHeight
|
||||||
|
? {
|
||||||
|
...contentInset,
|
||||||
|
top: headerHeight,
|
||||||
|
}
|
||||||
|
: contentInset
|
||||||
|
}
|
||||||
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
||||||
contentOffset={contentOffset}
|
contentOffset={contentOffset}
|
||||||
refreshControl={refreshControl}
|
refreshControl={refreshControl}
|
||||||
@@ -175,7 +197,9 @@ let List = forwardRef<ListMethods, ListProps>(
|
|||||||
scrollEventThrottle={1}
|
scrollEventThrottle={1}
|
||||||
style={style}
|
style={style}
|
||||||
// @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn
|
// @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn
|
||||||
ref={ref}
|
ref={
|
||||||
|
transparentHeaderRef ? mergeRefs([ref, transparentHeaderRef]) : ref
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user