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 */}