From 2ab1e2c9e952ceac3bf68dee37367a4b02bca69e Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:43:22 -0700 Subject: [PATCH] Display system messages in chats (#10312) Co-authored-by: Samuel Newman --- .../arrowBoxRight_stroke2_corner3_rounded.svg | 1 + ...hainLinkBroken_stroke2_corner0_rounded.svg | 1 + .../icons/unlock_stroke2_corner2_rounded.svg | 1 + package.json | 2 +- src/components/dms/DateDivider.tsx | 1 - src/components/dms/MessageItem.tsx | 18 ++++- src/components/dms/SystemMessageItem.tsx | 44 ++++++++++++ src/components/dms/systemMessage.ts | 68 +++++++++++++++++++ src/components/icons/ArrowBoxRight.tsx | 5 ++ src/components/icons/ChainLink.tsx | 4 ++ src/components/icons/Lock.tsx | 4 ++ .../Messages/components/ChatListItem.tsx | 16 ++++- .../Messages/components/MessagesList.tsx | 3 + src/state/messages/convo/agent.ts | 49 ++++++++++++- src/state/messages/convo/types.ts | 5 ++ yarn.lock | 8 +-- 16 files changed, 219 insertions(+), 11 deletions(-) create mode 100644 assets/icons/arrowBoxRight_stroke2_corner3_rounded.svg create mode 100644 assets/icons/chainLinkBroken_stroke2_corner0_rounded.svg create mode 100644 assets/icons/unlock_stroke2_corner2_rounded.svg create mode 100644 src/components/dms/SystemMessageItem.tsx create mode 100644 src/components/dms/systemMessage.ts create mode 100644 src/components/icons/ArrowBoxRight.tsx diff --git a/assets/icons/arrowBoxRight_stroke2_corner3_rounded.svg b/assets/icons/arrowBoxRight_stroke2_corner3_rounded.svg new file mode 100644 index 0000000000..7006406561 --- /dev/null +++ b/assets/icons/arrowBoxRight_stroke2_corner3_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/chainLinkBroken_stroke2_corner0_rounded.svg b/assets/icons/chainLinkBroken_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..c5197634c7 --- /dev/null +++ b/assets/icons/chainLinkBroken_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/unlock_stroke2_corner2_rounded.svg b/assets/icons/unlock_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..a9fefda12e --- /dev/null +++ b/assets/icons/unlock_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/package.json b/package.json index 86c06d9943..78a7ea4978 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.19.9", + "@atproto/api": "^0.19.10", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@bsky.app/alf": "^0.1.7", diff --git a/src/components/dms/DateDivider.tsx b/src/components/dms/DateDivider.tsx index 21724ba3ea..5e7de4ebc0 100644 --- a/src/components/dms/DateDivider.tsx +++ b/src/components/dms/DateDivider.tsx @@ -61,7 +61,6 @@ let DateDivider = ({date: dateStr}: {date: string}): React.ReactNode => { style={[ a.text_xs, a.text_center, - t.atoms.bg, t.atoms.text_contrast_medium, a.px_md, ]}> diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index b1c288de78..7d60cc2a9d 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -474,7 +474,23 @@ let MessageItem = ({ ]}> + + + {i18n._(message)} + + + ) +} diff --git a/src/components/dms/systemMessage.ts b/src/components/dms/systemMessage.ts new file mode 100644 index 0000000000..5764a2a637 --- /dev/null +++ b/src/components/dms/systemMessage.ts @@ -0,0 +1,68 @@ +import {ChatBskyConvoDefs} from '@atproto/api' +import {type MessageDescriptor} from '@lingui/core' +import {msg} from '@lingui/core/macro' + +import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' +import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft' +import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight' +import { + ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon, + ChainLinkBroken_Stroke2_Corner0_Rounded as ChainLinkBrokenIcon, +} from '#/components/icons/ChainLink' +import {type Props as SVGIconProps} from '#/components/icons/common' +import { + Lock_Stroke2_Corner0_Rounded as LockIcon, + Unlock_Stroke2_Corner2_Rounded as UnlockIcon, +} from '#/components/icons/Lock' +import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil' + +export type SystemMessageInfo = { + message: MessageDescriptor + Icon: React.ComponentType +} + +export function getSystemMessageInfo( + data: ChatBskyConvoDefs.SystemMessageView['data'], +): SystemMessageInfo | null { + if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) { + return { + Icon: JoinIcon, + message: msg`${createSanitizedDisplayName(data.member)} was added to the group`, + } + } else if (ChatBskyConvoDefs.isSystemMessageDataRemoveMember(data)) { + return { + Icon: LeaveIcon, + message: msg`${createSanitizedDisplayName(data.member)} was removed from the group`, + } + } else if (ChatBskyConvoDefs.isSystemMessageDataMemberJoin(data)) { + return { + Icon: JoinIcon, + message: msg`${createSanitizedDisplayName(data.member)} joined the group`, + } + } else if (ChatBskyConvoDefs.isSystemMessageDataMemberLeave(data)) { + return { + Icon: LeaveIcon, + message: msg`${createSanitizedDisplayName(data.member)} left the group`, + } + } else if (ChatBskyConvoDefs.isSystemMessageDataLockConvo(data)) { + return {Icon: LockIcon, message: msg`Chat locked`} + } else if (ChatBskyConvoDefs.isSystemMessageDataUnlockConvo(data)) { + return {Icon: UnlockIcon, message: msg`Chat unlocked`} + } else if (ChatBskyConvoDefs.isSystemMessageDataLockConvoPermanently(data)) { + return {Icon: LockIcon, message: msg`Chat locked permanently`} + } else if (ChatBskyConvoDefs.isSystemMessageDataEditGroup(data)) { + return { + Icon: PencilIcon, + message: msg`Chat title changed to ${data.newName ?? ''}`, + } + } else if (ChatBskyConvoDefs.isSystemMessageDataCreateJoinLink(data)) { + return {Icon: ChainLinkIcon, message: msg`Invite link created`} + } else if (ChatBskyConvoDefs.isSystemMessageDataEditJoinLink(data)) { + return {Icon: ChainLinkIcon, message: msg`Invite link edited`} + } else if (ChatBskyConvoDefs.isSystemMessageDataEnableJoinLink(data)) { + return {Icon: ChainLinkIcon, message: msg`Invite link enabled`} + } else if (ChatBskyConvoDefs.isSystemMessageDataDisableJoinLink(data)) { + return {Icon: ChainLinkBrokenIcon, message: msg`Invite link disabled`} + } + return null +} diff --git a/src/components/icons/ArrowBoxRight.tsx b/src/components/icons/ArrowBoxRight.tsx new file mode 100644 index 0000000000..b0d5e68c41 --- /dev/null +++ b/src/components/icons/ArrowBoxRight.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const ArrowBoxRight_Stroke2_Corner3_Rounded = createSinglePathSVG({ + path: 'M17 3a4 4 0 0 1 4 4v10a4 4 0 0 1-4 4h-2a1 1 0 1 1 0-2h2a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2a1 1 0 1 1 0-2h2Zm-6.707 4.793a1 1 0 0 1 1.414 0l3.5 3.5a1 1 0 0 1 0 1.414l-3.5 3.5a1 1 0 1 1-1.414-1.414L12.086 13H4a1 1 0 1 1 0-2h8.086l-1.793-1.793a1 1 0 0 1 0-1.414Z', +}) diff --git a/src/components/icons/ChainLink.tsx b/src/components/icons/ChainLink.tsx index be19b556a4..de3a466490 100644 --- a/src/components/icons/ChainLink.tsx +++ b/src/components/icons/ChainLink.tsx @@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE' export const ChainLink_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z', }) + +export const ChainLinkBroken_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M14.3 23v-1.1a1 1 0 0 1 2 0V23a1 1 0 1 1-2 0Zm5.243-3.457a1 1 0 0 1 1.414 0l1.1 1.1a1 1 0 1 1-1.414 1.414l-1.1-1.1a1 1 0 0 1 0-1.414ZM4.788 9.298a1 1 0 0 1 1.424 1.404l-.742.752-.004.005a5.003 5.003 0 1 0 7.075 7.075l.005-.004.752-.742a1 1 0 0 1 1.404 1.424l-.747.736a7.003 7.003 0 1 1-9.904-9.904l.737-.746ZM23 14.3a1 1 0 0 1 0 2h-1.1a1 1 0 1 1 0-2H23ZM10.044 4.05a7.005 7.005 0 0 1 9.905 9.906h0l-.737.746a1 1 0 0 1-1.424-1.404l.742-.752.004-.005a5.003 5.003 0 1 0-7.075-7.075l-.005.004-.752.742a1 1 0 0 1-1.404-1.424l.746-.737ZM2.1 7.7a1 1 0 1 1 0 2H1a1 1 0 0 1 0-2h1.1Zm-.157-5.757a1 1 0 0 1 1.414 0l1.1 1.1a1 1 0 1 1-1.414 1.414l-1.1-1.1a1 1 0 0 1 0-1.414ZM7.7 2.1V1a1 1 0 1 1 2 0v1.1a1 1 0 0 1-2 0Z', +}) diff --git a/src/components/icons/Lock.tsx b/src/components/icons/Lock.tsx index 44d6d2159a..1c12e94098 100644 --- a/src/components/icons/Lock.tsx +++ b/src/components/icons/Lock.tsx @@ -7,3 +7,7 @@ export const Lock_Stroke2_Corner0_Rounded = createSinglePathSVG({ export const Lock_Stroke2_Corner2_Rounded = createSinglePathSVG({ path: 'M7 7a5 5 0 0 1 10 0v2a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-7a3 3 0 0 1 3-3V7Zm0 4a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H7Zm8-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z', }) + +export const Unlock_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M12 13a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z"/>