Add relatedProfiles to convo

This commit is contained in:
Samuel Newman
2026-04-24 11:34:15 +03:00
parent a9b4a35d23
commit bc085d935b
8 changed files with 102 additions and 39 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
+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 (
<> <>
+14 -10
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,20 @@ 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.find(
p => p.did === item.message.sender.did,
)
const reactionsControl = useDialogControl() const reactionsControl = useDialogControl()
const reactionTapRef = useRef(false) const reactionTapRef = useRef(false)
@@ -277,9 +276,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.find(p => p.did === 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 +287,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 +378,7 @@ let MessageItem = ({
) : null} ) : null}
<ReactionsDialog <ReactionsDialog
control={reactionsControl} control={reactionsControl}
members={convo.members} members={item.relatedProfiles}
message={message} message={message}
reactions={message.reactions} reactions={message.reactions}
groupedReactions={groupedReactions} groupedReactions={groupedReactions}
@@ -436,6 +439,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 -3
View File
@@ -1,4 +1,4 @@
import {type ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api' import {ChatBskyConvoDefs} from '@atproto/api'
import {type MessageDescriptor} from '@lingui/core' import {type MessageDescriptor} from '@lingui/core'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -15,6 +15,7 @@ import {
Unlock_Stroke2_Corner2_Rounded as UnlockIcon, Unlock_Stroke2_Corner2_Rounded as UnlockIcon,
} from '#/components/icons/Lock' } from '#/components/icons/Lock'
import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil' import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil'
import type * as bsky from '#/types/bsky'
export type SystemMessageInfo = { export type SystemMessageInfo = {
message: MessageDescriptor message: MessageDescriptor
@@ -23,7 +24,7 @@ export type SystemMessageInfo = {
function getReferredDisplayName( function getReferredDisplayName(
user: ChatBskyConvoDefs.SystemMessageReferredUser, user: ChatBskyConvoDefs.SystemMessageReferredUser,
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[], relatedProfiles: bsky.profile.AnyProfileView[],
): string | null { ): string | null {
const profile = relatedProfiles.find(p => p.did === user.did) const profile = relatedProfiles.find(p => p.did === user.did)
return profile ? createSanitizedDisplayName(profile) : null return profile ? createSanitizedDisplayName(profile) : null
@@ -31,7 +32,7 @@ function getReferredDisplayName(
export function getSystemMessageInfo( export function getSystemMessageInfo(
data: ChatBskyConvoDefs.SystemMessageView['data'], data: ChatBskyConvoDefs.SystemMessageView['data'],
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[], relatedProfiles: bsky.profile.AnyProfileView[],
): SystemMessageInfo | null { ): SystemMessageInfo | null {
if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) { if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) {
const name = getReferredDisplayName(data.member, relatedProfiles) const name = getReferredDisplayName(data.member, relatedProfiles)
@@ -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'}
/> />
) )
+64 -16
View File
@@ -43,6 +43,7 @@ import {
parseConvoView, parseConvoView,
} from '#/components/dms/util' } from '#/components/dms/util'
import {IS_NATIVE} from '#/env' import {IS_NATIVE} from '#/env'
import type * as bsky from '#/types/bsky'
const logger = Logger.create(Logger.Context.ConversationAgent) const logger = Logger.create(Logger.Context.ConversationAgent)
@@ -107,10 +108,7 @@ 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, bsky.profile.AnyProfileView> = new Map()
string,
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
} }
} }
@@ -1168,6 +1172,44 @@ export class Convo {
} }
} }
private getRelatedProfilesForItem(
m:
| ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView
| ChatBskyConvoDefs.SystemMessageView,
): bsky.profile.AnyProfileView[] {
const seen = new Set<string>()
const profiles: bsky.profile.AnyProfileView[] = []
const add = (did: string) => {
if (seen.has(did)) return
seen.add(did)
const p = this.relatedProfiles.get(did)
if (p) profiles.push(p)
}
if (ChatBskyConvoDefs.isMessageView(m)) {
add(m.sender.did)
if (m.reactions) {
for (const reaction of m.reactions) {
add(reaction.sender.did)
}
}
} else if (ChatBskyConvoDefs.isDeletedMessageView(m)) {
add(m.sender.did)
} else if (ChatBskyConvoDefs.isSystemMessageView(m)) {
const data = m.data
if ('member' in data && data.member?.did) {
add(data.member.did)
}
if ('addedBy' in data && data.addedBy?.did) {
add(data.addedBy.did)
}
}
return profiles
}
/* /*
* Items in reverse order, since FlatList inverts * Items in reverse order, since FlatList inverts
*/ */
@@ -1180,6 +1222,7 @@ export class Convo {
type: 'message', type: 'message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: this.getRelatedProfilesForItem(m),
nextMessage: null, nextMessage: null,
prevMessage: null, prevMessage: null,
}) })
@@ -1188,6 +1231,7 @@ export class Convo {
type: 'deleted-message', type: 'deleted-message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: this.getRelatedProfilesForItem(m),
nextMessage: null, nextMessage: null,
prevMessage: null, prevMessage: null,
}) })
@@ -1196,7 +1240,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.getRelatedProfilesForItem(m),
}) })
} }
}) })
@@ -1218,6 +1262,7 @@ export class Convo {
type: 'message', type: 'message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: this.getRelatedProfilesForItem(m),
nextMessage: null, nextMessage: null,
prevMessage: null, prevMessage: null,
}) })
@@ -1226,6 +1271,7 @@ export class Convo {
type: 'deleted-message', type: 'deleted-message',
key: m.id, key: m.id,
message: m, message: m,
relatedProfiles: this.getRelatedProfilesForItem(m),
nextMessage: null, nextMessage: null,
prevMessage: null, prevMessage: null,
}) })
@@ -1234,12 +1280,13 @@ 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.getRelatedProfilesForItem(m),
}) })
} }
}) })
this.pendingMessages.forEach(m => { this.pendingMessages.forEach(m => {
const senderProfile = this.relatedProfiles.get(this.senderUserDid)
items.push({ items.push({
type: 'pending-message', type: 'pending-message',
key: m.id, key: m.id,
@@ -1255,6 +1302,7 @@ export class Convo {
did: this.senderUserDid, did: this.senderUserDid,
}, },
}, },
relatedProfiles: senderProfile ? [senderProfile] : [],
nextMessage: null, nextMessage: null,
prevMessage: null, prevMessage: null,
failed: this.pendingMessageFailure !== null, failed: this.pendingMessageFailure !== null,
+5 -2
View File
@@ -1,12 +1,12 @@
import { import {
type BskyAgent, type BskyAgent,
type ChatBskyActorDefs,
type ChatBskyConvoDefs, type ChatBskyConvoDefs,
type ChatBskyConvoSendMessage, type ChatBskyConvoSendMessage,
} from '@atproto/api' } from '@atproto/api'
import {type MessagesEventBus} from '#/state/messages/events/agent' import {type MessagesEventBus} from '#/state/messages/events/agent'
import {type ConvoWithDetails} from '#/components/dms/util' import {type ConvoWithDetails} from '#/components/dms/util'
import type * as bsky from '#/types/bsky'
export type ConvoParams = { export type ConvoParams = {
convoId: string convoId: string
@@ -87,6 +87,7 @@ export type ConvoItem =
type: 'message' type: 'message'
key: string key: string
message: ChatBskyConvoDefs.MessageView message: ChatBskyConvoDefs.MessageView
relatedProfiles: bsky.profile.AnyProfileView[]
nextMessage: nextMessage:
| ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView | ChatBskyConvoDefs.DeletedMessageView
@@ -100,6 +101,7 @@ export type ConvoItem =
type: 'pending-message' type: 'pending-message'
key: string key: string
message: ChatBskyConvoDefs.MessageView message: ChatBskyConvoDefs.MessageView
relatedProfiles: bsky.profile.AnyProfileView[]
nextMessage: nextMessage:
| ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView | ChatBskyConvoDefs.DeletedMessageView
@@ -118,6 +120,7 @@ export type ConvoItem =
type: 'deleted-message' type: 'deleted-message'
key: string key: string
message: ChatBskyConvoDefs.DeletedMessageView message: ChatBskyConvoDefs.DeletedMessageView
relatedProfiles: bsky.profile.AnyProfileView[]
nextMessage: nextMessage:
| ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView | ChatBskyConvoDefs.DeletedMessageView
@@ -131,7 +134,7 @@ export type ConvoItem =
type: 'system-message' type: 'system-message'
key: string key: string
message: ChatBskyConvoDefs.SystemMessageView message: ChatBskyConvoDefs.SystemMessageView
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[] relatedProfiles: bsky.profile.AnyProfileView[]
} }
| { | {
type: 'error' type: 'error'