animated pill

This commit is contained in:
Samuel Newman
2026-08-26 12:45:22 +01:00
parent c3807ad37d
commit 75cbc450ba
5 changed files with 425 additions and 104 deletions
@@ -1,5 +1,6 @@
import { import {
Children, Children,
memo,
useCallback, useCallback,
useContext, useContext,
useEffect, useEffect,
@@ -7,16 +8,31 @@ import {
useRef, useRef,
useState, useState,
} from 'react' } from 'react'
import {type StyleProp, View, type ViewStyle} from 'react-native' import {
type NativeSyntheticEvent,
type StyleProp,
View,
type ViewStyle,
} from 'react-native'
import {DrawerGestureContext} from 'react-native-drawer-layout' import {DrawerGestureContext} from 'react-native-drawer-layout'
import {Gesture, GestureDetector} from 'react-native-gesture-handler' import {Gesture, GestureDetector} from 'react-native-gesture-handler'
import NativePagerView from 'react-native-pager-view' import NativePagerView from 'react-native-pager-view'
import {
type PagerViewOnPageScrollEventData,
type PagerViewOnPageSelectedEventData,
type PageScrollStateChangedNativeEventData,
} from 'react-native-pager-view'
import Animated, {useEvent, useSharedValue} from 'react-native-reanimated'
import {scheduleOnRN} from 'react-native-worklets'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {useSetDrawerSwipeDisabled} from '#/state/shell' import {useSetDrawerSwipeDisabled} from '#/state/shell'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {usePagerContext} from './context' import {usePagerContext} from './context'
const AnimatedPagerView = Animated.createAnimatedComponent(NativePagerView)
const MemoizedAnimatedPagerView = memo(AnimatedPagerView)
export function Content({ export function Content({
children, children,
manageDrawerGesture = false, manageDrawerGesture = false,
@@ -28,8 +44,14 @@ export function Content({
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
testID?: string testID?: string
}) { }) {
const {initialPage, selectedPage, onPageSelected, onPageScrollStateChanged} = const {
usePagerContext() initialPage,
selectedPage,
dragProgress,
dragState,
onPageSelected,
onPageScrollStateChanged,
} = usePagerContext()
const pagerRef = useRef<NativePagerView>(null) const pagerRef = useRef<NativePagerView>(null)
const currentPage = useRef(initialPage) const currentPage = useRef(initialPage)
const [isIdle, setIsIdle] = useState(true) const [isIdle, setIsIdle] = useState(true)
@@ -52,28 +74,73 @@ export function Content({
} }
}, [selectedPage]) }, [selectedPage])
const handlePageSelected = useCallback(
(page: number) => {
currentPage.current = page
onPageSelected(page)
},
[onPageSelected],
)
const handlePageScrollStateChanged = useCallback(
(state: 'idle' | 'dragging' | 'settling') => {
setIsIdle(state === 'idle')
onPageScrollStateChanged(state)
},
[onPageScrollStateChanged],
)
const didInit = useSharedValue(false)
const handlePageScroll = useEvent<PagerNativeEvent>(
event => {
'worklet'
if (event.eventName.endsWith('onPageScroll') && 'offset' in event) {
if (didInit.get() === false) {
// iOS emits a spurious zero-position event before confirming the
// supplied initial page.
return
}
dragProgress.set(event.offset + event.position)
} else if (
event.eventName.endsWith('onPageScrollStateChanged') &&
'pageScrollState' in event
) {
scheduleOnRN(handlePageScrollStateChanged, event.pageScrollState)
if (
dragState.get() === 'idle' &&
event.pageScrollState === 'settling'
) {
// Android reports programmatic paging as a settling gesture. Keep
// this idle so tab bars can distinguish taps from direct swipes.
return
}
dragState.set(event.pageScrollState)
} else if (
event.eventName.endsWith('onPageSelected') &&
'position' in event
) {
didInit.set(true)
dragProgress.set(event.position)
scheduleOnRN(handlePageSelected, event.position)
}
},
['onPageScroll', 'onPageScrollStateChanged', 'onPageSelected'],
true,
)
const content = ( const content = (
<NativePagerView <MemoizedAnimatedPagerView
ref={pagerRef} ref={pagerRef}
testID={testID} testID={testID}
style={[a.flex_1, style]} style={[a.flex_1, style]}
initialPage={initialPage} initialPage={initialPage}
onPageSelected={event => { onPageScroll={handlePageScroll}>
const page = event.nativeEvent.position
currentPage.current = page
onPageSelected(page)
}}
onPageScrollStateChanged={event => {
const state = event.nativeEvent.pageScrollState
setIsIdle(state === 'idle')
onPageScrollStateChanged(state)
}}>
{Children.map(children, child => ( {Children.map(children, child => (
<View collapsable={false} style={a.flex_1}> <View collapsable={false} style={a.flex_1}>
{child} {child}
</View> </View>
))} ))}
</NativePagerView> </MemoizedAnimatedPagerView>
) )
return manageDrawerGesture ? ( return manageDrawerGesture ? (
@@ -83,6 +150,13 @@ export function Content({
) )
} }
type PagerEventData =
| PagerViewOnPageScrollEventData
| PagerViewOnPageSelectedEventData
| PageScrollStateChangedNativeEventData
type PagerNativeEvent = NativeSyntheticEvent<PagerEventData>
function DrawerGestureRequireFail({children}: {children: React.ReactNode}) { function DrawerGestureRequireFail({children}: {children: React.ReactNode}) {
const drawerGesture = useContext(DrawerGestureContext) const drawerGesture = useContext(DrawerGestureContext)
const pagerGesture = useMemo(() => { const pagerGesture = useMemo(() => {
@@ -1,5 +1,6 @@
import {Activity, Children, useEffect, useId, useRef, useState} from 'react' import {Activity, Children, useEffect, useId, useRef, useState} from 'react'
import {type StyleProp, View, type ViewStyle} from 'react-native' import {type StyleProp, View, type ViewStyle} from 'react-native'
import {ReduceMotion, withTiming} from 'react-native-reanimated'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {usePagerContext} from './context' import {usePagerContext} from './context'
@@ -15,7 +16,8 @@ export function Content({
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
testID?: string testID?: string
}) { }) {
const {selectedPage, onPageSelected} = usePagerContext() const {selectedPage, dragProgress, dragState, onPageSelected} =
usePagerContext()
const pages = Children.toArray(children) const pages = Children.toArray(children)
const activityName = useId() const activityName = useId()
const previousPage = useRef(selectedPage) const previousPage = useRef(selectedPage)
@@ -26,6 +28,17 @@ export function Content({
useEffect(() => { useEffect(() => {
if (selectedPage !== previousPage.current) { if (selectedPage !== previousPage.current) {
previousPage.current = selectedPage previousPage.current = selectedPage
dragState.set('settling')
dragProgress.set(
withTiming(
selectedPage,
{duration: 200, reduceMotion: ReduceMotion.System},
finished => {
'worklet'
if (finished) dragState.set('idle')
},
),
)
onPageSelected(selectedPage) onPageSelected(selectedPage)
} }
@@ -33,7 +46,7 @@ export function Content({
if (current.has(selectedPage)) return current if (current.has(selectedPage)) return current
return new Set([...current, selectedPage]) return new Set([...current, selectedPage])
}) })
}, [selectedPage, onPageSelected]) }, [selectedPage, dragProgress, dragState, onPageSelected])
return ( return (
<View testID={testID} style={[a.flex_1, style]}> <View testID={testID} style={[a.flex_1, style]}>
@@ -8,6 +8,7 @@ import {
useState, useState,
} from 'react' } from 'react'
import {type StyleProp, View, type ViewStyle} from 'react-native' import {type StyleProp, View, type ViewStyle} from 'react-native'
import {type SharedValue, useSharedValue} from 'react-native-reanimated'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
@@ -16,6 +17,8 @@ export type PagerScrollState = 'idle' | 'dragging' | 'settling'
export type PagerRenderProps = { export type PagerRenderProps = {
selectedPage: number selectedPage: number
selectPage: (page: number) => void selectPage: (page: number) => void
dragProgress: SharedValue<number>
dragState: SharedValue<PagerScrollState>
} }
type PagerContextValue = PagerRenderProps & { type PagerContextValue = PagerRenderProps & {
@@ -45,6 +48,8 @@ export function Root({
}) { }) {
const [selectedPage, setSelectedPage] = useState(initialPage) const [selectedPage, setSelectedPage] = useState(initialPage)
const selectedPageRef = useRef(initialPage) const selectedPageRef = useRef(initialPage)
const dragProgress = useSharedValue(initialPage)
const dragState = useSharedValue<PagerScrollState>('idle')
const handlePageSelected = useCallback( const handlePageSelected = useCallback(
(page: number) => { (page: number) => {
@@ -73,6 +78,8 @@ export function Root({
initialPage, initialPage,
selectedPage, selectedPage,
selectPage, selectPage,
dragProgress,
dragState,
onPageSelected: handlePageSelected, onPageSelected: handlePageSelected,
onPageScrollStateChanged: (state: PagerScrollState) => onPageScrollStateChanged: (state: PagerScrollState) =>
onPageScrollStateChanged?.(state), onPageScrollStateChanged?.(state),
@@ -81,6 +88,8 @@ export function Root({
initialPage, initialPage,
selectedPage, selectedPage,
selectPage, selectPage,
dragProgress,
dragState,
handlePageSelected, handlePageSelected,
onPageScrollStateChanged, onPageScrollStateChanged,
], ],
@@ -100,13 +109,12 @@ export function TabBar({
}: { }: {
children: (props: PagerRenderProps) => ReactNode children: (props: PagerRenderProps) => ReactNode
}) { }) {
const {selectedPage, selectPage} = usePager() return children(usePager())
return children({selectedPage, selectPage})
} }
export function usePager(): PagerRenderProps { export function usePager(): PagerRenderProps {
const {selectedPage, selectPage} = usePagerContext() const {selectedPage, selectPage, dragProgress, dragState} = usePagerContext()
return {selectedPage, selectPage} return {selectedPage, selectPage, dragProgress, dragState}
} }
export function usePagerContext() { export function usePagerContext() {
+292 -80
View File
@@ -1,13 +1,21 @@
import {useEffect, useRef, useState} from 'react' import {useEffect, useLayoutEffect, useRef, useState} from 'react'
import { import {
Pressable,
type ReactNativeElement,
type ScrollView, type ScrollView,
type StyleProp, type StyleProp,
useWindowDimensions,
View, View,
type ViewStyle, type ViewStyle,
} from 'react-native' } from 'react-native'
import Animated, {
interpolate,
type SharedValue,
useAnimatedStyle,
useDerivedValue,
} from 'react-native-reanimated'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView' import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {atoms as a, tokens, useTheme, utils, web} from '#/alf' import {atoms as a, tokens, useTheme, utils, web} from '#/alf'
@@ -27,60 +35,75 @@ export type TabPillItem = {
export function TabPills({ export function TabPills({
tabs, tabs,
selectedTab, selectedTab,
dragProgress,
onSelectTab, onSelectTab,
contentContainerStyle, contentContainerStyle,
gutterWidth = tokens.space.lg, gutterWidth = tokens.space.lg,
}: { }: {
tabs: TabPillItem[] tabs: TabPillItem[]
selectedTab: string selectedTab: string
dragProgress: SharedValue<number>
onSelectTab: (tab: string) => void onSelectTab: (tab: string) => void
contentContainerStyle?: StyleProp<ViewStyle> contentContainerStyle?: StyleProp<ViewStyle>
gutterWidth?: number gutterWidth?: number
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const listRef = useRef<ScrollView>(null) const {width: windowWidth} = useWindowDimensions()
const listRef = useRef<ScrollView & ReactNativeElement>(null)
const [totalWidth, setTotalWidth] = useState(0) const [totalWidth, setTotalWidth] = useState(0)
const [scrollX, setScrollX] = useState(0) const [scrollX, setScrollX] = useState(0)
const [contentWidth, setContentWidth] = useState(0) const [contentWidth, setContentWidth] = useState(0)
const pendingTabOffsets = useRef<{x: number; width: number}[]>([]) const [tabOffsets, setTabOffsets] = useState<PillLayout[]>([])
const [tabOffsets, setTabOffsets] = useState<{x: number; width: number}[]>([]) const contentRef = useRef<View>(null)
const tabRefs = useRef<Array<View | null>>([])
const didMeasure = useRef(false)
const tabLayoutKey = tabs.map(tab => `${tab.key}:${tab.label}`).join('|')
const tabCount = tabs.length
const onInitialLayout = useNonReactiveCallback(() => { useLayoutEffect(() => {
scrollIntoViewIfNeeded(tabs.findIndex(tab => tab.key === selectedTab)) const viewportRect = listRef.current?.getBoundingClientRect()
}) const contentRect = contentRef.current?.getBoundingClientRect()
if (!viewportRect || !contentRect) return
useEffect(() => { const layouts = Array.from({length: tabCount}, (_, index) => {
if (tabOffsets) { const rect = tabRefs.current[index]?.getBoundingClientRect()
onInitialLayout() if (!rect) return null
} return {
}, [tabOffsets, onInitialLayout]) x: rect.left - contentRect.left,
y: rect.top - contentRect.top,
function scrollIntoViewIfNeeded(index: number) { width: rect.width,
const btnLayout = tabOffsets[index] height: rect.height,
if (!btnLayout) return }
listRef.current?.scrollTo({
x: btnLayout.x - (totalWidth / 2 - btnLayout.width / 2),
animated: true,
}) })
} if (layouts.some(layout => layout === null)) return
const nextLayouts = layouts as PillLayout[]
setTabOffsets(current =>
areLayoutsEqual(current, nextLayouts) ? current : nextLayouts,
)
if (IS_WEB) {
setTotalWidth(viewportRect.width)
setContentWidth(contentRect.width)
}
const selectedIndex = tabs.findIndex(tab => tab.key === selectedTab)
const selectedLayout = nextLayouts[selectedIndex]
if (selectedLayout) {
const centeredOffset =
selectedLayout.x - (viewportRect.width / 2 - selectedLayout.width / 2)
const maxOffset = Math.max(0, contentRect.width - viewportRect.width)
listRef.current?.scrollTo({
x: Math.min(maxOffset, Math.max(0, centeredOffset)),
animated: didMeasure.current,
})
}
didMeasure.current = true
}, [selectedTab, tabLayoutKey, tabCount, tabs, windowWidth])
function handleSelectTab(index: number) { function handleSelectTab(index: number) {
const tab = tabs[index] const tab = tabs[index]
onSelectTab(tab.key) onSelectTab(tab.key)
scrollIntoViewIfNeeded(index)
}
function handleTabLayout(index: number, x: number, width: number) {
if (!tabOffsets.length) {
pendingTabOffsets.current[index] = {x, width}
if (
pendingTabOffsets.current.filter(offset => !!offset).length ===
tabs.length
) {
setTabOffsets(pendingTabOffsets.current)
}
}
} }
const canScrollLeft = scrollX > 0 const canScrollLeft = scrollX > 0
@@ -170,15 +193,10 @@ export function TabPills({
}, []) }, [])
return ( return (
<View style={[a.relative, a.flex_row]}> <View style={[a.relative, a.flex_row]} accessibilityRole="tablist">
<BlockDrawerGesture> <BlockDrawerGesture>
<DraggableScrollView <DraggableScrollView
ref={listRef} ref={listRef}
contentContainerStyle={[
a.gap_sm,
{paddingHorizontal: gutterWidth},
contentContainerStyle,
]}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
decelerationRate="fast" decelerationRate="fast"
snapToOffsets={ snapToOffsets={
@@ -186,22 +204,93 @@ export function TabPills({
? tabOffsets.map(offset => offset.x - tokens.space.xl) ? tabOffsets.map(offset => offset.x - tokens.space.xl)
: undefined : undefined
} }
onLayout={event => setTotalWidth(event.nativeEvent.layout.width)} onScroll={
onContentSizeChange={width => setContentWidth(width)} IS_WEB
onScroll={event => { ? event => setScrollX(event.nativeEvent.contentOffset.x)
setScrollX(event.nativeEvent.contentOffset.x) : undefined
}} }
scrollEventThrottle={16}> scrollEventThrottle={IS_WEB ? 16 : undefined}>
{tabs.map((tab, index) => ( <View
<TabPill ref={contentRef}
key={tab.key} style={[
tab={tab} a.flex_row,
index={index} a.gap_sm,
active={tab.key === selectedTab} {paddingHorizontal: gutterWidth},
onSelectTab={handleSelectTab} contentContainerStyle,
onLayout={handleTabLayout} ]}>
/> {tabs.map((tab, index) => (
))} <TabPill
key={tab.key}
elementRef={element => {
tabRefs.current[index] = element
}}
tab={tab}
index={index}
active={tab.key === selectedTab}
onSelectTab={handleSelectTab}
/>
))}
{tabOffsets.map((layout, index) => (
<View
key={`border-${tabs[index].key}`}
accessible={false}
pointerEvents="none"
style={[
a.absolute,
a.rounded_full,
a.curve_continuous,
t.atoms.bg,
t.atoms.border_contrast_low,
{
zIndex: 1,
borderWidth: 1,
left: layout.x,
top: layout.y,
width: layout.width,
height: layout.height,
},
]}></View>
))}
{tabOffsets.length === tabs.length && (
<PillIndicator
layouts={tabOffsets}
dragProgress={dragProgress}
backgroundColor={t.atoms.bg_contrast_50.backgroundColor}
borderColor={t.palette.contrast_50}
/>
)}
{tabOffsets.map((layout, index) => (
<View
key={`label-${tabs[index].key}`}
aria-hidden
accessible={false}
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
pointerEvents="none"
style={[
a.absolute,
a.align_center,
a.justify_center,
{
zIndex: 3,
left: layout.x,
top: layout.y,
width: layout.width,
height: layout.height,
},
]}>
<Text
style={[
a.font_medium,
tabs[index].key === selectedTab
? t.atoms.text
: t.atoms.text_contrast_high,
]}>
{tabs[index].label}
</Text>
</View>
))}
</View>
</DraggableScrollView> </DraggableScrollView>
</BlockDrawerGesture> </BlockDrawerGesture>
@@ -234,6 +323,7 @@ export function TabPills({
a.h_full, a.h_full,
a.aspect_square, a.aspect_square,
a.rounded_full, a.rounded_full,
a.curve_continuous,
]}> ]}>
<ButtonIcon icon={ArrowLeft} /> <ButtonIcon icon={ArrowLeft} />
</Button> </Button>
@@ -269,6 +359,7 @@ export function TabPills({
a.h_full, a.h_full,
a.aspect_square, a.aspect_square,
a.rounded_full, a.rounded_full,
a.curve_continuous,
]}> ]}>
<ButtonIcon icon={ArrowRight} /> <ButtonIcon icon={ArrowRight} />
</Button> </Button>
@@ -279,56 +370,177 @@ export function TabPills({
} }
function TabPill({ function TabPill({
elementRef,
tab, tab,
active, active,
index, index,
onSelectTab, onSelectTab,
onLayout,
}: { }: {
elementRef: React.Ref<View>
tab: TabPillItem tab: TabPillItem
active: boolean active: boolean
index: number index: number
onSelectTab: (index: number) => void onSelectTab: (index: number) => void
onLayout: (index: number, x: number, width: number) => void
}) { }) {
const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
return ( return (
<View <View ref={elementRef}>
onLayout={event => <Pressable
onLayout( accessibilityLabel={
index,
event.nativeEvent.layout.x,
event.nativeEvent.layout.width,
)
}>
<Button
label={
active ? l`${tab.label} tab, selected` : l`Select ${tab.label} tab` active ? l`${tab.label} tab, selected` : l`Select ${tab.label} tab`
} }
accessibilityHint={l`Shows ${tab.label} notifications`}
accessibilityRole="tab" accessibilityRole="tab"
accessibilityState={{selected: active}} accessibilityState={{selected: active}}
onPress={() => onSelectTab(index)}> onPress={() => onSelectTab(index)}
style={[a.rounded_full, a.curve_continuous]}>
<View <View
style={[ style={[
a.rounded_full, a.rounded_full,
a.curve_continuous,
a.px_lg, a.px_lg,
a.py_sm, a.py_sm,
{borderWidth: 1}, a.bg_transparent,
active {borderWidth: 1, borderColor: 'transparent'},
? [t.atoms.bg_contrast_50, {borderColor: t.palette.contrast_50}]
: [a.bg_transparent, t.atoms.border_contrast_low],
]}> ]}>
<Text <Text accessible={false} style={[a.font_medium, {opacity: 0}]}>
style={[
a.font_medium,
active ? t.atoms.text : t.atoms.text_contrast_high,
]}>
{tab.label} {tab.label}
</Text> </Text>
</View> </View>
</Button> </Pressable>
</View> </View>
) )
} }
type PillLayout = {
x: number
y: number
width: number
height: number
}
function areLayoutsEqual(a: PillLayout[], b: PillLayout[]) {
return (
a.length === b.length &&
a.every(
(layout, index) =>
layout.x === b[index].x &&
layout.y === b[index].y &&
layout.width === b[index].width &&
layout.height === b[index].height,
)
)
}
function PillIndicator({
layouts,
dragProgress,
backgroundColor,
borderColor,
}: {
layouts: PillLayout[]
dragProgress: SharedValue<number>
backgroundColor: string
borderColor: string
}) {
const height = layouts[0].height
const radius = height / 2
const inputRange = layouts.map((_, index) => index)
const xOutputRange = layouts.map(layout => layout.x)
const widthOutputRange = layouts.map(layout => layout.width)
const geometry = useDerivedValue(() => {
const progress = dragProgress.get()
return {
x: interpolate(progress, inputRange, xOutputRange, 'clamp'),
width: interpolate(progress, inputRange, widthOutputRange, 'clamp'),
}
})
const containerStyle = useAnimatedStyle(() => ({
transform: [{translateX: geometry.get().x}],
}))
const middleStyle = useAnimatedStyle(() => ({
transform: [{scaleX: Math.max(0.01, geometry.get().width - height)}],
}))
const rightCapStyle = useAnimatedStyle(() => ({
transform: [{translateX: geometry.get().width - radius - 1}],
}))
return (
<Animated.View
accessible={false}
pointerEvents="none"
style={[
a.absolute,
a.curve_continuous,
{
zIndex: 1,
top: layouts[0].y,
left: 0,
width: radius + 1,
height,
},
containerStyle,
]}>
<View
style={[
a.curve_continuous,
a.absolute,
a.top_0,
a.left_0,
{
width: radius + 1,
height,
backgroundColor,
borderColor,
borderTopWidth: 1,
borderBottomWidth: 1,
borderLeftWidth: 1,
borderTopLeftRadius: radius,
borderBottomLeftRadius: radius,
},
]}
/>
<Animated.View
style={[
a.curve_continuous,
a.absolute,
a.top_0,
{
left: radius,
width: 1,
height,
transformOrigin: 'left center',
backgroundColor,
borderColor,
borderTopWidth: 1,
borderBottomWidth: 1,
},
middleStyle,
]}
/>
<Animated.View
style={[
a.curve_continuous,
a.absolute,
a.top_0,
a.left_0,
{
width: radius + 1,
height,
backgroundColor,
borderColor,
borderTopWidth: 1,
borderRightWidth: 1,
borderBottomWidth: 1,
borderTopRightRadius: radius,
borderBottomRightRadius: radius,
},
rightCapStyle,
]}
/>
</Animated.View>
)
}
+17 -3
View File
@@ -24,11 +24,11 @@ import {NotificationsScreen as LegacyNotificationsScreen} from '#/view/screens/N
import {PageList} from '#/screens/Notifications/components/PageList' import {PageList} from '#/screens/Notifications/components/PageList'
import * as Pager from '#/screens/Notifications/components/PagerView' import * as Pager from '#/screens/Notifications/components/PagerView'
import {TabPills} from '#/screens/Notifications/components/TabPills' import {TabPills} from '#/screens/Notifications/components/TabPills'
import {atoms as a, useTheme, utils} from '#/alf' import {atoms as a, useBreakpoints, useTheme, utils} from '#/alf'
import {useHeaderOffset} from '#/components/hooks/useHeaderOffset' import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_LIQUID_GLASS} from '#/env' import {IS_LIQUID_GLASS, IS_WEB} from '#/env'
type Props = NativeStackScreenProps< type Props = NativeStackScreenProps<
NotificationsTabNavigatorParams, NotificationsTabNavigatorParams,
@@ -99,10 +99,11 @@ function NewNotificationsScreenInner() {
}}> }}>
<NotificationsHeader onHeightChange={setHeaderOffset}> <NotificationsHeader onHeightChange={setHeaderOffset}>
<Pager.TabBar> <Pager.TabBar>
{({selectedPage, selectPage}) => ( {({selectedPage, selectPage, dragProgress}) => (
<TabPills <TabPills
tabs={tabs} tabs={tabs}
selectedTab={tabs[selectedPage].key} selectedTab={tabs[selectedPage].key}
dragProgress={dragProgress}
onSelectTab={tab => onSelectTab={tab =>
selectPage(tabs.findIndex(candidate => candidate.key === tab)) selectPage(tabs.findIndex(candidate => candidate.key === tab))
} }
@@ -134,6 +135,7 @@ function NotificationsHeader({
const t = useTheme() const t = useTheme()
const headerMode = useHomeHeaderMode() const headerMode = useHomeHeaderMode()
const {headerHeight} = useShellLayout() const {headerHeight} = useShellLayout()
const {gtMobile} = useBreakpoints()
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const headerPinnedHeight = IS_LIQUID_GLASS ? insets.top : 0 const headerPinnedHeight = IS_LIQUID_GLASS ? insets.top : 0
@@ -183,6 +185,18 @@ function NotificationsHeader({
style={[a.absolute, a.inset_0, t.atoms.bg]} style={[a.absolute, a.inset_0, t.atoms.bg]}
/> />
)} )}
{IS_WEB && gtMobile && (
<Layout.Center
pointerEvents="none"
style={[
a.absolute,
a.inset_0,
a.border_x,
t.atoms.border_contrast_low,
{maxWidth: Layout.CENTER_COLUMN_WIDTH + 2},
]}
/>
)}
<Animated.View <Animated.View
style={[IS_LIQUID_GLASS && {paddingTop: insets.top}, titleStyle]} style={[IS_LIQUID_GLASS && {paddingTop: insets.top}, titleStyle]}
onLayout={event => { onLayout={event => {