[Chat] Keep track of relatedProfiles in Convo agent (#10354)

This commit is contained in:
Samuel Newman
2026-04-24 14:55:04 +03:00
committed by GitHub
parent 2215e4461f
commit 263474a0b9
11 changed files with 82 additions and 71 deletions
+7 -1
View File
@@ -4,23 +4,29 @@ import {useLingui} from '@lingui/react/macro'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {MessageContextMenu} from '#/components/dms/MessageContextMenu' import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
import type * as bsky from '#/types/bsky'
export function ActionsWrapper({ export function ActionsWrapper({
message, message,
isFromSelf, isFromSelf,
senderProfile,
children, children,
onTap, onTap,
}: { }: {
message: ChatBskyConvoDefs.MessageView message: ChatBskyConvoDefs.MessageView
hasReactions?: boolean hasReactions?: boolean
isFromSelf: boolean isFromSelf: boolean
senderProfile?: bsky.profile.AnyProfileView
children: React.ReactNode children: React.ReactNode
onTap?: () => void onTap?: () => void
}) { }) {
const {t: l} = useLingui() const {t: l} = useLingui()
return ( return (
<MessageContextMenu message={message} onTap={onTap}> <MessageContextMenu
message={message}
senderProfile={senderProfile}
onTap={onTap}>
{trigger => {trigger =>
// will always be true, since this file is platform split // will always be true, since this file is platform split
trigger.IS_NATIVE && ( trigger.IS_NATIVE && (
+4 -1
View File
@@ -10,6 +10,7 @@ import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid' import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
import {EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji' import {EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import type * as bsky from '#/types/bsky'
import {EmojiReactionPicker} from './EmojiReactionPicker' import {EmojiReactionPicker} from './EmojiReactionPicker'
import {hasReachedReactionLimit} from './util' import {hasReachedReactionLimit} from './util'
@@ -17,12 +18,14 @@ export function ActionsWrapper({
message, message,
hasReactions, hasReactions,
isFromSelf, isFromSelf,
senderProfile,
children, children,
onTap, onTap,
}: { }: {
message: ChatBskyConvoDefs.MessageView message: ChatBskyConvoDefs.MessageView
hasReactions?: boolean hasReactions?: boolean
isFromSelf: boolean isFromSelf: boolean
senderProfile?: bsky.profile.AnyProfileView
children: React.ReactNode children: React.ReactNode
onTap?: () => void onTap?: () => void
}) { }) {
@@ -114,7 +117,7 @@ export function ActionsWrapper({
) )
}} }}
</EmojiReactionPicker> </EmojiReactionPicker>
<MessageContextMenu message={message}> <MessageContextMenu message={message} senderProfile={senderProfile}>
{({props, state, IS_NATIVE, control}) => { {({props, state, IS_NATIVE, control}) => {
// always false, file is platform split // always false, file is platform split
if (IS_NATIVE) return null if (IS_NATIVE) return null
+1 -1
View File
@@ -4,7 +4,7 @@ import {Trans, useLingui} from '@lingui/react/macro'
import {subDays} from 'date-fns' import {subDays} from 'date-fns'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Text} from '../Typography' import {Text} from '#/components/Typography'
import {localDateString} from './util' import {localDateString} from './util'
const timeFormatter = new Intl.DateTimeFormat(undefined, { const timeFormatter = new Intl.DateTimeFormat(undefined, {
+4 -3
View File
@@ -25,15 +25,18 @@ import {usePromptControl} from '#/components/Prompt'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env' import {IS_NATIVE} from '#/env'
import type * as bsky from '#/types/bsky'
import {EmojiReactionPicker} from './EmojiReactionPicker' import {EmojiReactionPicker} from './EmojiReactionPicker'
import {hasReachedReactionLimit} from './util' import {hasReachedReactionLimit} from './util'
export let MessageContextMenu = ({ export let MessageContextMenu = ({
message, message,
senderProfile,
children, children,
onTap, onTap,
}: { }: {
message: ChatBskyConvoDefs.MessageView message: ChatBskyConvoDefs.MessageView
senderProfile?: bsky.profile.AnyProfileView
children: TriggerProps['children'] children: TriggerProps['children']
onTap?: () => void onTap?: () => void
}): React.ReactNode => { }): React.ReactNode => {
@@ -110,9 +113,7 @@ export let MessageContextMenu = ({
[l, convo, message, currentAccount?.did], [l, convo, message, currentAccount?.did],
) )
const sender = convo.convo.members.find( const sender = senderProfile
member => member.did === message.sender.did,
)
return ( return (
<> <>
+19 -15
View File
@@ -30,7 +30,6 @@ import {useQueryClient} from '@tanstack/react-query'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {useConvoActive} from '#/state/messages/convo'
import {type ConvoItem} from '#/state/messages/convo/types' import {type ConvoItem} from '#/state/messages/convo/types'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache'
@@ -43,7 +42,6 @@ import {InlineLinkText, Link} from '#/components/Link'
import * as ProfileCard from '#/components/ProfileCard' import * as ProfileCard from '#/components/ProfileCard'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
import {DateDivider} from './DateDivider' import {DateDivider} from './DateDivider'
import {useDateDividerToggle} from './DateDividerToggle' import {useDateDividerToggle} from './DateDividerToggle'
import {MessageItemEmbed} from './MessageItemEmbed' import {MessageItemEmbed} from './MessageItemEmbed'
@@ -93,19 +91,18 @@ function isWithinClusterBoundary({
let MessageItem = ({ let MessageItem = ({
item, item,
isGroupChat = false, isGroupChat = false,
profile,
}: { }: {
item: ConvoItem & {type: 'message' | 'pending-message'} item: ConvoItem & {type: 'message' | 'pending-message'}
isGroupChat?: boolean isGroupChat?: boolean
profile?: bsky.profile.AnyProfileView
}): React.ReactNode => { }): React.ReactNode => {
const t = useTheme() const t = useTheme()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {t: l} = useLingui() const {t: l} = useLingui()
const {convo} = useConvoActive()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const profile = item.relatedProfiles.get(item.message.sender.did)
const reactionsControl = useDialogControl() const reactionsControl = useDialogControl()
const reactionTapRef = useRef(false) const reactionTapRef = useRef(false)
@@ -277,9 +274,7 @@ let MessageItem = ({
return l`You reacted ${reaction.value}` return l`You reacted ${reaction.value}`
} else { } else {
const senderDid = reaction.sender.did const senderDid = reaction.sender.did
const memberSender = convo.members.find( const memberSender = item.relatedProfiles.get(senderDid)
member => member.did === senderDid,
)
if (memberSender) { if (memberSender) {
return l`${createSanitizedDisplayName(memberSender)} reacted ${reaction.value}` return l`${createSanitizedDisplayName(memberSender)} reacted ${reaction.value}`
} }
@@ -290,7 +285,13 @@ let MessageItem = ({
one: '# person', one: '# person',
other: '# people', other: '# people',
})} reacted – ${groupedReactions.map(g => g.value).join(' ')}` })} reacted – ${groupedReactions.map(g => g.value).join(' ')}`
}, [reactions, groupedReactions, currentAccount?.did, convo.members, l]) }, [
reactions,
groupedReactions,
currentAccount?.did,
item.relatedProfiles,
l,
])
const appliedReactions = ( const appliedReactions = (
<LayoutAnimationConfig skipEntering skipExiting> <LayoutAnimationConfig skipEntering skipExiting>
@@ -375,7 +376,7 @@ let MessageItem = ({
) : null} ) : null}
<ReactionsDialog <ReactionsDialog
control={reactionsControl} control={reactionsControl}
members={convo.members} relatedProfiles={item.relatedProfiles}
message={message} message={message}
reactions={message.reactions} reactions={message.reactions}
groupedReactions={groupedReactions} groupedReactions={groupedReactions}
@@ -391,11 +392,13 @@ let MessageItem = ({
return ( return (
<> <>
{(hasLargeGapFromPrev || isDateDividerToggled) && ( <LayoutAnimationConfig skipExiting skipEntering>
<Animated.View entering={native(FadeIn)} exiting={native(FadeOut)}> {(hasLargeGapFromPrev || isDateDividerToggled) && (
<DateDivider date={message.sentAt} /> <Animated.View entering={native(FadeIn)} exiting={native(FadeOut)}>
</Animated.View> <DateDivider date={message.sentAt} />
)} </Animated.View>
)}
</LayoutAnimationConfig>
<View style={[messageInset, effectiveFirstInCluster && a.mt_md]}> <View style={[messageInset, effectiveFirstInCluster && a.mt_md]}>
<View style={[a.relative]}> <View style={[a.relative]}>
{showAvatar ? ( {showAvatar ? (
@@ -434,6 +437,7 @@ let MessageItem = ({
hasReactions={hasReactions} hasReactions={hasReactions}
isFromSelf={isFromSelf} isFromSelf={isFromSelf}
message={message} message={message}
senderProfile={profile}
onTap={() => { onTap={() => {
if (reactionTapRef.current) return if (reactionTapRef.current) return
if (!hasLargeGapFromPrev) { if (!hasLargeGapFromPrev) {
+4 -4
View File
@@ -7,7 +7,7 @@ import {
View, View,
} from 'react-native' } from 'react-native'
import Animated from 'react-native-reanimated' import Animated from 'react-native-reanimated'
import {type ChatBskyConvoDefs} from '@atproto/api' import {type ChatBskyActorDefs, type ChatBskyConvoDefs} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
@@ -33,13 +33,13 @@ type Reaction = {
export function ReactionsDialog({ export function ReactionsDialog({
control, control,
members, relatedProfiles,
message, message,
reactions, reactions,
groupedReactions, groupedReactions,
}: { }: {
control: Dialog.DialogControlProps control: Dialog.DialogControlProps
members: bsky.profile.AnyProfileView[] relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
message: ChatBskyConvoDefs.MessageView message: ChatBskyConvoDefs.MessageView
reactions?: ChatBskyConvoDefs.ReactionView[] reactions?: ChatBskyConvoDefs.ReactionView[]
groupedReactions?: Reaction[] groupedReactions?: Reaction[]
@@ -100,7 +100,7 @@ export function ReactionsDialog({
return 0 return 0
}) })
.map(reaction => { .map(reaction => {
const sender = members.find(m => m.did === reaction.sender.did) const sender = relatedProfiles.get(reaction.sender.did)
if (!sender) return null if (!sender) return null
return ( return (
<ReactionRow <ReactionRow
+3 -3
View File
@@ -23,15 +23,15 @@ export type SystemMessageInfo = {
function getReferredDisplayName( function getReferredDisplayName(
user: ChatBskyConvoDefs.SystemMessageReferredUser, user: ChatBskyConvoDefs.SystemMessageReferredUser,
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[], relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>,
): string | null { ): string | null {
const profile = relatedProfiles.find(p => p.did === user.did) const profile = relatedProfiles.get(user.did)
return profile ? createSanitizedDisplayName(profile) : null return profile ? createSanitizedDisplayName(profile) : null
} }
export function getSystemMessageInfo( export function getSystemMessageInfo(
data: ChatBskyConvoDefs.SystemMessageView['data'], data: ChatBskyConvoDefs.SystemMessageView['data'],
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[], relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>,
): SystemMessageInfo | null { ): SystemMessageInfo | null {
if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) { if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) {
const name = getReferredDisplayName(data.member, relatedProfiles) const name = getReferredDisplayName(data.member, relatedProfiles)
@@ -298,7 +298,10 @@ function BaseChatItem({
// System message // System message
if (ChatBskyConvoDefs.isSystemMessageView(convo.lastMessage)) { if (ChatBskyConvoDefs.isSystemMessageView(convo.lastMessage)) {
const info = getSystemMessageInfo(convo.lastMessage.data, convo.members) const info = getSystemMessageInfo(
convo.lastMessage.data,
new Map(convo.members.map(m => [m.did, m])),
)
if (info) { if (info) {
lastMessage = i18n._(info.message) lastMessage = i18n._(info.message)
lastMessageSentAt = convo.lastMessage.sentAt lastMessageSentAt = convo.lastMessage.sentAt
@@ -374,9 +374,6 @@ export function MessagesList({
return ( return (
<MessageItem <MessageItem
item={item} item={item}
profile={convoState.convo.members.find(
member => member.did === item.message.sender.did,
)}
isGroupChat={convoState.convo.kind === 'group'} isGroupChat={convoState.convo.kind === 'group'}
/> />
) )
+25 -16
View File
@@ -107,10 +107,8 @@ export class Convo {
{id: string; message: ChatBskyConvoSendMessage.InputSchema['message']} {id: string; message: ChatBskyConvoSendMessage.InputSchema['message']}
> = new Map() > = new Map()
private deletedMessages: Set<string> = new Set() private deletedMessages: Set<string> = new Set()
private systemMessageProfiles: Map< private relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic> =
string, new Map()
ChatBskyActorDefs.ProfileViewBasic
> = new Map()
private isProcessingPendingMessages = false private isProcessingPendingMessages = false
@@ -483,7 +481,7 @@ export class Convo {
this.newMessages = new Map() this.newMessages = new Map()
this.pendingMessages = new Map() this.pendingMessages = new Map()
this.deletedMessages = new Set() this.deletedMessages = new Set()
this.systemMessageProfiles = new Map() this.relatedProfiles = new Map()
this.pendingMessageFailure = null this.pendingMessageFailure = null
this.fetchMessageHistoryError = undefined this.fetchMessageHistoryError = undefined
@@ -510,6 +508,11 @@ export class Convo {
private setConvo(convo: ChatBskyConvoDefs.ConvoView) { private setConvo(convo: ChatBskyConvoDefs.ConvoView) {
this.convo = parseConvoView(convo, this.senderUserDid) ?? this.convo this.convo = parseConvoView(convo, this.senderUserDid) ?? this.convo
if (this.convo) {
for (const member of this.convo.members) {
this.relatedProfiles.set(member.did, member)
}
}
} }
private updateConvo(convo: Partial<ChatBskyConvoDefs.ConvoView>) { private updateConvo(convo: Partial<ChatBskyConvoDefs.ConvoView>) {
@@ -517,6 +520,9 @@ export class Convo {
this.convo = this.convo =
parseConvoView({...this.convo.view, ...convo}, this.senderUserDid) ?? parseConvoView({...this.convo.view, ...convo}, this.senderUserDid) ??
this.convo this.convo
for (const member of this.convo.members) {
this.relatedProfiles.set(member.did, member)
}
} }
} }
@@ -704,7 +710,7 @@ export class Convo {
if (relatedProfiles) { if (relatedProfiles) {
for (const profile of relatedProfiles) { for (const profile of relatedProfiles) {
this.systemMessageProfiles.set(profile.did, profile) this.relatedProfiles.set(profile.did, profile)
} }
} }
@@ -824,6 +830,12 @@ export class Convo {
*/ */
this.latestRev = ev.rev this.latestRev = ev.rev
if ('relatedProfiles' in ev && Array.isArray(ev.relatedProfiles)) {
for (const profile of ev.relatedProfiles) {
this.relatedProfiles.set(profile.did, profile)
}
}
if ( if (
ChatBskyConvoDefs.isLogCreateMessage(ev) && ChatBskyConvoDefs.isLogCreateMessage(ev) &&
ChatBskyConvoDefs.isMessageView(ev.message) ChatBskyConvoDefs.isMessageView(ev.message)
@@ -876,14 +888,6 @@ export class Convo {
const systemView = toSystemMessageView(ev) const systemView = toSystemMessageView(ev)
if (systemView) { if (systemView) {
this.newMessages.set(systemView.id, systemView) this.newMessages.set(systemView.id, systemView)
if (
'relatedProfiles' in ev &&
Array.isArray(ev.relatedProfiles)
) {
for (const profile of ev.relatedProfiles) {
this.systemMessageProfiles.set(profile.did, profile)
}
}
needsCommit = true needsCommit = true
} }
} }
@@ -1180,6 +1184,7 @@ export class Convo {
type: 'message', type: 'message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: this.relatedProfiles,
nextMessage: null, nextMessage: null,
prevMessage: null, prevMessage: null,
}) })
@@ -1188,6 +1193,7 @@ export class Convo {
type: 'deleted-message', type: 'deleted-message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: this.relatedProfiles,
nextMessage: null, nextMessage: null,
prevMessage: null, prevMessage: null,
}) })
@@ -1196,7 +1202,7 @@ export class Convo {
type: 'system-message', type: 'system-message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: Array.from(this.systemMessageProfiles.values()), relatedProfiles: this.relatedProfiles,
}) })
} }
}) })
@@ -1218,6 +1224,7 @@ export class Convo {
type: 'message', type: 'message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: this.relatedProfiles,
nextMessage: null, nextMessage: null,
prevMessage: null, prevMessage: null,
}) })
@@ -1226,6 +1233,7 @@ export class Convo {
type: 'deleted-message', type: 'deleted-message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: this.relatedProfiles,
nextMessage: null, nextMessage: null,
prevMessage: null, prevMessage: null,
}) })
@@ -1234,7 +1242,7 @@ export class Convo {
type: 'system-message', type: 'system-message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: Array.from(this.systemMessageProfiles.values()), relatedProfiles: this.relatedProfiles,
}) })
} }
}) })
@@ -1255,6 +1263,7 @@ export class Convo {
did: this.senderUserDid, did: this.senderUserDid,
}, },
}, },
relatedProfiles: this.relatedProfiles,
nextMessage: null, nextMessage: null,
prevMessage: null, prevMessage: null,
failed: this.pendingMessageFailure !== null, failed: this.pendingMessageFailure !== null,
+11 -23
View File
@@ -59,34 +59,20 @@ export enum ConvoDispatchEvent {
} }
export type ConvoDispatch = export type ConvoDispatch =
| { | {event: ConvoDispatchEvent.Init}
event: ConvoDispatchEvent.Init | {event: ConvoDispatchEvent.Ready}
} | {event: ConvoDispatchEvent.Resume}
| { | {event: ConvoDispatchEvent.Background}
event: ConvoDispatchEvent.Ready | {event: ConvoDispatchEvent.Suspend}
} | {event: ConvoDispatchEvent.Error; payload: ConvoError}
| { | {event: ConvoDispatchEvent.Disable}
event: ConvoDispatchEvent.Resume
}
| {
event: ConvoDispatchEvent.Background
}
| {
event: ConvoDispatchEvent.Suspend
}
| {
event: ConvoDispatchEvent.Error
payload: ConvoError
}
| {
event: ConvoDispatchEvent.Disable
}
export type ConvoItem = export type ConvoItem =
| { | {
type: 'message' type: 'message'
key: string key: string
message: ChatBskyConvoDefs.MessageView message: ChatBskyConvoDefs.MessageView
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
nextMessage: nextMessage:
| ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView | ChatBskyConvoDefs.DeletedMessageView
@@ -100,6 +86,7 @@ export type ConvoItem =
type: 'pending-message' type: 'pending-message'
key: string key: string
message: ChatBskyConvoDefs.MessageView message: ChatBskyConvoDefs.MessageView
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
nextMessage: nextMessage:
| ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView | ChatBskyConvoDefs.DeletedMessageView
@@ -118,6 +105,7 @@ export type ConvoItem =
type: 'deleted-message' type: 'deleted-message'
key: string key: string
message: ChatBskyConvoDefs.DeletedMessageView message: ChatBskyConvoDefs.DeletedMessageView
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
nextMessage: nextMessage:
| ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView | ChatBskyConvoDefs.DeletedMessageView
@@ -131,7 +119,7 @@ export type ConvoItem =
type: 'system-message' type: 'system-message'
key: string key: string
message: ChatBskyConvoDefs.SystemMessageView message: ChatBskyConvoDefs.SystemMessageView
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[] relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
} }
| { | {
type: 'error' type: 'error'