[Notifications] Add a Mentions tab (#7044)

* Split out NotificationsTab

* Remove unused route parameter

* Refine the split between components

* Hoist some logic out of NotificationFeed

* Remove unused option

* Add all|conversations to query, hardcode "all"

* Add a Conversations tab

* Rename to Mentions

* Bump packages

* Rename fields

* Fix oopsie

* Simplify header

* Track active tab

* Fix types

* Separate logic for tabs

* Better border for first unread

* Highlight unread for all only

* Fix spinner races

* Fix fetchPage races

* Fix bottom bar border being obscured by glimmer

* Remember last tab within the session

* One tab at a time

* Fix TS

* Handle all RQKEY usages

* Nit
This commit is contained in:
dan
2024-12-12 17:37:07 +00:00
committed by GitHub
parent 10e241e7eb
commit f8cdd6b9ae
15 changed files with 409 additions and 267 deletions
+11 -4
View File
@@ -239,14 +239,21 @@ export function useNotificationsHandler() {
)
logEvent('notifications:openApp', {})
invalidateCachedUnreadPage()
truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
const payload = e.notification.request.trigger
.payload as NotificationPayload
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
if (
payload.reason === 'mention' ||
payload.reason === 'quote' ||
payload.reason === 'reply'
) {
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
}
logger.debug('Notifications: handleNotification', {
content: e.notification.request.content,
payload: e.notification.request.trigger.payload,
})
handleNotification(
e.notification.request.trigger.payload as NotificationPayload,
)
handleNotification(payload)
Notifications.dismissAllNotificationsAsync()
}
})
+3 -3
View File
@@ -75,7 +75,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
}
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
Notifications: {show?: 'all'}
Notifications: undefined
}
export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
@@ -90,7 +90,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
Home: undefined
Search: {q?: string}
Feeds: undefined
Notifications: {show?: 'all'}
Notifications: undefined
Hashtag: {tag: string; author?: string}
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
}
@@ -102,7 +102,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
Search: {q?: string}
Feeds: undefined
NotificationsTab: undefined
Notifications: {show?: 'all'}
Notifications: undefined
MyProfileTab: undefined
Hashtag: {tag: string; author?: string}
MessagesTab: undefined
@@ -18,7 +18,13 @@ type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationSettings'>
export function NotificationSettingsScreen({}: Props) {
const {_} = useLingui()
const {data, isError: isQueryError, refetch} = useNotificationFeedQuery()
const {
data,
isError: isQueryError,
refetch,
} = useNotificationFeedQuery({
filter: 'all',
})
const serverPriority = data?.pages.at(0)?.priority
const {
+20 -14
View File
@@ -52,25 +52,22 @@ const PAGE_SIZE = 30
type RQPageParam = string | undefined
const RQKEY_ROOT = 'notification-feed'
export function RQKEY(priority?: false) {
return [RQKEY_ROOT, priority]
export function RQKEY(filter: 'all' | 'mentions') {
return [RQKEY_ROOT, filter]
}
export function useNotificationFeedQuery(opts?: {
export function useNotificationFeedQuery(opts: {
enabled?: boolean
overridePriorityNotifications?: boolean
filter: 'all' | 'mentions'
}) {
const agent = useAgent()
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
const unreads = useUnreadNotificationsApi()
const enabled = opts?.enabled !== false
const enabled = opts.enabled !== false
const filter = opts.filter
const {uris: hiddenReplyUris} = useThreadgateHiddenReplyUris()
// false: force showing all notifications
// undefined: let the server decide
const priority = opts?.overridePriorityNotifications ? false : undefined
const selectArgs = useMemo(() => {
return {
moderationOpts,
@@ -91,14 +88,23 @@ export function useNotificationFeedQuery(opts?: {
RQPageParam
>({
staleTime: STALE.INFINITY,
queryKey: RQKEY(priority),
queryKey: RQKEY(filter),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
let page
if (!pageParam) {
if (filter === 'all' && !pageParam) {
// for the first page, we check the cached page held by the unread-checker first
page = unreads.getCachedUnreadPage()
}
if (!page) {
let reasons: string[] = []
if (filter === 'mentions') {
reasons = [
// Anything that's a post
'mention',
'reply',
'quote',
]
}
const {page: fetchedPage} = await fetchPage({
agent,
limit: PAGE_SIZE,
@@ -106,13 +112,13 @@ export function useNotificationFeedQuery(opts?: {
queryClient,
moderationOpts,
fetchAdditionalData: true,
priority,
reasons,
})
page = fetchedPage
}
// if the first page has an unread, mark all read
if (!pageParam) {
if (filter === 'all' && !pageParam) {
// if the first page has an unread, mark all read
unreads.markAllRead()
}
+6 -3
View File
@@ -45,7 +45,8 @@ export function useNotificationSettingsMutation() {
},
onSettled: () => {
invalidateCachedUnreadPage()
queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()})
queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('all')})
queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('mentions')})
},
})
}
@@ -54,7 +55,7 @@ function eagerlySetCachedPriority(
queryClient: ReturnType<typeof useQueryClient>,
enabled: boolean,
) {
queryClient.setQueryData(RQKEY_NOTIFS(), (old: any) => {
function updateData(old: any) {
if (!old) return old
return {
...old,
@@ -65,5 +66,7 @@ function eagerlySetCachedPriority(
}
}),
}
})
}
queryClient.setQueryData(RQKEY_NOTIFS('all'), updateData)
queryClient.setQueryData(RQKEY_NOTIFS('mentions'), updateData)
}
+14 -2
View File
@@ -2,7 +2,7 @@
* A kind of companion API to ./feed.ts. See that file for more info.
*/
import React from 'react'
import React, {useRef} from 'react'
import {AppState} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3'
@@ -105,6 +105,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
}, [setNumUnread])
const isFetchingRef = useRef(false)
// create API
const api = React.useMemo<ApiContext>(() => {
return {
@@ -138,6 +140,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
}
if (isFetchingRef.current) {
return
}
// Do not move this without ensuring it gets a symmetrical reset in the finally block.
isFetchingRef.current = true
// count
const {page, indexedAt: lastIndexed} = await fetchPage({
agent,
@@ -145,6 +153,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
limit: 40,
queryClient,
moderationOpts,
reasons: [],
// only fetch subjects when the page is going to be used
// in the notifications query, otherwise skip it
@@ -174,11 +183,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
// update & broadcast
setNumUnread(unreadCountStr)
if (invalidate) {
truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
}
broadcast.postMessage({event: unreadCountStr})
} catch (e) {
logger.warn('Failed to check unread notifications', {error: e})
} finally {
isFetchingRef.current = false
}
},
+3 -2
View File
@@ -31,6 +31,7 @@ export async function fetchPage({
queryClient,
moderationOpts,
fetchAdditionalData,
reasons,
}: {
agent: BskyAgent
cursor: string | undefined
@@ -38,7 +39,7 @@ export async function fetchPage({
queryClient: QueryClient
moderationOpts: ModerationOpts | undefined
fetchAdditionalData: boolean
priority?: boolean
reasons: string[]
}): Promise<{
page: FeedPage
indexedAt: string | undefined
@@ -46,7 +47,7 @@ export async function fetchPage({
const res = await agent.listNotifications({
limit,
cursor,
// priority,
reasons,
})
const indexedAt = res.data.notifications[0]?.indexedAt
+2 -2
View File
@@ -8,7 +8,7 @@ import {
} from '@atproto/api'
import {InfiniteData, QueryClient, QueryKey} from '@tanstack/react-query'
export function truncateAndInvalidate<T = any>(
export async function truncateAndInvalidate<T = any>(
queryClient: QueryClient,
queryKey: QueryKey,
) {
@@ -21,7 +21,7 @@ export function truncateAndInvalidate<T = any>(
}
return data
})
queryClient.invalidateQueries({queryKey})
return queryClient.invalidateQueries({queryKey})
}
// Given an AtUri, this function will check if the AtUri matches a
+14 -19
View File
@@ -9,13 +9,11 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {usePalette} from '#/lib/hooks/usePalette'
import {cleanError} from '#/lib/strings/errors'
import {s} from '#/lib/styles'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
import {EmptyState} from '#/view/com/util/EmptyState'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {List, ListRef} from '#/view/com/util/List'
@@ -28,26 +26,26 @@ const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
const LOADING_ITEM = {_reactKey: '__loading__'}
export function NotificationFeed({
filter,
enabled,
scrollElRef,
onPressTryAgain,
onScrolledDownChange,
ListHeaderComponent,
overridePriorityNotifications,
refreshNotifications,
}: {
filter: 'all' | 'mentions'
enabled: boolean
scrollElRef?: ListRef
onPressTryAgain?: () => void
onScrolledDownChange: (isScrolledDown: boolean) => void
ListHeaderComponent?: () => JSX.Element
overridePriorityNotifications?: boolean
refreshNotifications: () => Promise<void>
}) {
const initialNumToRender = useInitialNumToRender()
const [isPTRing, setIsPTRing] = React.useState(false)
const pal = usePalette('default')
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const {checkUnread} = useUnreadNotificationsApi()
const {
data,
isFetching,
@@ -58,8 +56,8 @@ export function NotificationFeed({
isFetchingNextPage,
fetchNextPage,
} = useNotificationFeedQuery({
enabled: !!moderationOpts,
overridePriorityNotifications,
enabled: enabled && !!moderationOpts,
filter,
})
const isEmpty = !isFetching && !data?.pages[0]?.items.length
@@ -85,7 +83,7 @@ export function NotificationFeed({
const onRefresh = React.useCallback(async () => {
try {
setIsPTRing(true)
await checkUnread({invalidate: true})
await refreshNotifications()
} catch (err) {
logger.error('Failed to refresh notifications feed', {
message: err,
@@ -93,7 +91,7 @@ export function NotificationFeed({
} finally {
setIsPTRing(false)
}
}, [checkUnread, setIsPTRing])
}, [refreshNotifications, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
@@ -129,21 +127,18 @@ export function NotificationFeed({
/>
)
} else if (item === LOADING_ITEM) {
return (
<View style={[pal.border]}>
<NotificationFeedLoadingPlaceholder />
</View>
)
return <NotificationFeedLoadingPlaceholder />
}
return (
<NotificationFeedItem
highlightUnread={filter === 'all'}
item={item}
moderationOpts={moderationOpts!}
hideTopBorder={index === 0}
hideTopBorder={index === 0 && item.notification.isRead}
/>
)
},
[moderationOpts, _, onPressRetryLoadMore, pal.border],
[moderationOpts, _, onPressRetryLoadMore, filter],
)
const FeedFooter = React.useCallback(
@@ -79,10 +79,12 @@ interface Author {
let NotificationFeedItem = ({
item,
moderationOpts,
highlightUnread,
hideTopBorder,
}: {
item: FeedNotification
moderationOpts: ModerationOpts
highlightUnread: boolean
hideTopBorder?: boolean
}): React.ReactNode => {
const queryClient = useQueryClient()
@@ -151,6 +153,7 @@ let NotificationFeedItem = ({
if (!item.subject) {
return null
}
const isHighlighted = highlightUnread && !item.notification.isRead
return (
<Link
testID={`feedItem-by-${item.notification.author.handle}`}
@@ -160,12 +163,10 @@ let NotificationFeedItem = ({
<Post
post={item.subject}
style={
item.notification.isRead
? undefined
: {
backgroundColor: pal.colors.unreadNotifBg,
borderColor: pal.colors.unreadNotifBorder,
}
isHighlighted && {
backgroundColor: pal.colors.unreadNotifBg,
borderColor: pal.colors.unreadNotifBorder,
}
}
hideTopBorder={hideTopBorder}
/>
+8 -6
View File
@@ -136,12 +136,14 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
return (
<View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
{renderTabBar({
selectedPage,
onSelect: onTabBarSelect,
dragProgress,
dragState,
})}
<View style={a.z_10 /* Let tabbar bottom border cover the glimmer */}>
{renderTabBar({
selectedPage,
onSelect: onTabBarSelect,
dragProgress,
dragState,
})}
</View>
<GestureDetector gesture={nativeGesture}>
<AnimatedPagerView
ref={pagerView}
+7 -1
View File
@@ -872,7 +872,13 @@ function MockNotifItem({
</P>
)
}
return <NotificationFeedItem item={notif} moderationOpts={moderationOpts} />
return (
<NotificationFeedItem
item={notif}
moderationOpts={moderationOpts}
highlightUnread
/>
)
}
function MockAccountCard({
+175 -81
View File
@@ -13,7 +13,7 @@ import {
} from '#/lib/routes/types'
import {s} from '#/lib/styles'
import {logger} from '#/logger'
import {isNative, isWeb} from '#/platform/detection'
import {isNative} from '#/platform/detection'
import {emitSoftReset, listenSoftReset} from '#/state/events'
import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
import {
@@ -24,35 +24,173 @@ import {truncateAndInvalidate} from '#/state/queries/util'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {NotificationFeed} from '#/view/com/notifications/NotificationFeed'
import {Pager} from '#/view/com/pager/Pager'
import {TabBar} from '#/view/com/pager/TabBar'
import {FAB} from '#/view/com/util/fab/FAB'
import {ListMethods} from '#/view/com/util/List'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {atoms as a} from '#/alf'
import {web} from '#/alf'
import {ButtonIcon} from '#/components/Button'
import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
import * as Layout from '#/components/Layout'
import {Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
// We don't currently persist this across reloads since
// you gotta visit All to clear the badge anyway.
// But let's at least persist it during the sesssion.
let lastActiveTab = 0
type Props = NativeStackScreenProps<
NotificationsTabNavigatorParams,
'Notifications'
>
export function NotificationsScreen({route: {params}}: Props) {
const t = useTheme()
const {gtTablet} = useBreakpoints()
export function NotificationsScreen({}: Props) {
const {_} = useLingui()
const {openComposer} = useComposerControls()
const unreadNotifs = useUnreadNotifications()
const hasNew = !!unreadNotifs
const {checkUnread: checkUnreadAll} = useUnreadNotificationsApi()
const [isLoadingAll, setIsLoadingAll] = React.useState(false)
const [isLoadingMentions, setIsLoadingMentions] = React.useState(false)
const initialActiveTab = lastActiveTab
const [activeTab, setActiveTab] = React.useState(initialActiveTab)
const isLoading = activeTab === 0 ? isLoadingAll : isLoadingMentions
const onPageSelected = React.useCallback(
(index: number) => {
setActiveTab(index)
lastActiveTab = index
},
[setActiveTab],
)
const queryClient = useQueryClient()
const checkUnreadMentions = React.useCallback(
async ({invalidate}: {invalidate: boolean}) => {
if (invalidate) {
return truncateAndInvalidate(queryClient, NOTIFS_RQKEY('mentions'))
} else {
// Background polling is not implemented for the mentions tab.
// Just ignore it.
}
},
[queryClient],
)
const sections = React.useMemo(() => {
return [
{
title: _(msg`All`),
component: (
<NotificationsTab
filter="all"
isActive={activeTab === 0}
isLoading={isLoadingAll}
hasNew={hasNew}
setIsLoadingLatest={setIsLoadingAll}
checkUnread={checkUnreadAll}
/>
),
},
{
title: _(msg`Mentions`),
component: (
<NotificationsTab
filter="mentions"
isActive={activeTab === 1}
isLoading={isLoadingMentions}
hasNew={false /* We don't know for sure */}
setIsLoadingLatest={setIsLoadingMentions}
checkUnread={checkUnreadMentions}
/>
),
},
]
}, [
_,
hasNew,
checkUnreadAll,
checkUnreadMentions,
activeTab,
isLoadingAll,
isLoadingMentions,
])
return (
<Layout.Screen testID="notificationsScreen">
<Layout.Header.Outer noBottomBorder>
<Layout.Header.MenuButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Notifications</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot>
<Link
to="/notifications/settings"
label={_(msg`Notification settings`)}
size="small"
variant="ghost"
color="secondary"
shape="round"
style={[a.justify_center]}>
<ButtonIcon icon={isLoading ? Loader : SettingsIcon} size="lg" />
</Link>
</Layout.Header.Slot>
</Layout.Header.Outer>
<Pager
onPageSelected={onPageSelected}
renderTabBar={props => (
<Layout.Center style={web([a.sticky, a.z_10, {top: 0}])}>
<TabBar
{...props}
items={sections.map(section => section.title)}
onPressSelected={() => emitSoftReset()}
/>
</Layout.Center>
)}
initialPage={initialActiveTab}>
{sections.map((section, i) => (
<View key={i}>{section.component}</View>
))}
</Pager>
<FAB
testID="composeFAB"
onPress={() => openComposer({})}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
</Layout.Screen>
)
}
function NotificationsTab({
filter,
isActive,
isLoading,
hasNew,
checkUnread,
setIsLoadingLatest,
}: {
filter: 'all' | 'mentions'
isActive: boolean
isLoading: boolean
hasNew: boolean
checkUnread: ({invalidate}: {invalidate: boolean}) => Promise<void>
setIsLoadingLatest: (v: boolean) => void
}) {
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
const scrollElRef = React.useRef<ListMethods>(null)
const queryClient = useQueryClient()
const unreadNotifs = useUnreadNotifications()
const unreadApi = useUnreadNotificationsApi()
const hasNew = !!unreadNotifs
const isScreenFocused = useIsFocused()
const {openComposer} = useComposerControls()
const isFocusedAndActive = isScreenFocused && isActive
// event handlers
// =
@@ -65,16 +203,23 @@ export function NotificationsScreen({route: {params}}: Props) {
scrollToTop()
if (hasNew) {
// render what we have now
truncateAndInvalidate(queryClient, NOTIFS_RQKEY())
} else {
truncateAndInvalidate(queryClient, NOTIFS_RQKEY(filter))
} else if (!isLoading) {
// check with the server
setIsLoadingLatest(true)
unreadApi
.checkUnread({invalidate: true})
checkUnread({invalidate: true})
.catch(() => undefined)
.then(() => setIsLoadingLatest(false))
}
}, [scrollToTop, queryClient, unreadApi, hasNew, setIsLoadingLatest])
}, [
scrollToTop,
queryClient,
checkUnread,
hasNew,
isLoading,
setIsLoadingLatest,
filter,
])
const onFocusCheckLatest = useNonReactiveCallback(() => {
// on focus, check for latest, but only invalidate if the user
@@ -87,79 +232,36 @@ export function NotificationsScreen({route: {params}}: Props) {
// we're just going to look it up synchronously.
currentIsScrolledDown = window.scrollY > 200
}
unreadApi.checkUnread({invalidate: !currentIsScrolledDown})
checkUnread({invalidate: !currentIsScrolledDown})
})
// on-visible setup
// =
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
logger.debug('NotificationsScreen: Focus')
onFocusCheckLatest()
}, [setMinimalShellMode, onFocusCheckLatest]),
if (isFocusedAndActive) {
setMinimalShellMode(false)
logger.debug('NotificationsScreen: Focus')
onFocusCheckLatest()
}
}, [setMinimalShellMode, onFocusCheckLatest, isFocusedAndActive]),
)
React.useEffect(() => {
if (!isScreenFocused) {
if (!isFocusedAndActive) {
return
}
return listenSoftReset(onPressLoadLatest)
}, [onPressLoadLatest, isScreenFocused])
}, [onPressLoadLatest, isFocusedAndActive])
return (
<Layout.Screen testID="notificationsScreen">
<Layout.Header.Outer>
<Layout.Header.MenuButton />
<Layout.Header.Content>
<Button
label={_(msg`Notifications`)}
accessibilityHint={_(msg`Refresh notifications`)}
onPress={emitSoftReset}
style={[a.justify_start]}>
{({hovered}) => (
<Layout.Header.TitleText
style={[a.w_full, hovered && a.underline]}>
<Trans>Notifications</Trans>
{isWeb && gtTablet && hasNew && (
<View
style={[
a.rounded_full,
{
width: 8,
height: 8,
bottom: 3,
left: 6,
backgroundColor: t.palette.primary_500,
},
]}
/>
)}
</Layout.Header.TitleText>
)}
</Button>
</Layout.Header.Content>
<Layout.Header.Slot>
<Link
to="/notifications/settings"
label={_(msg`Notification settings`)}
size="small"
variant="ghost"
color="secondary"
shape="round"
style={[a.justify_center]}>
<ButtonIcon
icon={isLoadingLatest ? Loader : SettingsIcon}
size="lg"
/>
</Link>
</Layout.Header.Slot>
</Layout.Header.Outer>
<>
<MainScrollProvider>
<NotificationFeed
enabled={isFocusedAndActive}
filter={filter}
refreshNotifications={() => checkUnread({invalidate: true})}
onScrolledDownChange={setIsScrolledDown}
scrollElRef={scrollElRef}
overridePriorityNotifications={params?.show === 'all'}
/>
</MainScrollProvider>
{(isScrolledDown || hasNew) && (
@@ -169,14 +271,6 @@ export function NotificationsScreen({route: {params}}: Props) {
showIndicator={hasNew}
/>
)}
<FAB
testID="composeFAB"
onPress={() => openComposer({})}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
</Layout.Screen>
</>
)
}