···11-import {ChatBskyConvoDefs} from '@atproto/api'
11+import {type ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
22import {type MessageDescriptor} from '@lingui/core'
33import {msg} from '@lingui/core/macro'
44···2121 Icon: React.ComponentType<SVGIconProps>
2222}
23232424+function getReferredDisplayName(
2525+ user: ChatBskyConvoDefs.SystemMessageReferredUser,
2626+ relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[],
2727+): string | null {
2828+ const profile = relatedProfiles.find(p => p.did === user.did)
2929+ return profile ? createSanitizedDisplayName(profile) : null
3030+}
3131+2432export function getSystemMessageInfo(
2533 data: ChatBskyConvoDefs.SystemMessageView['data'],
3434+ relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[],
2635): SystemMessageInfo | null {
2736 if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) {
3737+ const name = getReferredDisplayName(data.member, relatedProfiles)
2838 return {
2939 Icon: JoinIcon,
3030- message: msg`${createSanitizedDisplayName(data.member)} was added to the group`,
4040+ message: name
4141+ ? msg`${name} was added to the group`
4242+ : msg`Someone was added to the group`,
3143 }
3244 } else if (ChatBskyConvoDefs.isSystemMessageDataRemoveMember(data)) {
4545+ const name = getReferredDisplayName(data.member, relatedProfiles)
3346 return {
3447 Icon: LeaveIcon,
3535- message: msg`${createSanitizedDisplayName(data.member)} was removed from the group`,
4848+ message: name
4949+ ? msg`${name} was removed from the group`
5050+ : msg`Someone was removed from the group`,
3651 }
3752 } else if (ChatBskyConvoDefs.isSystemMessageDataMemberJoin(data)) {
5353+ const name = getReferredDisplayName(data.member, relatedProfiles)
3854 return {
3955 Icon: JoinIcon,
4040- message: msg`${createSanitizedDisplayName(data.member)} joined the group`,
5656+ message: name
5757+ ? msg`${name} joined the group`
5858+ : msg`Someone joined the group`,
4159 }
4260 } else if (ChatBskyConvoDefs.isSystemMessageDataMemberLeave(data)) {
6161+ const name = getReferredDisplayName(data.member, relatedProfiles)
4362 return {
4463 Icon: LeaveIcon,
4545- message: msg`${createSanitizedDisplayName(data.member)} left the group`,
6464+ message: name ? msg`${name} left the group` : msg`Someone left the group`,
4665 }
4766 } else if (ChatBskyConvoDefs.isSystemMessageDataLockConvo(data)) {
4867 return {Icon: LockIcon, message: msg`Chat locked`}
···5372 } else if (ChatBskyConvoDefs.isSystemMessageDataEditGroup(data)) {
5473 return {
5574 Icon: PencilIcon,
5656- message: msg`Chat title changed to ${data.newName ?? ''}`,
7575+ message: data.newName
7676+ ? msg`Chat title changed to ${data.newName}`
7777+ : msg`Chat title changed`,
5778 }
5879 } else if (ChatBskyConvoDefs.isSystemMessageDataCreateJoinLink(data)) {
5980 return {Icon: ChainLinkIcon, message: msg`Invite link created`}
+6-3
src/components/dms/util.ts
···110110 owner = member as GroupConvoMember
111111 }
112112 } else {
113113- throw new Error(
113113+ logger.warn(
114114 'Expected a GroupConvoMember, got an unknown kind of member',
115115 )
116116+ return null
116117 }
117118 }
118119119120 if (!owner) {
120120- throw new Error('No owner found in group convo')
121121+ logger.warn('No owner found in group convo')
122122+ return null
121123 }
122124123125 return {
···136138 const otherUser = convoView.members.find(m => m.did !== ownDid)
137139138140 if (!otherUser) {
139139- throw new Error('No other user found in direct convo')
141141+ logger.warn('No other user found in direct convo')
142142+ return null
140143 }
141144142145 return {
+36-116
src/screens/Messages/components/ChatListItem.tsx
···11import {useCallback, useMemo, useState} from 'react'
22import {type GestureResponderEvent, View} from 'react-native'
33import {
44- AppBskyEmbedRecord,
54 ChatBskyConvoDefs,
65 moderateProfile,
76 type ModerationDecision,
···1413import {useHaptics} from '#/lib/haptics'
1514import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
1615import {decrementBadgeCount} from '#/lib/notifications/notifications'
1717-import {sanitizeDisplayName} from '#/lib/strings/display-names'
1816import {sanitizeHandle} from '#/lib/strings/handles'
1919-import {
2020- postUriToRelativePath,
2121- toBskyAppUrl,
2222- toShortUrl,
2323-} from '#/lib/strings/url-helpers'
2417import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
2518import {useModerationOpts} from '#/state/preferences/moderation-opts'
2619import {
···3629import {AvatarBubbles} from '#/components/AvatarBubbles'
3730import {useDialogControl} from '#/components/Dialog'
3831import {ConvoMenu} from '#/components/dms/ConvoMenu'
3232+import {getMessageInfo} from '#/components/dms/getMessageInfo'
3333+import {getReactionInfo} from '#/components/dms/getReactionInfo'
3434+import {getSystemMessageInfo} from '#/components/dms/getSystemMessageInfo'
3935import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
4040-import {getSystemMessageInfo} from '#/components/dms/systemMessage'
4136import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
4237import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
4338import {Envelope_Open_Stroke2_Corner0_Rounded as EnvelopeOpen} from '#/components/icons/EnveopeOpen'
···5348import type * as bsky from '#/types/bsky'
54495550export const ChatListItemPortal = createPortalGroup()
5656-5757-/**
5858- * IMPORTANT NOTE: THIS IS CURRENTLY JANKY AF AND PROBABLY BROKEN, JUST WANTED TO ADD GROUPCHAT SUPPPORT
5959- *
6060- * TAKE A SECOND PASS PLEASE -sfn
6161- */
62516352export function ChatListItem({
6453 convo: convoView,
···9483 <GroupChatItem
9584 convo={convo}
9685 moderationOpts={moderationOpts}
9797- showMenu={showMenu}
9898- />
8686+ showMenu={showMenu}>
8787+ {children}
8888+ </GroupChatItem>
9989 )
10090 }
10191 default: {
···189179 return (
190180 <BaseChatItem
191181 convo={convo.view}
192192- avatar={<AvatarBubbles profiles={convo.members} size="medium" />}
182182+ avatar={<AvatarBubbles profiles={convo.members} size={52} />}
193183 title={chatName}
194184 accessibilityHint={l`Go to the group chat named "${chatName}"`}
195185 primaryProfile={groupOwner}
···243233244234 const playHaptic = useHaptics()
245235 const queryClient = useQueryClient()
246246- const isUnread = convo.unreadCount > 0
236236+ const hasUnread = convo.unreadCount > 0 && !isDeletedAccount
247237248238 const blockInfo = useMemo(() => {
249239 const modui = primaryProfileModeration.ui('profileView')
···266256267257 let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined
268258269269- // Message
270270- if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
271271- const isFromMe = convo.lastMessage.sender?.did === currentAccount?.did
272272-273273- if (!isFromMe) {
274274- latestReportableMessage = convo.lastMessage
275275- }
276276-277277- if (convo.lastMessage.text) {
278278- if (isFromMe) {
279279- lastMessage = l`You: ${convo.lastMessage.text}`
280280- } else {
281281- lastMessage = convo.lastMessage.text
282282- }
283283- } else if (convo.lastMessage.embed) {
284284- const defaultEmbeddedContentMessage = l`(contains embedded content)`
285285-286286- if (AppBskyEmbedRecord.isView(convo.lastMessage.embed)) {
287287- const embed = convo.lastMessage.embed
288288-289289- if (AppBskyEmbedRecord.isViewRecord(embed.record)) {
290290- const record = embed.record
291291- const path = postUriToRelativePath(record.uri, {
292292- handle: record.author.handle,
293293- })
294294- const href = path ? toBskyAppUrl(path) : undefined
295295- const short = href
296296- ? toShortUrl(href)
297297- : defaultEmbeddedContentMessage
298298- if (isFromMe) {
299299- lastMessage = l`You: ${short}`
300300- } else {
301301- lastMessage = short
302302- }
303303- }
304304- } else {
305305- if (isFromMe) {
306306- lastMessage = l`You: ${defaultEmbeddedContentMessage}`
307307- } else {
308308- lastMessage = defaultEmbeddedContentMessage
309309- }
310310- }
311311- }
312312-313313- lastMessageSentAt = convo.lastMessage.sentAt
314314- }
315315-316259 // Deleted message
317260 if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) {
318261 lastMessageSentAt = convo.lastMessage.sentAt
···322265 : l`Message deleted`
323266 }
324267268268+ // Message
269269+ if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
270270+ const info = getMessageInfo({
271271+ convo,
272272+ currentAccountDid: currentAccount?.did,
273273+ i18n,
274274+ })
275275+ if (info) {
276276+ lastMessage = info.message ?? lastMessage
277277+ lastMessageSentAt = info.sentAt
278278+ latestReportableMessage = info.reportableMessage
279279+ }
280280+ }
281281+325282 // Reaction
326283 if (ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction)) {
284284+ const info = getReactionInfo({
285285+ convo,
286286+ currentAccountDid: currentAccount?.did,
287287+ i18n,
288288+ })
327289 if (
328328- !lastMessageSentAt ||
329329- new Date(lastMessageSentAt) <
330330- new Date(convo.lastReaction.reaction.createdAt)
290290+ info &&
291291+ (!lastMessageSentAt ||
292292+ new Date(lastMessageSentAt) < new Date(info.createdAt))
331293 ) {
332332- const isFromMe =
333333- convo.lastReaction.reaction.sender.did === currentAccount?.did
334334- const lastMessageText = convo.lastReaction.message.text
335335- const fallbackMessage = l({
336336- message: 'a message',
337337- comment: `If last message does not contain text, fall back to "{user} reacted to {a message}"`,
338338- })
339339-340340- if (isFromMe) {
341341- lastMessage = l`You reacted ${convo.lastReaction.reaction.value} to ${
342342- lastMessageText
343343- ? `"${convo.lastReaction.message.text}"`
344344- : fallbackMessage
345345- }`
346346- } else {
347347- const senderDid = convo.lastReaction.reaction.sender.did
348348- const sender = convo.members.find(
349349- member => member.did === senderDid,
350350- )
351351- if (sender) {
352352- lastMessage = l`${sanitizeDisplayName(
353353- sender.displayName || sender.handle,
354354- )} reacted ${convo.lastReaction.reaction.value} to ${
355355- lastMessageText
356356- ? `"${convo.lastReaction.message.text}"`
357357- : fallbackMessage
358358- }`
359359- } else {
360360- lastMessage = l`Someone reacted ${convo.lastReaction.reaction.value} to ${
361361- lastMessageText
362362- ? `"${convo.lastReaction.message.text}"`
363363- : fallbackMessage
364364- }`
365365- }
366366- }
294294+ lastMessage = info.message
295295+ lastMessageSentAt = info.createdAt
367296 }
368297 }
369298370299 // System message
371300 if (ChatBskyConvoDefs.isSystemMessageView(convo.lastMessage)) {
372372- const info = getSystemMessageInfo(convo.lastMessage.data)
301301+ const info = getSystemMessageInfo(convo.lastMessage.data, convo.members)
373302 if (info) {
374303 lastMessage = i18n._(info.message)
304304+ lastMessageSentAt = convo.lastMessage.sentAt
375305 }
376306 }
377307···380310 lastMessageSentAt,
381311 latestReportableMessage,
382312 }
383383- }, [
384384- l,
385385- i18n,
386386- convo.lastMessage,
387387- convo.lastReaction,
388388- currentAccount?.did,
389389- isDeletedAccount,
390390- convo.members,
391391- ])
313313+ }, [l, convo, currentAccount?.did, isDeletedAccount, i18n])
392314393315 const [showActions, setShowActions] = useState(false)
394316···448370 },
449371 }
450372451451- const actions = isUnread
373373+ const actions = hasUnread
452374 ? {
453375 leftFirst: markReadAction,
454376 leftSecond: deleteAction,
···456378 : {
457379 leftFirst: deleteAction,
458380 }
459459-460460- const hasUnread = convo.unreadCount > 0 && !isDeletedAccount
461381462382 return (
463383 <ChatListItemPortal.Provider>
+22-1
src/state/messages/convo/agent.ts
···102102 {id: string; message: ChatBskyConvoSendMessage.InputSchema['message']}
103103 > = new Map()
104104 private deletedMessages: Set<string> = new Set()
105105+ private systemMessageProfiles: Map<
106106+ string,
107107+ ChatBskyActorDefs.ProfileViewBasic
108108+ > = new Map()
105109106110 private isProcessingPendingMessages = false
107111···469473 this.newMessages = new Map()
470474 this.pendingMessages = new Map()
471475 this.deletedMessages = new Set()
476476+ this.systemMessageProfiles = new Map()
472477473478 this.pendingMessageFailure = null
474479 this.fetchMessageHistoryError = undefined
···689694 {headers: DM_SERVICE_HEADERS},
690695 )
691696 })
692692- const {cursor, messages} = response.data
697697+ const {cursor, messages, relatedProfiles} = response.data
693698694699 this.oldestRev = cursor ?? null
695700701701+ if (relatedProfiles) {
702702+ for (const profile of relatedProfiles) {
703703+ this.systemMessageProfiles.set(profile.did, profile)
704704+ }
705705+ }
706706+696707 /*
697708 * If the response contained fewer messages than the limit, we know
698709 * there are no more pages, regardless of whether a cursor was returned.
···861872 const systemView = toSystemMessageView(ev)
862873 if (systemView) {
863874 this.newMessages.set(systemView.id, systemView)
875875+ if (
876876+ 'relatedProfiles' in ev &&
877877+ Array.isArray(ev.relatedProfiles)
878878+ ) {
879879+ for (const profile of ev.relatedProfiles) {
880880+ this.systemMessageProfiles.set(profile.did, profile)
881881+ }
882882+ }
864883 needsCommit = true
865884 }
866885 }
···11551174 type: 'system-message',
11561175 key: m.id,
11571176 message: m,
11771177+ relatedProfiles: Array.from(this.systemMessageProfiles.values()),
11581178 })
11591179 }
11601180 })
···11921212 type: 'system-message',
11931213 key: m.id,
11941214 message: m,
12151215+ relatedProfiles: Array.from(this.systemMessageProfiles.values()),
11951216 })
11961217 }
11971218 })