diff --git a/package.json b/package.json
index 9ed7549b55..d4b0992d7a 100644
--- a/package.json
+++ b/package.json
@@ -81,7 +81,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
- "@atproto/api": "^0.19.10",
+ "@atproto/api": "^0.19.11",
"@atproto/syntax": "0.5.2",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
diff --git a/src/components/AvatarBubbles.tsx b/src/components/AvatarBubbles.tsx
index 2ed58f5ad3..0c6f67db37 100644
--- a/src/components/AvatarBubbles.tsx
+++ b/src/components/AvatarBubbles.tsx
@@ -45,7 +45,12 @@ export function AvatarBubbles({
: size === 'medium'
? 56 / 120
: 1
- const marginOffset = size === 'small' || size === 'medium' ? -2 : 0
+ const marginOffset =
+ (typeof size === 'number' && size < 120) ||
+ size === 'small' ||
+ size === 'medium'
+ ? -2
+ : 0
const initialValue = animate ? 0 : 1
const p0 = useSharedValue(initialValue)
diff --git a/src/components/dms/SystemMessageItem.tsx b/src/components/dms/SystemMessageItem.tsx
index d863871d18..91075597d5 100644
--- a/src/components/dms/SystemMessageItem.tsx
+++ b/src/components/dms/SystemMessageItem.tsx
@@ -3,7 +3,7 @@ import {useLingui} from '@lingui/react/macro'
import {type ConvoItem} from '#/state/messages/convo/types'
import {atoms as a, useTheme} from '#/alf'
-import {getSystemMessageInfo} from '#/components/dms/systemMessage'
+import {getSystemMessageInfo} from '#/components/dms/getSystemMessageInfo'
import {Text} from '#/components/Typography'
export function SystemMessageItem({
@@ -14,7 +14,7 @@ export function SystemMessageItem({
const t = useTheme()
const {i18n} = useLingui()
- const info = getSystemMessageInfo(item.message.data)
+ const info = getSystemMessageInfo(item.message.data, item.relatedProfiles)
if (!info) return null
const {Icon, message} = info
@@ -28,6 +28,7 @@ export function SystemMessageItem({
a.justify_center,
a.px_md,
a.mt_md,
+ a.mb_xs,
]}>
m.did === senderDid)
+ const name = sender ? createSanitizedDisplayName(sender) : null
+ const isGroup = ChatBskyConvoDefs.isGroupConvo(convo.kind)
+
+ const reportableMessage = isFromMe ? undefined : lastMessage
+
+ const prefix = (message: string) => {
+ if (isFromMe) {
+ return i18n._(
+ msg({
+ message: `You: ${message}`,
+ comment: 'When the last message in a chat was made by you.',
+ }),
+ )
+ } else if (isGroup && name) {
+ return i18n._(
+ msg({
+ message: `${name}: ${message}`,
+ comment:
+ 'When the last message in a group chat came from someone other than you.',
+ }),
+ )
+ }
+ return message
+ }
+
+ let message: string | null = null
+
+ if (lastMessage.text) {
+ message = prefix(lastMessage.text)
+ } else if (lastMessage.embed) {
+ const defaultEmbeddedContentMessage = i18n._(
+ msg`(contains embedded content)`,
+ )
+
+ if (AppBskyEmbedRecord.isView(lastMessage.embed)) {
+ const embed = lastMessage.embed
+
+ if (AppBskyEmbedRecord.isViewRecord(embed.record)) {
+ const record = embed.record
+ const path = postUriToRelativePath(record.uri, {
+ handle: record.author.handle,
+ })
+ const href = path ? toBskyAppUrl(path) : undefined
+ const short = href ? toShortUrl(href) : defaultEmbeddedContentMessage
+ message = prefix(short)
+ } else {
+ message = prefix(defaultEmbeddedContentMessage)
+ }
+ } else {
+ message = prefix(defaultEmbeddedContentMessage)
+ }
+ }
+
+ return {
+ message,
+ sentAt: lastMessage.sentAt,
+ reportableMessage,
+ }
+}
diff --git a/src/components/dms/getReactionInfo.ts b/src/components/dms/getReactionInfo.ts
new file mode 100644
index 0000000000..351c659698
--- /dev/null
+++ b/src/components/dms/getReactionInfo.ts
@@ -0,0 +1,54 @@
+import {ChatBskyConvoDefs} from '@atproto/api'
+import {type I18n} from '@lingui/core'
+import {msg} from '@lingui/core/macro'
+
+import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
+
+export type UserReactionInfo = {
+ message: string
+ createdAt: string
+}
+
+export function getReactionInfo({
+ convo,
+ currentAccountDid,
+ i18n,
+}: {
+ convo: ChatBskyConvoDefs.ConvoView
+ currentAccountDid: string | undefined
+ i18n: I18n
+}): UserReactionInfo | null {
+ if (!ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction)) {
+ return null
+ }
+
+ const {reaction, message: reactedTo} = convo.lastReaction
+ const isFromMe = reaction.sender.did === currentAccountDid
+ const senderDid = reaction.sender.did
+ const sender = convo.members.find(m => m.did === senderDid)
+ const name = sender ? createSanitizedDisplayName(sender) : null
+
+ const lastMessageText = reactedTo.text
+ const fallbackMessage = i18n._(
+ msg({
+ message: 'a message',
+ comment:
+ 'If last message does not contain text, fall back to "{user} reacted to {a message}"',
+ }),
+ )
+ const target = lastMessageText ? `"${lastMessageText}"` : fallbackMessage
+
+ let message: string
+ if (isFromMe) {
+ message = i18n._(msg`You reacted ${reaction.value} to ${target}`)
+ } else if (name) {
+ message = i18n._(msg`${name} reacted ${reaction.value} to ${target}`)
+ } else {
+ message = i18n._(msg`Someone reacted ${reaction.value} to ${target}`)
+ }
+
+ return {
+ message,
+ createdAt: reaction.createdAt,
+ }
+}
diff --git a/src/components/dms/systemMessage.ts b/src/components/dms/getSystemMessageInfo.ts
similarity index 68%
rename from src/components/dms/systemMessage.ts
rename to src/components/dms/getSystemMessageInfo.ts
index 5764a2a637..f811e2b92c 100644
--- a/src/components/dms/systemMessage.ts
+++ b/src/components/dms/getSystemMessageInfo.ts
@@ -1,4 +1,4 @@
-import {ChatBskyConvoDefs} from '@atproto/api'
+import {type ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
import {type MessageDescriptor} from '@lingui/core'
import {msg} from '@lingui/core/macro'
@@ -21,28 +21,47 @@ export type SystemMessageInfo = {
Icon: React.ComponentType
}
+function getReferredDisplayName(
+ user: ChatBskyConvoDefs.SystemMessageReferredUser,
+ relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[],
+): string | null {
+ const profile = relatedProfiles.find(p => p.did === user.did)
+ return profile ? createSanitizedDisplayName(profile) : null
+}
+
export function getSystemMessageInfo(
data: ChatBskyConvoDefs.SystemMessageView['data'],
+ relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[],
): SystemMessageInfo | null {
if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) {
+ const name = getReferredDisplayName(data.member, relatedProfiles)
return {
Icon: JoinIcon,
- message: msg`${createSanitizedDisplayName(data.member)} was added to the group`,
+ message: name
+ ? msg`${name} was added to the group`
+ : msg`Someone was added to the group`,
}
} else if (ChatBskyConvoDefs.isSystemMessageDataRemoveMember(data)) {
+ const name = getReferredDisplayName(data.member, relatedProfiles)
return {
Icon: LeaveIcon,
- message: msg`${createSanitizedDisplayName(data.member)} was removed from the group`,
+ message: name
+ ? msg`${name} was removed from the group`
+ : msg`Someone was removed from the group`,
}
} else if (ChatBskyConvoDefs.isSystemMessageDataMemberJoin(data)) {
+ const name = getReferredDisplayName(data.member, relatedProfiles)
return {
Icon: JoinIcon,
- message: msg`${createSanitizedDisplayName(data.member)} joined the group`,
+ message: name
+ ? msg`${name} joined the group`
+ : msg`Someone joined the group`,
}
} else if (ChatBskyConvoDefs.isSystemMessageDataMemberLeave(data)) {
+ const name = getReferredDisplayName(data.member, relatedProfiles)
return {
Icon: LeaveIcon,
- message: msg`${createSanitizedDisplayName(data.member)} left the group`,
+ message: name ? msg`${name} left the group` : msg`Someone left the group`,
}
} else if (ChatBskyConvoDefs.isSystemMessageDataLockConvo(data)) {
return {Icon: LockIcon, message: msg`Chat locked`}
@@ -53,7 +72,9 @@ export function getSystemMessageInfo(
} else if (ChatBskyConvoDefs.isSystemMessageDataEditGroup(data)) {
return {
Icon: PencilIcon,
- message: msg`Chat title changed to ${data.newName ?? ''}`,
+ message: data.newName
+ ? msg`Chat title changed to ${data.newName}`
+ : msg`Chat title changed`,
}
} else if (ChatBskyConvoDefs.isSystemMessageDataCreateJoinLink(data)) {
return {Icon: ChainLinkIcon, message: msg`Invite link created`}
diff --git a/src/components/dms/util.ts b/src/components/dms/util.ts
index 64e6bb16c6..5c159a72f0 100644
--- a/src/components/dms/util.ts
+++ b/src/components/dms/util.ts
@@ -110,14 +110,16 @@ export function parseConvoView(
owner = member as GroupConvoMember
}
} else {
- throw new Error(
+ logger.warn(
'Expected a GroupConvoMember, got an unknown kind of member',
)
+ return null
}
}
if (!owner) {
- throw new Error('No owner found in group convo')
+ logger.warn('No owner found in group convo')
+ return null
}
return {
@@ -136,7 +138,8 @@ export function parseConvoView(
const otherUser = convoView.members.find(m => m.did !== ownDid)
if (!otherUser) {
- throw new Error('No other user found in direct convo')
+ logger.warn('No other user found in direct convo')
+ return null
}
return {
diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx
index 6d3d5a783f..7cedb5d4be 100644
--- a/src/screens/Messages/components/ChatListItem.tsx
+++ b/src/screens/Messages/components/ChatListItem.tsx
@@ -1,7 +1,6 @@
import {useCallback, useMemo, useState} from 'react'
import {type GestureResponderEvent, View} from 'react-native'
import {
- AppBskyEmbedRecord,
ChatBskyConvoDefs,
moderateProfile,
type ModerationDecision,
@@ -14,13 +13,7 @@ import {GestureActionView} from '#/lib/custom-animations/GestureActionView'
import {useHaptics} from '#/lib/haptics'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {decrementBadgeCount} from '#/lib/notifications/notifications'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
-import {
- postUriToRelativePath,
- toBskyAppUrl,
- toShortUrl,
-} from '#/lib/strings/url-helpers'
import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {
@@ -36,8 +29,10 @@ import * as tokens from '#/alf/tokens'
import {AvatarBubbles} from '#/components/AvatarBubbles'
import {useDialogControl} from '#/components/Dialog'
import {ConvoMenu} from '#/components/dms/ConvoMenu'
+import {getMessageInfo} from '#/components/dms/getMessageInfo'
+import {getReactionInfo} from '#/components/dms/getReactionInfo'
+import {getSystemMessageInfo} from '#/components/dms/getSystemMessageInfo'
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
-import {getSystemMessageInfo} from '#/components/dms/systemMessage'
import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
import {Envelope_Open_Stroke2_Corner0_Rounded as EnvelopeOpen} from '#/components/icons/EnveopeOpen'
@@ -54,12 +49,6 @@ import type * as bsky from '#/types/bsky'
export const ChatListItemPortal = createPortalGroup()
-/**
- * IMPORTANT NOTE: THIS IS CURRENTLY JANKY AF AND PROBABLY BROKEN, JUST WANTED TO ADD GROUPCHAT SUPPPORT
- *
- * TAKE A SECOND PASS PLEASE -sfn
- */
-
export function ChatListItem({
convo: convoView,
showMenu = true,
@@ -94,8 +83,9 @@ export function ChatListItem({
+ showMenu={showMenu}>
+ {children}
+
)
}
default: {
@@ -189,7 +179,7 @@ function GroupChatItem({
return (
}
+ avatar={}
title={chatName}
accessibilityHint={l`Go to the group chat named "${chatName}"`}
primaryProfile={groupOwner}
@@ -243,7 +233,7 @@ function BaseChatItem({
const playHaptic = useHaptics()
const queryClient = useQueryClient()
- const isUnread = convo.unreadCount > 0
+ const hasUnread = convo.unreadCount > 0 && !isDeletedAccount
const blockInfo = useMemo(() => {
const modui = primaryProfileModeration.ui('profileView')
@@ -266,53 +256,6 @@ function BaseChatItem({
let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined
- // Message
- if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
- const isFromMe = convo.lastMessage.sender?.did === currentAccount?.did
-
- if (!isFromMe) {
- latestReportableMessage = convo.lastMessage
- }
-
- if (convo.lastMessage.text) {
- if (isFromMe) {
- lastMessage = l`You: ${convo.lastMessage.text}`
- } else {
- lastMessage = convo.lastMessage.text
- }
- } else if (convo.lastMessage.embed) {
- const defaultEmbeddedContentMessage = l`(contains embedded content)`
-
- if (AppBskyEmbedRecord.isView(convo.lastMessage.embed)) {
- const embed = convo.lastMessage.embed
-
- if (AppBskyEmbedRecord.isViewRecord(embed.record)) {
- const record = embed.record
- const path = postUriToRelativePath(record.uri, {
- handle: record.author.handle,
- })
- const href = path ? toBskyAppUrl(path) : undefined
- const short = href
- ? toShortUrl(href)
- : defaultEmbeddedContentMessage
- if (isFromMe) {
- lastMessage = l`You: ${short}`
- } else {
- lastMessage = short
- }
- }
- } else {
- if (isFromMe) {
- lastMessage = l`You: ${defaultEmbeddedContentMessage}`
- } else {
- lastMessage = defaultEmbeddedContentMessage
- }
- }
- }
-
- lastMessageSentAt = convo.lastMessage.sentAt
- }
-
// Deleted message
if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) {
lastMessageSentAt = convo.lastMessage.sentAt
@@ -322,56 +265,43 @@ function BaseChatItem({
: l`Message deleted`
}
+ // Message
+ if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
+ const info = getMessageInfo({
+ convo,
+ currentAccountDid: currentAccount?.did,
+ i18n,
+ })
+ if (info) {
+ lastMessage = info.message ?? lastMessage
+ lastMessageSentAt = info.sentAt
+ latestReportableMessage = info.reportableMessage
+ }
+ }
+
// Reaction
if (ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction)) {
+ const info = getReactionInfo({
+ convo,
+ currentAccountDid: currentAccount?.did,
+ i18n,
+ })
if (
- !lastMessageSentAt ||
- new Date(lastMessageSentAt) <
- new Date(convo.lastReaction.reaction.createdAt)
+ info &&
+ (!lastMessageSentAt ||
+ new Date(lastMessageSentAt) < new Date(info.createdAt))
) {
- const isFromMe =
- convo.lastReaction.reaction.sender.did === currentAccount?.did
- const lastMessageText = convo.lastReaction.message.text
- const fallbackMessage = l({
- message: 'a message',
- comment: `If last message does not contain text, fall back to "{user} reacted to {a message}"`,
- })
-
- if (isFromMe) {
- lastMessage = l`You reacted ${convo.lastReaction.reaction.value} to ${
- lastMessageText
- ? `"${convo.lastReaction.message.text}"`
- : fallbackMessage
- }`
- } else {
- const senderDid = convo.lastReaction.reaction.sender.did
- const sender = convo.members.find(
- member => member.did === senderDid,
- )
- if (sender) {
- lastMessage = l`${sanitizeDisplayName(
- sender.displayName || sender.handle,
- )} reacted ${convo.lastReaction.reaction.value} to ${
- lastMessageText
- ? `"${convo.lastReaction.message.text}"`
- : fallbackMessage
- }`
- } else {
- lastMessage = l`Someone reacted ${convo.lastReaction.reaction.value} to ${
- lastMessageText
- ? `"${convo.lastReaction.message.text}"`
- : fallbackMessage
- }`
- }
- }
+ lastMessage = info.message
+ lastMessageSentAt = info.createdAt
}
}
// System message
if (ChatBskyConvoDefs.isSystemMessageView(convo.lastMessage)) {
- const info = getSystemMessageInfo(convo.lastMessage.data)
+ const info = getSystemMessageInfo(convo.lastMessage.data, convo.members)
if (info) {
lastMessage = i18n._(info.message)
+ lastMessageSentAt = convo.lastMessage.sentAt
}
}
@@ -380,15 +310,7 @@ function BaseChatItem({
lastMessageSentAt,
latestReportableMessage,
}
- }, [
- l,
- i18n,
- convo.lastMessage,
- convo.lastReaction,
- currentAccount?.did,
- isDeletedAccount,
- convo.members,
- ])
+ }, [l, convo, currentAccount?.did, isDeletedAccount, i18n])
const [showActions, setShowActions] = useState(false)
@@ -448,7 +370,7 @@ function BaseChatItem({
},
}
- const actions = isUnread
+ const actions = hasUnread
? {
leftFirst: markReadAction,
leftSecond: deleteAction,
@@ -457,8 +379,6 @@ function BaseChatItem({
leftFirst: deleteAction,
}
- const hasUnread = convo.unreadCount > 0 && !isDeletedAccount
-
return (
diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts
index 77b59581b4..3552de39e6 100644
--- a/src/state/messages/convo/agent.ts
+++ b/src/state/messages/convo/agent.ts
@@ -102,6 +102,10 @@ export class Convo {
{id: string; message: ChatBskyConvoSendMessage.InputSchema['message']}
> = new Map()
private deletedMessages: Set = new Set()
+ private systemMessageProfiles: Map<
+ string,
+ ChatBskyActorDefs.ProfileViewBasic
+ > = new Map()
private isProcessingPendingMessages = false
@@ -469,6 +473,7 @@ export class Convo {
this.newMessages = new Map()
this.pendingMessages = new Map()
this.deletedMessages = new Set()
+ this.systemMessageProfiles = new Map()
this.pendingMessageFailure = null
this.fetchMessageHistoryError = undefined
@@ -689,10 +694,16 @@ export class Convo {
{headers: DM_SERVICE_HEADERS},
)
})
- const {cursor, messages} = response.data
+ const {cursor, messages, relatedProfiles} = response.data
this.oldestRev = cursor ?? null
+ if (relatedProfiles) {
+ for (const profile of relatedProfiles) {
+ this.systemMessageProfiles.set(profile.did, profile)
+ }
+ }
+
/*
* If the response contained fewer messages than the limit, we know
* there are no more pages, regardless of whether a cursor was returned.
@@ -861,6 +872,14 @@ export class Convo {
const systemView = toSystemMessageView(ev)
if (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
}
}
@@ -1155,6 +1174,7 @@ export class Convo {
type: 'system-message',
key: m.id,
message: m,
+ relatedProfiles: Array.from(this.systemMessageProfiles.values()),
})
}
})
@@ -1192,6 +1212,7 @@ export class Convo {
type: 'system-message',
key: m.id,
message: m,
+ relatedProfiles: Array.from(this.systemMessageProfiles.values()),
})
}
})
diff --git a/src/state/messages/convo/types.ts b/src/state/messages/convo/types.ts
index ef81e9009d..dad3b7f82a 100644
--- a/src/state/messages/convo/types.ts
+++ b/src/state/messages/convo/types.ts
@@ -130,6 +130,7 @@ export type ConvoItem =
type: 'system-message'
key: string
message: ChatBskyConvoDefs.SystemMessageView
+ relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[]
}
| {
type: 'error'
diff --git a/yarn.lock b/yarn.lock
index a0f45c607a..edd987d5a8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -20,10 +20,10 @@
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
-"@atproto/api@^0.19.10":
- version "0.19.10"
- resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.10.tgz#311a3fb8c642ee8bb1eea96acadfedbf500e9f08"
- integrity sha512-HrHWjL6aEEfUKBSZFZ7Fz+MF3WcGOdhTlX6f71j4fgf91pMhwxgdo2K13Qjn2CxIh8/iHAJi+oiLWKOgZnOLNA==
+"@atproto/api@^0.19.11":
+ version "0.19.11"
+ resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.11.tgz#73885a47959907f22b68d671011ee70f80afd44b"
+ integrity sha512-7V4Sg6hcv/UxoXobjfvy/Ox2ioKQtZ3DzbsiFndYCcBfsZ5GO8rNEroHPq3hT0CFBJK1NAD6JfOtTBN2z267Xg==
dependencies:
"@atproto/common-web" "^0.4.21"
"@atproto/lexicon" "^0.6.2"