diff --git a/src/components/Layout/Header/index.ios.tsx b/src/components/Layout/Header/index.ios.tsx new file mode 100644 index 0000000000..c236c9fd3b --- /dev/null +++ b/src/components/Layout/Header/index.ios.tsx @@ -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 + } + + return +} + +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 ( + setHeaderWidth(evt.nativeEvent.layout.width)} + style={[ + a.flex_1, + a.flex_row, + a.align_center, + a.gap_sm, + a.px_xs, + animatedOpacity, + ]}> + {children} + + ) + }, [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() + 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 +} diff --git a/src/components/Layout/Header/index.shared.tsx b/src/components/Layout/Header/index.shared.tsx new file mode 100644 index 0000000000..471286e7da --- /dev/null +++ b/src/components/Layout/Header/index.shared.tsx @@ -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 + 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 + * + * ``` + * 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 ( + + {children} + + ) +} + +const AlignmentContext = createContext<'platform' | 'left'>('platform') +AlignmentContext.displayName = 'AlignmentContext' + +export function Content({ + children, + align = 'platform', +}: { + children?: React.ReactNode + align?: 'platform' | 'left' +}) { + return ( + + + {children} + + + ) +} + +export function Slot({children}: {children?: React.ReactNode}) { + return {children} +} + +export function BackButton({onPress, style, ...props}: Partial) { + const {_} = useLingui() + const navigation = useNavigation() + + const onPressBack = useCallback( + (evt: GestureResponderEvent) => { + onPress?.(evt) + if (evt.defaultPrevented) return + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } + }, + [onPress, navigation], + ) + + return ( + + + + ) +} + +export function MenuButton() { + const {_} = useLingui() + const setDrawerOpen = useSetDrawerOpen() + const {gtMobile} = useBreakpoints() + + const onPress = useCallback(() => { + Keyboard.dismiss() + setDrawerOpen(true) + }, [setDrawerOpen]) + + return gtMobile ? null : ( + + + + ) +} + +export function TitleText({ + children, + style, +}: {children: React.ReactNode} & TextStyleProp) { + const {gtMobile} = useBreakpoints() + const align = useContext(AlignmentContext) + return ( + + {children} + + ) +} + +export function SubtitleText({children}: {children: React.ReactNode}) { + const t = useTheme() + const align = useContext(AlignmentContext) + return ( + + {children} + + ) +} diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx index 3a20b76b36..3acc8f5be9 100644 --- a/src/components/Layout/Header/index.tsx +++ b/src/components/Layout/Header/index.tsx @@ -1,215 +1 @@ -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 function Outer({ - children, - noBottomBorder, - headerRef, - sticky = true, -}: { - children: React.ReactNode - noBottomBorder?: boolean - headerRef?: React.RefObject - sticky?: boolean -}) { - const t = useTheme() - const gutters = useGutters([0, 'base']) - const {gtMobile} = useBreakpoints() - const {isWithinOffsetView} = useContext(ScrollbarOffsetContext) - const {centerColumnOffset} = useLayoutBreakpoints() - - return ( - - {children} - - ) -} - -const AlignmentContext = createContext<'platform' | 'left'>('platform') -AlignmentContext.displayName = 'AlignmentContext' - -export function Content({ - children, - align = 'platform', -}: { - children?: React.ReactNode - align?: 'platform' | 'left' -}) { - return ( - - - {children} - - - ) -} - -export function Slot({children}: {children?: React.ReactNode}) { - return {children} -} - -export function BackButton({onPress, style, ...props}: Partial) { - const {_} = useLingui() - const navigation = useNavigation() - - const onPressBack = useCallback( - (evt: GestureResponderEvent) => { - onPress?.(evt) - if (evt.defaultPrevented) return - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, - [onPress, navigation], - ) - - return ( - - - - ) -} - -export function MenuButton() { - const {_} = useLingui() - const setDrawerOpen = useSetDrawerOpen() - const {gtMobile} = useBreakpoints() - - const onPress = useCallback(() => { - Keyboard.dismiss() - setDrawerOpen(true) - }, [setDrawerOpen]) - - return gtMobile ? null : ( - - - - ) -} - -export function TitleText({ - children, - style, -}: {children: React.ReactNode} & TextStyleProp) { - const {gtMobile} = useBreakpoints() - const align = useContext(AlignmentContext) - return ( - - {children} - - ) -} - -export function SubtitleText({children}: {children: React.ReactNode}) { - const t = useTheme() - const align = useContext(AlignmentContext) - return ( - - {children} - - ) -} +export * from './index.shared' diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index afdeee545e..4dc483f3c4 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -12,16 +12,18 @@ import {makeProfileLink} from '#/lib/routes/links' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {type Shadow} from '#/state/cache/profile-shadow' 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 {atoms as a, useTheme, web} from '#/alf' import {ConvoMenu} from '#/components/dms/ConvoMenu' import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2' import * as Layout from '#/components/Layout' import {Link} from '#/components/Link' -import {PostAlerts} from '#/components/moderation/PostAlerts' import {Text} from '#/components/Typography' -import {useSimpleVerificationState} from '#/components/verification' +import { + type SimpleVerificationState, + useSimpleVerificationState, +} from '#/components/verification' import {VerificationCheck} from '#/components/verification/VerificationCheck' import {IS_WEB} from '#/env' @@ -35,6 +37,10 @@ export function MessagesListHeader({ moderation?: ModerationDecision }) { const t = useTheme() + const convoState = useConvo() + const verification = useSimpleVerificationState({ + profile, + }) const blockInfo = useMemo(() => { if (!moderation) return @@ -49,8 +55,8 @@ export function MessagesListHeader({ }, [moderation]) return ( - - + + @@ -59,6 +65,8 @@ export function MessagesListHeader({ profile={profile} moderation={moderation} blockInfo={blockInfo} + convoState={convoState} + verification={verification} /> ) : ( <> @@ -101,6 +109,8 @@ function HeaderReady({ profile, moderation, blockInfo, + convoState, + verification, }: { profile: Shadow moderation: ModerationDecision @@ -108,13 +118,11 @@ function HeaderReady({ listBlocks: ModerationCause[] userBlock?: ModerationCause } + convoState: ConvoState + verification: SimpleVerificationState }) { const {_} = useLingui() const t = useTheme() - const convoState = useConvo() - const verification = useSimpleVerificationState({ - profile, - }) const isDeletedAccount = profile?.handle === 'missing.invalid' const displayName = isDeletedAccount @@ -136,90 +144,60 @@ function HeaderReady({ : undefined return ( - - - - - - - - {displayName} - - {verification.showBadge && ( - - - - )} - - {!isDeletedAccount && ( - - @{profile.handle} - {convoState.convo?.muted && ( - <> - {' '} - ·{' '} - - - )} - + + + + + + + {displayName} + + {verification.showBadge && ( + + + + )} + {convoState.convo?.muted && ( + <> + · + + )} - - - - - {isConvoActive(convoState) && ( - - )} - - + - - + + + {isConvoActive(convoState) && ( + + )} + ) diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx index be90ee8c1a..9c5d81646f 100644 --- a/src/screens/Messages/Conversation.tsx +++ b/src/screens/Messages/Conversation.tsx @@ -42,7 +42,7 @@ import {MessagesListHeader} from '#/components/dms/MessagesListHeader' import {Error} from '#/components/Error' import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' -import {IS_WEB} from '#/env' +import {IS_LIQUID_GLASS, IS_WEB} from '#/env' type Props = NativeStackScreenProps< CommonNavigatorParams, @@ -86,7 +86,10 @@ export function MessagesConversationScreenInner({route}: Props) { ) return ( - + diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index e06c9659c4..ef0ab122b7 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -50,8 +50,7 @@ import {MessageItem} from '#/components/dms/MessageItem' import {NewMessagesPill} from '#/components/dms/NewMessagesPill' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' -import {IS_NATIVE} from '#/env' -import {IS_WEB} from '#/env' +import {IS_NATIVE, IS_WEB} from '#/env' import {ChatStatusInfo} from './ChatStatusInfo' 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 */}