blurred header on iOS 26
This commit is contained in:
@@ -0,0 +1,87 @@
|
|||||||
|
import {useLayoutEffect, useMemo, useState} from 'react'
|
||||||
|
import {useWindowDimensions} from 'react-native'
|
||||||
|
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated'
|
||||||
|
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {IS_LIQUID_GLASS} from '#/env'
|
||||||
|
import {Outer as DefaultOuter, type OuterProps} from './index.shared'
|
||||||
|
|
||||||
|
export {
|
||||||
|
BackButton,
|
||||||
|
Content,
|
||||||
|
MenuButton,
|
||||||
|
type OuterProps,
|
||||||
|
Slot,
|
||||||
|
SubtitleText,
|
||||||
|
TitleText,
|
||||||
|
} from './index.shared'
|
||||||
|
|
||||||
|
export function Outer(props: OuterProps) {
|
||||||
|
if (IS_LIQUID_GLASS && props.transparent) {
|
||||||
|
return <TransparentOuter {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DefaultOuter {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function TransparentOuter({children, headerRef}: OuterProps) {
|
||||||
|
const [headerWidth, setHeaderWidth] = useState(0)
|
||||||
|
const {width: screenWidth} = useWindowDimensions()
|
||||||
|
|
||||||
|
// 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 animatedOpacity = useAnimatedStyle(() => ({
|
||||||
|
opacity: withTiming(isInitialRender ? 0 : 1, {duration: 100}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const headerElement = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
ref={headerRef}
|
||||||
|
onLayout={evt => setHeaderWidth(evt.nativeEvent.layout.width)}
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.px_xs,
|
||||||
|
animatedOpacity,
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
|
}, [children, headerRef, animatedOpacity])
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
import {createContext, useCallback, useContext} from 'react'
|
||||||
|
import {type GestureResponderEvent, Keyboard, View} from 'react-native'
|
||||||
|
import {msg} from '@lingui/core/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {HITSLOP_30} from '#/lib/constants'
|
||||||
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
|
import {useSetDrawerOpen} from '#/state/shell'
|
||||||
|
import {
|
||||||
|
atoms as a,
|
||||||
|
platform,
|
||||||
|
type TextStyleProp,
|
||||||
|
useBreakpoints,
|
||||||
|
useGutters,
|
||||||
|
useLayoutBreakpoints,
|
||||||
|
useTheme,
|
||||||
|
web,
|
||||||
|
} from '#/alf'
|
||||||
|
import {Button, ButtonIcon, type ButtonProps} from '#/components/Button'
|
||||||
|
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
|
||||||
|
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||||
|
import {
|
||||||
|
BUTTON_VISUAL_ALIGNMENT_OFFSET,
|
||||||
|
CENTER_COLUMN_OFFSET,
|
||||||
|
HEADER_SLOT_SIZE,
|
||||||
|
SCROLLBAR_OFFSET,
|
||||||
|
} from '#/components/Layout/const'
|
||||||
|
import {ScrollbarOffsetContext} from '#/components/Layout/context'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {IS_IOS} from '#/env'
|
||||||
|
|
||||||
|
export type OuterProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
noBottomBorder?: boolean
|
||||||
|
headerRef?: React.RefObject<View | null>
|
||||||
|
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({
|
||||||
|
children,
|
||||||
|
noBottomBorder,
|
||||||
|
headerRef,
|
||||||
|
sticky = true,
|
||||||
|
}: OuterProps) {
|
||||||
|
const t = useTheme()
|
||||||
|
const gutters = useGutters([0, 'base'])
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
|
||||||
|
const {centerColumnOffset} = useLayoutBreakpoints()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
ref={headerRef}
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
!noBottomBorder && a.border_b,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
sticky && web([a.sticky, {top: 0}, a.z_10, t.atoms.bg]),
|
||||||
|
gutters,
|
||||||
|
platform({
|
||||||
|
native: [a.pb_xs, {minHeight: 48}],
|
||||||
|
web: [a.py_xs, {minHeight: 52}],
|
||||||
|
}),
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
gtMobile && [a.mx_auto, {maxWidth: 600}],
|
||||||
|
!isWithinOffsetView && {
|
||||||
|
transform: [
|
||||||
|
{translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0},
|
||||||
|
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlignmentContext = createContext<'platform' | 'left'>('platform')
|
||||||
|
AlignmentContext.displayName = 'AlignmentContext'
|
||||||
|
|
||||||
|
export function Content({
|
||||||
|
children,
|
||||||
|
align = 'platform',
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode
|
||||||
|
align?: 'platform' | 'left'
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.justify_center,
|
||||||
|
IS_IOS && align === 'platform' && a.align_center,
|
||||||
|
{minHeight: HEADER_SLOT_SIZE},
|
||||||
|
]}>
|
||||||
|
<AlignmentContext.Provider value={align}>
|
||||||
|
{children}
|
||||||
|
</AlignmentContext.Provider>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Slot({children}: {children?: React.ReactNode}) {
|
||||||
|
return <View style={[a.z_50, {width: HEADER_SLOT_SIZE}]}>{children}</View>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
|
const onPressBack = useCallback(
|
||||||
|
(evt: GestureResponderEvent) => {
|
||||||
|
onPress?.(evt)
|
||||||
|
if (evt.defaultPrevented) return
|
||||||
|
if (navigation.canGoBack()) {
|
||||||
|
navigation.goBack()
|
||||||
|
} else {
|
||||||
|
navigation.navigate('Home')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onPress, navigation],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Go back`)}
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
shape="round"
|
||||||
|
onPress={onPressBack}
|
||||||
|
hitSlop={HITSLOP_30}
|
||||||
|
style={[
|
||||||
|
{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
|
||||||
|
a.bg_transparent,
|
||||||
|
style,
|
||||||
|
]}
|
||||||
|
{...props}>
|
||||||
|
<ButtonIcon icon={ArrowLeft} size="lg" />
|
||||||
|
</Button>
|
||||||
|
</Slot>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MenuButton() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const setDrawerOpen = useSetDrawerOpen()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
Keyboard.dismiss()
|
||||||
|
setDrawerOpen(true)
|
||||||
|
}, [setDrawerOpen])
|
||||||
|
|
||||||
|
return gtMobile ? null : (
|
||||||
|
<Slot>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Open drawer menu`)}
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
shape="square"
|
||||||
|
onPress={onPress}
|
||||||
|
hitSlop={HITSLOP_30}
|
||||||
|
style={[
|
||||||
|
{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
|
||||||
|
a.bg_transparent,
|
||||||
|
]}>
|
||||||
|
<ButtonIcon icon={Menu} size="lg" />
|
||||||
|
</Button>
|
||||||
|
</Slot>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TitleText({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
}: {children: React.ReactNode} & TextStyleProp) {
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const align = useContext(AlignmentContext)
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_lg,
|
||||||
|
a.font_semi_bold,
|
||||||
|
a.leading_tight,
|
||||||
|
IS_IOS && align === 'platform' && a.text_center,
|
||||||
|
gtMobile && a.text_xl,
|
||||||
|
style,
|
||||||
|
]}
|
||||||
|
numberOfLines={2}
|
||||||
|
emoji>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SubtitleText({children}: {children: React.ReactNode}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const align = useContext(AlignmentContext)
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_sm,
|
||||||
|
a.leading_snug,
|
||||||
|
IS_IOS && align === 'platform' && a.text_center,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}
|
||||||
|
numberOfLines={2}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,215 +1 @@
|
|||||||
import {createContext, useCallback, useContext} from 'react'
|
export * from './index.shared'
|
||||||
import {type GestureResponderEvent, Keyboard, View} from 'react-native'
|
|
||||||
import {msg} from '@lingui/core/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {useNavigation} from '@react-navigation/native'
|
|
||||||
|
|
||||||
import {HITSLOP_30} from '#/lib/constants'
|
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
|
||||||
import {useSetDrawerOpen} from '#/state/shell'
|
|
||||||
import {
|
|
||||||
atoms as a,
|
|
||||||
platform,
|
|
||||||
type TextStyleProp,
|
|
||||||
useBreakpoints,
|
|
||||||
useGutters,
|
|
||||||
useLayoutBreakpoints,
|
|
||||||
useTheme,
|
|
||||||
web,
|
|
||||||
} from '#/alf'
|
|
||||||
import {Button, ButtonIcon, type ButtonProps} from '#/components/Button'
|
|
||||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
|
|
||||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
|
||||||
import {
|
|
||||||
BUTTON_VISUAL_ALIGNMENT_OFFSET,
|
|
||||||
CENTER_COLUMN_OFFSET,
|
|
||||||
HEADER_SLOT_SIZE,
|
|
||||||
SCROLLBAR_OFFSET,
|
|
||||||
} from '#/components/Layout/const'
|
|
||||||
import {ScrollbarOffsetContext} from '#/components/Layout/context'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {IS_IOS} from '#/env'
|
|
||||||
|
|
||||||
export function Outer({
|
|
||||||
children,
|
|
||||||
noBottomBorder,
|
|
||||||
headerRef,
|
|
||||||
sticky = true,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
noBottomBorder?: boolean
|
|
||||||
headerRef?: React.RefObject<View | null>
|
|
||||||
sticky?: boolean
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const gutters = useGutters([0, 'base'])
|
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
|
|
||||||
const {centerColumnOffset} = useLayoutBreakpoints()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
ref={headerRef}
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
!noBottomBorder && a.border_b,
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.gap_sm,
|
|
||||||
sticky && web([a.sticky, {top: 0}, a.z_10, t.atoms.bg]),
|
|
||||||
gutters,
|
|
||||||
platform({
|
|
||||||
native: [a.pb_xs, {minHeight: 48}],
|
|
||||||
web: [a.py_xs, {minHeight: 52}],
|
|
||||||
}),
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
gtMobile && [a.mx_auto, {maxWidth: 600}],
|
|
||||||
!isWithinOffsetView && {
|
|
||||||
transform: [
|
|
||||||
{translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0},
|
|
||||||
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const AlignmentContext = createContext<'platform' | 'left'>('platform')
|
|
||||||
AlignmentContext.displayName = 'AlignmentContext'
|
|
||||||
|
|
||||||
export function Content({
|
|
||||||
children,
|
|
||||||
align = 'platform',
|
|
||||||
}: {
|
|
||||||
children?: React.ReactNode
|
|
||||||
align?: 'platform' | 'left'
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_1,
|
|
||||||
a.justify_center,
|
|
||||||
IS_IOS && align === 'platform' && a.align_center,
|
|
||||||
{minHeight: HEADER_SLOT_SIZE},
|
|
||||||
]}>
|
|
||||||
<AlignmentContext.Provider value={align}>
|
|
||||||
{children}
|
|
||||||
</AlignmentContext.Provider>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Slot({children}: {children?: React.ReactNode}) {
|
|
||||||
return <View style={[a.z_50, {width: HEADER_SLOT_SIZE}]}>{children}</View>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const navigation = useNavigation<NavigationProp>()
|
|
||||||
|
|
||||||
const onPressBack = useCallback(
|
|
||||||
(evt: GestureResponderEvent) => {
|
|
||||||
onPress?.(evt)
|
|
||||||
if (evt.defaultPrevented) return
|
|
||||||
if (navigation.canGoBack()) {
|
|
||||||
navigation.goBack()
|
|
||||||
} else {
|
|
||||||
navigation.navigate('Home')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onPress, navigation],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Slot>
|
|
||||||
<Button
|
|
||||||
label={_(msg`Go back`)}
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
color="secondary"
|
|
||||||
shape="round"
|
|
||||||
onPress={onPressBack}
|
|
||||||
hitSlop={HITSLOP_30}
|
|
||||||
style={[
|
|
||||||
{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
|
|
||||||
a.bg_transparent,
|
|
||||||
style,
|
|
||||||
]}
|
|
||||||
{...props}>
|
|
||||||
<ButtonIcon icon={ArrowLeft} size="lg" />
|
|
||||||
</Button>
|
|
||||||
</Slot>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MenuButton() {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const setDrawerOpen = useSetDrawerOpen()
|
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
|
||||||
Keyboard.dismiss()
|
|
||||||
setDrawerOpen(true)
|
|
||||||
}, [setDrawerOpen])
|
|
||||||
|
|
||||||
return gtMobile ? null : (
|
|
||||||
<Slot>
|
|
||||||
<Button
|
|
||||||
label={_(msg`Open drawer menu`)}
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
color="secondary"
|
|
||||||
shape="square"
|
|
||||||
onPress={onPress}
|
|
||||||
hitSlop={HITSLOP_30}
|
|
||||||
style={[
|
|
||||||
{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
|
|
||||||
a.bg_transparent,
|
|
||||||
]}>
|
|
||||||
<ButtonIcon icon={Menu} size="lg" />
|
|
||||||
</Button>
|
|
||||||
</Slot>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TitleText({
|
|
||||||
children,
|
|
||||||
style,
|
|
||||||
}: {children: React.ReactNode} & TextStyleProp) {
|
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
const align = useContext(AlignmentContext)
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_lg,
|
|
||||||
a.font_semi_bold,
|
|
||||||
a.leading_tight,
|
|
||||||
IS_IOS && align === 'platform' && a.text_center,
|
|
||||||
gtMobile && a.text_xl,
|
|
||||||
style,
|
|
||||||
]}
|
|
||||||
numberOfLines={2}
|
|
||||||
emoji>
|
|
||||||
{children}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SubtitleText({children}: {children: React.ReactNode}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const align = useContext(AlignmentContext)
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_sm,
|
|
||||||
a.leading_snug,
|
|
||||||
IS_IOS && align === 'platform' && a.text_center,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}
|
|
||||||
numberOfLines={2}>
|
|
||||||
{children}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,16 +12,18 @@ import {makeProfileLink} from '#/lib/routes/links'
|
|||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {type Shadow} from '#/state/cache/profile-shadow'
|
import {type Shadow} from '#/state/cache/profile-shadow'
|
||||||
import {isConvoActive, useConvo} from '#/state/messages/convo'
|
import {isConvoActive, useConvo} from '#/state/messages/convo'
|
||||||
import {type ConvoItem} from '#/state/messages/convo/types'
|
import {type ConvoItem, type ConvoState} from '#/state/messages/convo/types'
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
||||||
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
|
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useSimpleVerificationState} from '#/components/verification'
|
import {
|
||||||
|
type SimpleVerificationState,
|
||||||
|
useSimpleVerificationState,
|
||||||
|
} from '#/components/verification'
|
||||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
|
|
||||||
@@ -35,6 +37,10 @@ export function MessagesListHeader({
|
|||||||
moderation?: ModerationDecision
|
moderation?: ModerationDecision
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const convoState = useConvo()
|
||||||
|
const verification = useSimpleVerificationState({
|
||||||
|
profile,
|
||||||
|
})
|
||||||
|
|
||||||
const blockInfo = useMemo(() => {
|
const blockInfo = useMemo(() => {
|
||||||
if (!moderation) return
|
if (!moderation) return
|
||||||
@@ -49,8 +55,8 @@ export function MessagesListHeader({
|
|||||||
}, [moderation])
|
}, [moderation])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Header.Outer>
|
<Layout.Header.Outer transparent>
|
||||||
<View style={[a.w_full, 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 />
|
||||||
</View>
|
</View>
|
||||||
@@ -59,6 +65,8 @@ export function MessagesListHeader({
|
|||||||
profile={profile}
|
profile={profile}
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
blockInfo={blockInfo}
|
blockInfo={blockInfo}
|
||||||
|
convoState={convoState}
|
||||||
|
verification={verification}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -101,6 +109,8 @@ function HeaderReady({
|
|||||||
profile,
|
profile,
|
||||||
moderation,
|
moderation,
|
||||||
blockInfo,
|
blockInfo,
|
||||||
|
convoState,
|
||||||
|
verification,
|
||||||
}: {
|
}: {
|
||||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||||
moderation: ModerationDecision
|
moderation: ModerationDecision
|
||||||
@@ -108,13 +118,11 @@ function HeaderReady({
|
|||||||
listBlocks: ModerationCause[]
|
listBlocks: ModerationCause[]
|
||||||
userBlock?: ModerationCause
|
userBlock?: ModerationCause
|
||||||
}
|
}
|
||||||
|
convoState: ConvoState
|
||||||
|
verification: SimpleVerificationState
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const convoState = useConvo()
|
|
||||||
const verification = useSimpleVerificationState({
|
|
||||||
profile,
|
|
||||||
})
|
|
||||||
|
|
||||||
const isDeletedAccount = profile?.handle === 'missing.invalid'
|
const isDeletedAccount = profile?.handle === 'missing.invalid'
|
||||||
const displayName = isDeletedAccount
|
const displayName = isDeletedAccount
|
||||||
@@ -136,11 +144,10 @@ function HeaderReady({
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_1, a.flex_row, a.align_center, a.justify_between]}>
|
||||||
<View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
|
|
||||||
<Link
|
<Link
|
||||||
label={_(msg`View ${displayName}'s profile`)}
|
label={_(msg`View ${displayName}'s profile`)}
|
||||||
style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]}
|
style={[a.flex_row, a.align_center, a.gap_md, a.flex_1, a.pr_md]}
|
||||||
to={makeProfileLink(profile)}>
|
to={makeProfileLink(profile)}>
|
||||||
<PreviewableUserAvatar
|
<PreviewableUserAvatar
|
||||||
size={PFP_SIZE}
|
size={PFP_SIZE}
|
||||||
@@ -169,28 +176,13 @@ function HeaderReady({
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
|
||||||
{!isDeletedAccount && (
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
a.text_xs,
|
|
||||||
web([a.leading_normal, {marginTop: -2}]),
|
|
||||||
]}
|
|
||||||
numberOfLines={1}>
|
|
||||||
@{profile.handle}
|
|
||||||
{convoState.convo?.muted && (
|
{convoState.convo?.muted && (
|
||||||
<>
|
<>
|
||||||
{' '}
|
<Text> · </Text>
|
||||||
·{' '}
|
<BellStroke size="xs" style={t.atoms.text_contrast_medium} />
|
||||||
<BellStroke
|
|
||||||
size="xs"
|
|
||||||
style={t.atoms.text_contrast_medium}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</View>
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@@ -208,19 +200,5 @@ function HeaderReady({
|
|||||||
</Layout.Header.Slot>
|
</Layout.Header.Slot>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
paddingLeft: PFP_SIZE + a.gap_md.gap,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<PostAlerts
|
|
||||||
modui={moderation.ui('contentList')}
|
|
||||||
size="lg"
|
|
||||||
style={[a.pt_xs]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import {MessagesListHeader} from '#/components/dms/MessagesListHeader'
|
|||||||
import {Error} from '#/components/Error'
|
import {Error} from '#/components/Error'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_LIQUID_GLASS, IS_WEB} from '#/env'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<
|
type Props = NativeStackScreenProps<
|
||||||
CommonNavigatorParams,
|
CommonNavigatorParams,
|
||||||
@@ -86,7 +86,10 @@ export function MessagesConversationScreenInner({route}: Props) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen testID="convoScreen" style={web([{minHeight: 0}, a.flex_1])}>
|
<Layout.Screen
|
||||||
|
testID="convoScreen"
|
||||||
|
style={web([{minHeight: 0}, a.flex_1])}
|
||||||
|
noInsetTop={IS_LIQUID_GLASS}>
|
||||||
<ConvoProvider key={convoId} convoId={convoId}>
|
<ConvoProvider key={convoId} convoId={convoId}>
|
||||||
<Inner />
|
<Inner />
|
||||||
</ConvoProvider>
|
</ConvoProvider>
|
||||||
|
|||||||
@@ -50,8 +50,7 @@ import {MessageItem} from '#/components/dms/MessageItem'
|
|||||||
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
import {IS_WEB} from '#/env'
|
|
||||||
import {ChatStatusInfo} from './ChatStatusInfo'
|
import {ChatStatusInfo} from './ChatStatusInfo'
|
||||||
import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed'
|
import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed'
|
||||||
|
|
||||||
@@ -421,6 +420,8 @@ export function MessagesList({
|
|||||||
{/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
|
{/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
|
||||||
<ScrollProvider onScroll={onScroll}>
|
<ScrollProvider onScroll={onScroll}>
|
||||||
<List
|
<List
|
||||||
|
contentInsetAdjustmentBehavior="automatic"
|
||||||
|
automaticallyAdjustsScrollIndicatorInsets
|
||||||
ref={flatListRef}
|
ref={flatListRef}
|
||||||
data={convoState.items}
|
data={convoState.items}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
|
|||||||
Reference in New Issue
Block a user