[Chat] Remove convoState dependency from header (#10293)

This commit is contained in:
Samuel Newman
2026-04-18 12:58:16 -07:00
committed by GitHub
parent 35411e88c9
commit bc3672ceeb
7 changed files with 66 additions and 142 deletions
+1 -1
View File
@@ -190,7 +190,7 @@ function MenuContent({
const isDeletedAccount = profile.handle === 'missing.invalid'
const convoId = initialConvo.id
const {data: convo} = useConvoQuery(initialConvo)
const {data: convo} = useConvoQuery({convoId})
const onNavigateToProfile = useCallback(() => {
navigation.navigate('Profile', {name: profile.did})
+29 -68
View File
@@ -1,10 +1,9 @@
import {useMemo} from 'react'
import {View} from 'react-native'
import {
type AppBskyActorDefs,
ChatBskyConvoDefs,
type ModerationCause,
type ModerationDecision,
moderateProfile,
type ModerationOpts,
} from '@atproto/api'
import {useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
@@ -12,7 +11,8 @@ import {useNavigation} from '@react-navigation/native'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {makeProfileLink} from '#/lib/routes/links'
import {type NavigationProp} from '#/lib/routes/types'
import {type Shadow} from '#/state/cache/profile-shadow'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
@@ -30,30 +30,9 @@ import {type ConvoWithDetails} from './util'
const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE
export function MessagesListHeader({
convo,
profile,
moderation,
}: {
convo?: ConvoWithDetails | null
profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed>
moderation?: ModerationDecision | null
}) {
export function MessagesListHeader({convo}: {convo?: ConvoWithDetails | null}) {
const t = useTheme()
const isGroupChat = convo?.kind === 'group'
const blockInfo = useMemo(() => {
if (!moderation) return
const modui = moderation.ui('profileView')
const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
const listBlocks = blocks.filter(alert => alert.source.type === 'list')
const userBlock = blocks.find(alert => alert.source.type === 'user')
return {
listBlocks,
userBlock,
}
}, [moderation])
const moderationOpts = useModerationOpts()
return (
<Layout.Header.Outer noBottomBorder={IS_LIQUID_GLASS}>
@@ -61,20 +40,11 @@ export function MessagesListHeader({
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
<Layout.Header.BackButton />
</View>
{convo ? (
moderation && blockInfo && profile && !isGroupChat ? (
<ProfileHeaderReady
convo={convo}
profile={profile}
moderation={moderation}
blockInfo={blockInfo}
/>
{convo && moderationOpts ? (
convo.kind === 'direct' ? (
<ProfileHeaderReady convo={convo} moderationOpts={moderationOpts} />
) : (
<GroupHeaderReady
convo={convo}
profile={profile}
moderation={moderation}
/>
<GroupHeaderReady convo={convo} />
)
) : (
<>
@@ -108,20 +78,27 @@ export function MessagesListHeader({
function ProfileHeaderReady({
convo,
profile,
moderation,
blockInfo,
moderationOpts,
}: {
convo: ConvoWithDetails
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
moderation: ModerationDecision
blockInfo: {
listBlocks: ModerationCause[]
userBlock?: ModerationCause
}
convo: Extract<ConvoWithDetails, {kind: 'direct'}>
moderationOpts: ModerationOpts
}) {
const {t: l} = useLingui()
const {currentAccount} = useSession()
const profile = useProfileShadow(convo.primaryMember)
const moderation = moderateProfile(profile, moderationOpts)
const blockInfo = useMemo(() => {
const modui = moderation.ui('profileView')
const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
const listBlocks = blocks.filter(alert => alert.source.type === 'list')
const userBlock = blocks.find(alert => alert.source.type === 'user')
return {
listBlocks,
userBlock,
}
}, [moderation])
const isDeletedAccount = profile?.handle === 'missing.invalid'
const displayName = isDeletedAccount
@@ -171,29 +148,13 @@ function ProfileHeaderReady({
function GroupHeaderReady({
convo,
profile,
moderation,
}: {
convo: ConvoWithDetails
profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed>
moderation?: ModerationDecision | null
convo: Extract<ConvoWithDetails, {kind: 'group'}>
}) {
const {t: l} = useLingui()
const navigation = useNavigation<NavigationProp>()
const groupInfo = convo.kind === 'group' ? convo.details : undefined
const isDeletedAccount = profile?.handle === 'missing.invalid'
const displayName = isDeletedAccount
? l`Deleted Account`
: profile
? createSanitizedDisplayName(profile, true, moderation?.ui('displayName'))
: undefined
const groupName =
groupInfo?.name ??
(displayName ? l`${displayName}s group chat` : l`Group chat`)
const handleNavigateToSettings = () => {
navigation.navigate('MessagesConversationSettings', {
conversation: convo.view.id,
@@ -206,7 +167,7 @@ function GroupHeaderReady({
<>
<AvatarBubbles size="small" profiles={convo.members} />
<Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}>
{groupName}
{convo.details.name}
</Text>
</>
}
+2 -2
View File
@@ -56,12 +56,12 @@ export function hasReachedReactionLimit(
return myReactions.length >= EMOJI_REACTION_LIMIT
}
type GroupConvoMember = ChatBskyActorDefs.ProfileViewBasic & {
export type GroupConvoMember = ChatBskyActorDefs.ProfileViewBasic & {
// can be missing if account deleted
kind?: $Typed<ChatBskyActorDefs.GroupConvoMember>
}
type DirectConvoMember = ChatBskyActorDefs.ProfileViewBasic & {
export type DirectConvoMember = ChatBskyActorDefs.ProfileViewBasic & {
kind: $Typed<ChatBskyActorDefs.DirectConvoMember>
}
+28 -58
View File
@@ -1,11 +1,7 @@
import {useCallback, useEffect, useMemo, useState} from 'react'
import {type LayoutChangeEvent, View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {
type AppBskyActorDefs,
moderateProfile,
type ModerationDecision,
} from '@atproto/api'
import {moderateProfile} from '@atproto/api'
import {
ScrollEdgeEffect,
ScrollEdgeEffectProvider,
@@ -28,13 +24,13 @@ import {
type CommonNavigatorParams,
type NavigationProp,
} from '#/lib/routes/types'
import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow'
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
import {useEmail} from '#/state/email-verification'
import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
import {ConvoStatus} from '#/state/messages/convo/types'
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfileQuery} from '#/state/queries/profile'
import {useConvoQuery} from '#/state/queries/messages/conversation'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {MessagesList} from '#/screens/Messages/components/MessagesList'
@@ -52,6 +48,7 @@ import {Error} from '#/components/Error'
import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader'
import {IS_LIQUID_GLASS, IS_WEB} from '#/env'
import {ChatDisabled} from './components/ChatDisabled'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
@@ -95,36 +92,26 @@ export function MessagesConversationScreenInner({route}: Props) {
style={web([{minHeight: 0}, a.flex_1])}>
<ScrollEdgeEffectProvider>
<ConvoProvider key={convoId} convoId={convoId}>
<Inner />
<Inner convoId={convoId} />
</ConvoProvider>
</ScrollEdgeEffectProvider>
</Layout.Screen>
)
}
function Inner() {
function Inner({convoId}: {convoId: string}) {
const t = useTheme()
const convoState = useConvo()
const {_} = useLingui()
const {currentAccount} = useSession()
const isFocused = useIsFocused()
const {top: topInset} = useSafeAreaInsets()
const {data: convoData} = useConvoQuery({convoId})
const convo = convoState.convo
? parseConvoView(convoState.convo, currentAccount?.did)
const convo = convoData
? parseConvoView(convoData, currentAccount?.did)
: null
const moderationOpts = useModerationOpts()
const {data: recipientUnshadowed} = useProfileQuery({
did: convoState.getPrimaryMember?.()?.did,
})
const recipient = useMaybeProfileShadow(recipientUnshadowed)
const moderation = useMemo(() => {
if (!recipient || !moderationOpts) return null
return moderateProfile(recipient, moderationOpts)
}, [recipient, moderationOpts])
// Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user,
// we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be
// empty. So, we also check for that possible state as well and render once we can.
@@ -150,15 +137,7 @@ function Inner() {
<>
<Layout.Center
style={[a.w_full, IS_LIQUID_GLASS && {paddingTop: topInset}]}>
{moderation ? (
<MessagesListHeader
convo={convo}
profile={recipient}
moderation={moderation}
/>
) : (
<MessagesListHeader convo={convo} />
)}
<MessagesListHeader convo={convo} />
</Layout.Center>
<Error
title={_(msg`Something went wrong`)}
@@ -176,25 +155,16 @@ function Inner() {
{isFocused && IS_WEB && <RemoveScrollBar />}
{!readyToShow && (
<View style={IS_LIQUID_GLASS && {paddingTop: topInset}}>
{moderation ? (
<MessagesListHeader
convo={convo}
profile={recipient}
moderation={moderation}
/>
) : (
<MessagesListHeader convo={convo} />
)}
<MessagesListHeader convo={convo} />
</View>
)}
<View style={[a.flex_1]}>
<InnerReady
moderation={moderation}
recipient={recipient}
convo={convo}
hasScrolled={hasScrolled}
setHasScrolled={setHasScrolled}
convo={convo}
isActive={isConvoActive(convoState)}
isDisabled={convoState.status === ConvoStatus.Disabled}
hasMessages={isConvoActive(convoState) && convoState.items.length > 0}
/>
{!readyToShow && (
@@ -219,20 +189,18 @@ function Inner() {
}
function InnerReady({
moderation,
recipient,
hasScrolled,
setHasScrolled,
convo,
isActive,
isDisabled,
hasMessages,
}: {
moderation: ModerationDecision | null
recipient: Shadow<AppBskyActorDefs.ProfileViewDetailed> | undefined
hasScrolled: boolean
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
convo: ConvoWithDetails | null
isActive: boolean
isDisabled: boolean
hasMessages: boolean
}) {
const navigation = useNavigation<NavigationProp>()
@@ -284,13 +252,14 @@ function InnerReady({
maybeBlockForEmailVerification()
}, [maybeBlockForEmailVerification])
const header = (
<MessagesListHeader
convo={convo}
profile={recipient}
moderation={moderation}
/>
)
const primaryMember = useMaybeProfileShadow(convo?.primaryMember)
const moderationOpts = useModerationOpts()
const primaryMemberModeration = useMemo(() => {
if (!primaryMember || !moderationOpts) return null
return moderateProfile(primaryMember, moderationOpts)
}, [primaryMember, moderationOpts])
const header = <MessagesListHeader convo={convo} />
return (
<>
@@ -308,16 +277,17 @@ function InnerReady({
<MessagesList
hasScrolled={hasScrolled}
setHasScrolled={setHasScrolled}
blocked={moderation?.blocked}
hasAcceptOverride={!!params.accept}
transparentHeaderHeight={IS_LIQUID_GLASS ? headerHeight : 0}
footer={
moderation && recipient && convo ? (
isDisabled ? (
<ChatDisabled />
) : convo && primaryMember && primaryMemberModeration?.blocked ? (
<MessagesListBlockedFooter
recipient={recipient}
recipient={primaryMember}
convoId={convo.view.id}
hasMessages={hasMessages}
moderation={moderation}
moderation={primaryMemberModeration}
/>
) : null
}
@@ -482,6 +482,7 @@ function BaseChatItem({
]
: undefined
}
onPressIn={() => precacheConvoQuery(queryClient, convo)}
onPress={onPress}
onLongPress={showMenu && IS_NATIVE ? onLongPress : undefined}
onAccessibilityAction={showMenu ? onLongPress : undefined}>
@@ -50,7 +50,6 @@ import {
import {useGetPost} from '#/state/queries/post'
import {useAgent} from '#/state/session'
import {List, type ListMethods} from '#/view/com/util/List'
import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled'
import {MessageComposer} from '#/screens/Messages/components/MessageComposer'
import {MessageInput} from '#/screens/Messages/components/MessageInput'
import {MessageListError} from '#/screens/Messages/components/MessageListError'
@@ -93,14 +92,12 @@ function onScrollToIndexFailed() {
export function MessagesList({
hasScrolled,
setHasScrolled,
blocked,
footer,
hasAcceptOverride,
transparentHeaderHeight,
}: {
hasScrolled: boolean
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
blocked?: boolean
footer?: React.ReactNode
hasAcceptOverride?: boolean
transparentHeaderHeight?: number
@@ -489,11 +486,7 @@ export function MessagesList({
}),
opened: 0,
}}>
{convoState.status === ConvoStatus.Disabled ? (
<ChatDisabled />
) : blocked ? (
footer
) : (
{footer ?? (
<ConversationFooter
convoState={convoState}
hasAcceptOverride={hasAcceptOverride}>
+4 -5
View File
@@ -19,19 +19,18 @@ import {
const RQKEY_ROOT = 'convo'
export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
export function useConvoQuery(convo: ChatBskyConvoDefs.ConvoView) {
export function useConvoQuery({convoId}: {convoId: string}) {
const agent = useAgent()
return useQuery({
queryKey: RQKEY(convo.id),
queryKey: RQKEY(convoId),
queryFn: async () => {
const {data} = await agent.chat.bsky.convo.getConvo(
{convoId: convo.id},
{convoId},
{headers: DM_SERVICE_HEADERS},
)
return data.convo
},
initialData: convo,
staleTime: STALE.INFINITY,
})
}
@@ -58,7 +57,7 @@ export function useMarkAsReadMutation() {
}) => {
if (!convoId) throw new Error('No convoId provided')
await agent.api.chat.bsky.convo.updateRead(
await agent.chat.bsky.convo.updateRead(
{
convoId,
messageId,