From 935347c73d6058834bef08e2d3da86a7ded037ed Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:23:44 -0700 Subject: [PATCH] Use convo view instead of convo state for driving the UI (#10290) --- src/components/dms/MessagesListHeader.tsx | 96 +++++++--------- src/screens/Messages/Conversation.tsx | 47 ++++++-- src/screens/Messages/ConversationSettings.tsx | 104 +++++++++--------- 3 files changed, 127 insertions(+), 120 deletions(-) diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index c8fa7116dd..13e747231f 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -2,6 +2,7 @@ import {useMemo} from 'react' import {View} from 'react-native' import { type AppBskyActorDefs, + ChatBskyConvoDefs, type ModerationCause, type ModerationDecision, } from '@atproto/api' @@ -11,14 +12,7 @@ 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 {logger} from '#/logger' import {type Shadow} from '#/state/cache/profile-shadow' -import { - type ActiveConvoStates, - isConvoActive, - useConvo, -} from '#/state/messages/convo' -import {type ConvoItem} from '#/state/messages/convo/types' import {useSession} from '#/state/session' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' @@ -32,20 +26,22 @@ import {Link} from '#/components/Link' import {ProfileBadges} from '#/components/ProfileBadges' import {Text} from '#/components/Typography' import {IS_LIQUID_GLASS, IS_WEB} from '#/env' +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 moderation?: ModerationDecision | null }) { const t = useTheme() - const convoState = useConvo() - const isGroupChat = convoState?.isGroup?.() + const isGroupChat = convo?.kind === 'group' const blockInfo = useMemo(() => { if (!moderation) return @@ -65,17 +61,17 @@ export function MessagesListHeader({ - {isConvoActive(convoState) ? ( + {convo ? ( moderation && blockInfo && profile && !isGroupChat ? ( ) : ( @@ -111,12 +107,12 @@ export function MessagesListHeader({ } function ProfileHeaderReady({ - convoState, + convo, profile, moderation, blockInfo, }: { - convoState: ActiveConvoStates + convo: ConvoWithDetails profile: Shadow moderation: ModerationDecision blockInfo: { @@ -132,15 +128,10 @@ function ProfileHeaderReady({ ? l`Deleted Account` : createSanitizedDisplayName(profile, true, moderation.ui('displayName')) - const latestMessageFromOther = convoState.items.findLast( - (item: ConvoItem) => - item.type === 'message' && - item.message.sender.did !== currentAccount?.did, - ) - const latestReportableMessage = - latestMessageFromOther?.type === 'message' - ? latestMessageFromOther.message + ChatBskyConvoDefs.isMessageView(convo.view.lastMessage) && + convo.view.lastMessage.sender?.did !== currentAccount?.did + ? convo.view.lastMessage : undefined return ( @@ -164,28 +155,26 @@ function ProfileHeaderReady({ } - muted={convoState.convo?.muted} + muted={convo.view.muted} settings={ - isConvoActive(convoState) ? ( - - ) : null + } /> ) } function GroupHeaderReady({ - convoState, + convo, profile, moderation, }: { - convoState: ActiveConvoStates + convo: ConvoWithDetails profile?: Shadow moderation?: ModerationDecision | null }) { @@ -193,7 +182,7 @@ function GroupHeaderReady({ const navigation = useNavigation() - const groupInfo = convoState.getGroupInfo?.() + const groupInfo = convo.kind === 'group' ? convo.details : undefined const isDeletedAccount = profile?.handle === 'missing.invalid' const displayName = isDeletedAccount @@ -206,40 +195,33 @@ function GroupHeaderReady({ (displayName ? l`${displayName}’s group chat` : l`Group chat`) const handleNavigateToSettings = () => { - const convoId = convoState.convo?.id - if (convoId) { - navigation.navigate('MessagesConversationSettings', { - conversation: convoId, - }) - } else { - logger.error(`handleNavigateToSettings: missing convo ID`) - } + navigation.navigate('MessagesConversationSettings', { + conversation: convo.view.id, + }) } return ( - + {groupName} } - muted={convoState.convo?.muted} + muted={convo.view.muted} settings={ - isConvoActive(convoState) ? ( - - ) : null + } /> ) diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx index f56f612e28..5e390f7a0c 100644 --- a/src/screens/Messages/Conversation.tsx +++ b/src/screens/Messages/Conversation.tsx @@ -35,6 +35,7 @@ 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 {useSession} from '#/state/session' import {useSetMinimalShellMode} from '#/state/shell' import {MessagesList} from '#/screens/Messages/components/MessagesList' import {atoms as a, useTheme, web} from '#/alf' @@ -46,6 +47,7 @@ import { } from '#/components/dialogs/EmailDialog' import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter' import {MessagesListHeader} from '#/components/dms/MessagesListHeader' +import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util' import {Error} from '#/components/Error' import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' @@ -104,9 +106,14 @@ function Inner() { const t = useTheme() const convoState = useConvo() const {_} = useLingui() + const {currentAccount} = useSession() const isFocused = useIsFocused() const {top: topInset} = useSafeAreaInsets() + const convo = convoState.convo + ? parseConvoView(convoState.convo, currentAccount?.did) + : null + const moderationOpts = useModerationOpts() const {data: recipientUnshadowed} = useProfileQuery({ did: convoState.getPrimaryMember?.()?.did, @@ -144,9 +151,13 @@ function Inner() { {moderation ? ( - + ) : ( - + )} {moderation ? ( - + ) : ( - + )} )} @@ -178,6 +193,9 @@ function Inner() { recipient={recipient} hasScrolled={hasScrolled} setHasScrolled={setHasScrolled} + convo={convo} + isActive={isConvoActive(convoState)} + hasMessages={isConvoActive(convoState) && convoState.items.length > 0} /> {!readyToShow && ( | undefined hasScrolled: boolean setHasScrolled: React.Dispatch> + convo: ConvoWithDetails | null + isActive: boolean + hasMessages: boolean }) { - const convoState = useConvo() const navigation = useNavigation() const {top: topInset} = useSafeAreaInsets() const [headerHeight, setHeaderHeight] = useState(0) @@ -262,7 +285,11 @@ function InnerReady({ }, [maybeBlockForEmailVerification]) const header = ( - + ) return ( @@ -277,7 +304,7 @@ function InnerReady({ ) : ( header )} - {isConvoActive(convoState) && ( + {isActive && ( 0} + convoId={convo.view.id} + hasMessages={hasMessages} moderation={moderation} /> ) : null diff --git a/src/screens/Messages/ConversationSettings.tsx b/src/screens/Messages/ConversationSettings.tsx index c926afb62d..8da97a31ec 100644 --- a/src/screens/Messages/ConversationSettings.tsx +++ b/src/screens/Messages/ConversationSettings.tsx @@ -1,6 +1,6 @@ import {useMemo, useState} from 'react' import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native' -import {type ChatBskyConvoDefs, moderateProfile} from '@atproto/api' +import {moderateProfile} from '@atproto/api' import {plural} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro' import {StackActions, useNavigation} from '@react-navigation/native' @@ -34,6 +34,7 @@ import {AvatarBubbles} from '#/components/AvatarBubbles' import {Button, type ButtonColor, ButtonIcon} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {AddMembersFlow} from '#/components/dms/AddMembersFlow' +import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util' import {Error} from '#/components/Error' import * as TextField from '#/components/forms/TextField' import {useInteractionState} from '#/components/hooks/useInteractionState' @@ -126,9 +127,14 @@ function SettingsInner() { const convoState = useConvo() const {currentAccount} = useSession() - const primaryMember = convoState?.getPrimaryMember?.() - const data: bsky.profile.AnyProfileView[] = convoState.convo?.members ?? [] + const convo = convoState.convo + ? parseConvoView(convoState.convo, currentAccount?.did) + : null + const primaryMember = convo?.primaryMember + const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did + + const data: bsky.profile.AnyProfileView[] = convo?.members ?? [] const invites: string[] = [] const items = [ @@ -163,11 +169,23 @@ function SettingsInner() { function renderItem({item}: {item: Item}) { switch (item.type) { case 'MEMBERS_AND_REQUESTS': - return + return ( + + ) case 'ADD_MEMBERS_LINK': - return + return case 'CHAT_MEMBER': - return + return ( + + ) default: return null } @@ -194,8 +212,8 @@ function SettingsInner() { initialNumToRender={initialNumToRender} keyExtractor={keyExtractor} ListHeaderComponent={ - convoState.convo ? ( - + convo ? ( + ) : ( ) @@ -211,21 +229,15 @@ function SettingsInner() { function MembersAndRequests({ memberCount, requestCount, + isOwner, }: { memberCount: number requestCount: number + isOwner: boolean }) { const t = useTheme() const {t: l} = useLingui() - const convoState = useConvo() - const {currentAccount} = useSession() - - const isOwner = - currentAccount?.did == null - ? false - : convoState.getPrimaryMember?.()?.did === currentAccount.did - return ( @@ -254,20 +266,12 @@ function MembersAndRequests({ ) } -function AddMembersLink() { +function AddMembersLink({isOwner}: {isOwner: boolean}) { const t = useTheme() const {t: l} = useLingui() - const convoState = useConvo() - const {currentAccount} = useSession() - const addMembersControl = Dialog.useDialogControl() - const isOwner = - currentAccount?.did == null - ? false - : convoState.getPrimaryMember?.()?.did === currentAccount.did - if (!isOwner) { return null } @@ -354,9 +358,11 @@ function AddMembersLink() { function Member({ profile, status, + isOwner, }: { profile: Shadow status: 'owner' | 'member' | 'invited' + isOwner: boolean }) { const navigation = useNavigation() const t = useTheme() @@ -388,7 +394,9 @@ function Member({ break } } else { - statusBadge = + statusBadge = ( + + ) } return ( @@ -496,9 +504,11 @@ function StatusButton({ function MemberMenu({ profile, type, + isOwner, }: { profile: Shadow type: 'owner' | 'member' | 'invited' + isOwner: boolean }) { const navigation = useNavigation() const t = useTheme() @@ -506,16 +516,9 @@ function MemberMenu({ const ax = useAnalytics() const requireEmailVerification = useRequireEmailVerification() - const convoState = useConvo() - const {currentAccount} = useSession() const blockMemberPrompt = Prompt.usePromptControl() - const isOwner = - currentAccount?.did == null - ? false - : convoState.getPrimaryMember?.()?.did === currentAccount.did - const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did) const {mutate: initiateConvo} = useGetConvoForMembers({ onSuccess: ({convo}) => { @@ -706,29 +709,22 @@ function MemberMenu({ function SettingsHeader({ convo, - profiles, + isOwner, }: { - convo: ChatBskyConvoDefs.ConvoView - profiles: bsky.profile.AnyProfileView[] + convo: ConvoWithDetails + isOwner: boolean }) { const t = useTheme() const {t: l} = useLingui() const navigation = useNavigation() - const convoState = useConvo() - const {currentAccount} = useSession() - const groupName = convoState.getGroupInfo?.()?.name ?? '' + const groupName = convo.kind === 'group' ? convo.details.name : '' const [newGroupName, setNewGroupName] = useState(groupName) const [isLocked, setIsLocked] = useState(false) - const isOwner = - currentAccount?.did == null - ? false - : convoState.getPrimaryMember?.()?.did === currentAccount.did - - const {mutate: editGroupName} = useEditGroupName(convo.id, { + const {mutate: editGroupName} = useEditGroupName(convo.view.id, { onError: e => { setNewGroupName(groupName) logger.error('Failed to edit group chat name', {message: e}) @@ -738,7 +734,7 @@ function SettingsHeader({ }, }) - const {mutate: muteConvo} = useMuteConvo(convo.id, { + const {mutate: muteConvo} = useMuteConvo(convo.view.id, { onSuccess: data => { if (data.convo.muted) { Toast.show(l({message: 'Group chat muted', context: 'toast'})) @@ -754,7 +750,7 @@ function SettingsHeader({ }, }) - const {mutate: leaveConvo} = useLeaveConvo(convo.id, { + const {mutate: leaveConvo} = useLeaveConvo(convo.view.id, { onMutate: () => { navigation.dispatch(StackActions.pop(2)) }, @@ -772,7 +768,7 @@ function SettingsHeader({ const leaveChatPrompt = Prompt.usePromptControl() const handleToggleMute = () => { - muteConvo({mute: !convo?.muted}) + muteConvo({mute: !convo.view.muted}) } const handleLeaveChat = () => { @@ -815,7 +811,7 @@ function SettingsHeader({ - + {isOwner ? (