Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
117
fork

Configure Feed

Select the types of activity you want to include in your feed.

Display system messages in chats (#10312)

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

authored by

DS Boyce
Samuel Newman
and committed by
GitHub
2ab1e2c9 0df1d6f5

+219 -11
+1
assets/icons/arrowBoxRight_stroke2_corner3_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="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"/></svg>
+1
assets/icons/chainLinkBroken_stroke2_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="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"/></svg>
+1
assets/icons/unlock_stroke2_corner2_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12 13a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z"/><path fill="#000" fill-rule="evenodd" d="M12 2a5 5 0 0 1 4.843 3.751 1 1 0 0 1-1.938.498A3.002 3.002 0 0 0 9 7v2h8a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-7a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5Zm-5 9a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H7Z" clip-rule="evenodd"/></svg>
+1 -1
package.json
··· 81 81 "icons:optimize": "svgo -f ./assets/icons" 82 82 }, 83 83 "dependencies": { 84 - "@atproto/api": "^0.19.9", 84 + "@atproto/api": "^0.19.10", 85 85 "@bitdrift/react-native": "^0.6.8", 86 86 "@braintree/sanitize-url": "^6.0.2", 87 87 "@bsky.app/alf": "^0.1.7",
-1
src/components/dms/DateDivider.tsx
··· 61 61 style={[ 62 62 a.text_xs, 63 63 a.text_center, 64 - t.atoms.bg, 65 64 t.atoms.text_contrast_medium, 66 65 a.px_md, 67 66 ]}>
+17 -1
src/components/dms/MessageItem.tsx
··· 474 474 ]}> 475 475 <RichText 476 476 value={rt} 477 - style={[a.text_md, isFromSelf && {color: t.palette.white}]} 477 + style={[ 478 + a.text_md, 479 + isFromSelf && {color: t.palette.white}, 480 + // Emoji-only: add top leading to avoid clipping the 481 + // glyph, then pull the bottom up by the same amount so 482 + // the glyph bottom-aligns with the avatar instead of 483 + // sitting above its line-box baseline. 484 + isOnlyEmoji(message.text) && [ 485 + a.leading_tight, 486 + // Visually align bottom of the emoji with the avatar 487 + !isFromSelf && 488 + platform({ 489 + android: {marginTop: a.mt_2xs.marginTop}, 490 + default: {marginBottom: -a.mb_sm.marginBottom}, 491 + }), 492 + ], 493 + ]} 478 494 interactiveStyle={a.underline} 479 495 enableTags 480 496 emojiMultiplier={3}
+44
src/components/dms/SystemMessageItem.tsx
··· 1 + import {View} from 'react-native' 2 + import {useLingui} from '@lingui/react/macro' 3 + 4 + import {type ConvoItem} from '#/state/messages/convo/types' 5 + import {atoms as a, useTheme} from '#/alf' 6 + import {getSystemMessageInfo} from '#/components/dms/systemMessage' 7 + import {Text} from '#/components/Typography' 8 + 9 + export function SystemMessageItem({ 10 + item, 11 + }: { 12 + item: ConvoItem & {type: 'system-message'} 13 + }) { 14 + const t = useTheme() 15 + const {i18n} = useLingui() 16 + 17 + const info = getSystemMessageInfo(item.message.data) 18 + if (!info) return null 19 + 20 + const {Icon, message} = info 21 + 22 + return ( 23 + <View 24 + style={[ 25 + a.w_full, 26 + a.flex_row, 27 + a.align_center, 28 + a.justify_center, 29 + a.px_md, 30 + a.mt_md, 31 + ]}> 32 + <Icon size="xs" style={[a.mr_2xs, t.atoms.text_contrast_medium]} /> 33 + <Text 34 + style={[ 35 + a.text_xs, 36 + a.text_center, 37 + t.atoms.text_contrast_medium, 38 + {includeFontPadding: false, textAlignVertical: 'center'}, 39 + ]}> 40 + {i18n._(message)} 41 + </Text> 42 + </View> 43 + ) 44 + }
+68
src/components/dms/systemMessage.ts
··· 1 + import {ChatBskyConvoDefs} from '@atproto/api' 2 + import {type MessageDescriptor} from '@lingui/core' 3 + import {msg} from '@lingui/core/macro' 4 + 5 + import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 6 + import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft' 7 + import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight' 8 + import { 9 + ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon, 10 + ChainLinkBroken_Stroke2_Corner0_Rounded as ChainLinkBrokenIcon, 11 + } from '#/components/icons/ChainLink' 12 + import {type Props as SVGIconProps} from '#/components/icons/common' 13 + import { 14 + Lock_Stroke2_Corner0_Rounded as LockIcon, 15 + Unlock_Stroke2_Corner2_Rounded as UnlockIcon, 16 + } from '#/components/icons/Lock' 17 + import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil' 18 + 19 + export type SystemMessageInfo = { 20 + message: MessageDescriptor 21 + Icon: React.ComponentType<SVGIconProps> 22 + } 23 + 24 + export function getSystemMessageInfo( 25 + data: ChatBskyConvoDefs.SystemMessageView['data'], 26 + ): SystemMessageInfo | null { 27 + if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) { 28 + return { 29 + Icon: JoinIcon, 30 + message: msg`${createSanitizedDisplayName(data.member)} was added to the group`, 31 + } 32 + } else if (ChatBskyConvoDefs.isSystemMessageDataRemoveMember(data)) { 33 + return { 34 + Icon: LeaveIcon, 35 + message: msg`${createSanitizedDisplayName(data.member)} was removed from the group`, 36 + } 37 + } else if (ChatBskyConvoDefs.isSystemMessageDataMemberJoin(data)) { 38 + return { 39 + Icon: JoinIcon, 40 + message: msg`${createSanitizedDisplayName(data.member)} joined the group`, 41 + } 42 + } else if (ChatBskyConvoDefs.isSystemMessageDataMemberLeave(data)) { 43 + return { 44 + Icon: LeaveIcon, 45 + message: msg`${createSanitizedDisplayName(data.member)} left the group`, 46 + } 47 + } else if (ChatBskyConvoDefs.isSystemMessageDataLockConvo(data)) { 48 + return {Icon: LockIcon, message: msg`Chat locked`} 49 + } else if (ChatBskyConvoDefs.isSystemMessageDataUnlockConvo(data)) { 50 + return {Icon: UnlockIcon, message: msg`Chat unlocked`} 51 + } else if (ChatBskyConvoDefs.isSystemMessageDataLockConvoPermanently(data)) { 52 + return {Icon: LockIcon, message: msg`Chat locked permanently`} 53 + } else if (ChatBskyConvoDefs.isSystemMessageDataEditGroup(data)) { 54 + return { 55 + Icon: PencilIcon, 56 + message: msg`Chat title changed to ${data.newName ?? ''}`, 57 + } 58 + } else if (ChatBskyConvoDefs.isSystemMessageDataCreateJoinLink(data)) { 59 + return {Icon: ChainLinkIcon, message: msg`Invite link created`} 60 + } else if (ChatBskyConvoDefs.isSystemMessageDataEditJoinLink(data)) { 61 + return {Icon: ChainLinkIcon, message: msg`Invite link edited`} 62 + } else if (ChatBskyConvoDefs.isSystemMessageDataEnableJoinLink(data)) { 63 + return {Icon: ChainLinkIcon, message: msg`Invite link enabled`} 64 + } else if (ChatBskyConvoDefs.isSystemMessageDataDisableJoinLink(data)) { 65 + return {Icon: ChainLinkBrokenIcon, message: msg`Invite link disabled`} 66 + } 67 + return null 68 + }
+5
src/components/icons/ArrowBoxRight.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const ArrowBoxRight_Stroke2_Corner3_Rounded = createSinglePathSVG({ 4 + 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', 5 + })
+4
src/components/icons/ChainLink.tsx
··· 3 3 export const ChainLink_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 4 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', 5 5 }) 6 + 7 + export const ChainLinkBroken_Stroke2_Corner0_Rounded = createSinglePathSVG({ 8 + 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', 9 + })
+4
src/components/icons/Lock.tsx
··· 7 7 export const Lock_Stroke2_Corner2_Rounded = createSinglePathSVG({ 8 8 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', 9 9 }) 10 + 11 + export const Unlock_Stroke2_Corner2_Rounded = createSinglePathSVG({ 12 + path: 'M12 13a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z"/><path fill="#000" fill-rule="evenodd" d="M12 2a5 5 0 0 1 4.843 3.751 1 1 0 0 1-1.938.498A3.002 3.002 0 0 0 9 7v2h8a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-7a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5Zm-5 9a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H7Z', 13 + })
+15 -1
src/screens/Messages/components/ChatListItem.tsx
··· 37 37 import {useDialogControl} from '#/components/Dialog' 38 38 import {ConvoMenu} from '#/components/dms/ConvoMenu' 39 39 import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt' 40 + import {getSystemMessageInfo} from '#/components/dms/systemMessage' 40 41 import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util' 41 42 import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2' 42 43 import {Envelope_Open_Stroke2_Corner0_Rounded as EnvelopeOpen} from '#/components/icons/EnveopeOpen' ··· 233 234 }) { 234 235 const ax = useAnalytics() 235 236 const t = useTheme() 236 - const {t: l} = useLingui() 237 + const {t: l, i18n} = useLingui() 237 238 const {currentAccount} = useSession() 238 239 const menuControl = useMenuControl() 239 240 const leaveConvoControl = useDialogControl() ··· 265 266 266 267 let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined 267 268 269 + // Message 268 270 if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) { 269 271 const isFromMe = convo.lastMessage.sender?.did === currentAccount?.did 270 272 ··· 310 312 311 313 lastMessageSentAt = convo.lastMessage.sentAt 312 314 } 315 + 316 + // Deleted message 313 317 if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) { 314 318 lastMessageSentAt = convo.lastMessage.sentAt 315 319 ··· 318 322 : l`Message deleted` 319 323 } 320 324 325 + // Reaction 321 326 if (ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction)) { 322 327 if ( 323 328 !lastMessageSentAt || ··· 362 367 } 363 368 } 364 369 370 + // System message 371 + if (ChatBskyConvoDefs.isSystemMessageView(convo.lastMessage)) { 372 + const info = getSystemMessageInfo(convo.lastMessage.data) 373 + if (info) { 374 + lastMessage = i18n._(info.message) 375 + } 376 + } 377 + 365 378 return { 366 379 lastMessage, 367 380 lastMessageSentAt, ··· 369 382 } 370 383 }, [ 371 384 l, 385 + i18n, 372 386 convo.lastMessage, 373 387 convo.lastReaction, 374 388 currentAccount?.did,
+3
src/screens/Messages/components/MessagesList.tsx
··· 58 58 import {DateDividerToggleProvider} from '#/components/dms/DateDividerToggle' 59 59 import {MessageItem} from '#/components/dms/MessageItem' 60 60 import {NewMessagesPill} from '#/components/dms/NewMessagesPill' 61 + import {SystemMessageItem} from '#/components/dms/SystemMessageItem' 61 62 import {Loader} from '#/components/Loader' 62 63 import {Text} from '#/components/Typography' 63 64 import {useAnalytics} from '#/analytics' ··· 383 384 ) 384 385 } else if (item.type === 'deleted-message') { 385 386 return <Text>Deleted message</Text> 387 + } else if (item.type === 'system-message') { 388 + return <SystemMessageItem item={item} /> 386 389 } else if (item.type === 'error') { 387 390 return <MessageListError item={item} /> 388 391 }
+46 -3
src/state/messages/convo/agent.ts
··· 52 52 ) 53 53 } 54 54 55 + function toSystemMessageView( 56 + ev: ChatBskyConvoGetLog.OutputSchema['logs'][number], 57 + ): ChatBskyConvoDefs.SystemMessageView | null { 58 + const isSystem = 59 + ChatBskyConvoDefs.isLogAddMember(ev) || 60 + ChatBskyConvoDefs.isLogRemoveMember(ev) || 61 + ChatBskyConvoDefs.isLogMemberJoin(ev) || 62 + ChatBskyConvoDefs.isLogMemberLeave(ev) || 63 + ChatBskyConvoDefs.isLogLockConvo(ev) || 64 + ChatBskyConvoDefs.isLogUnlockConvo(ev) || 65 + ChatBskyConvoDefs.isLogLockConvoPermanently(ev) || 66 + ChatBskyConvoDefs.isLogEditGroup(ev) || 67 + ChatBskyConvoDefs.isLogCreateJoinLink(ev) || 68 + ChatBskyConvoDefs.isLogEditJoinLink(ev) || 69 + ChatBskyConvoDefs.isLogEnableJoinLink(ev) || 70 + ChatBskyConvoDefs.isLogDisableJoinLink(ev) 71 + if (!isSystem) return null 72 + return ev.message 73 + } 74 + 55 75 export class Convo { 56 76 private id: string 57 77 ··· 67 87 68 88 private pastMessages: Map< 69 89 string, 70 - ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.DeletedMessageView 90 + | ChatBskyConvoDefs.MessageView 91 + | ChatBskyConvoDefs.DeletedMessageView 92 + | ChatBskyConvoDefs.SystemMessageView 71 93 > = new Map() 72 94 private newMessages: Map< 73 95 string, 74 - ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.DeletedMessageView 96 + | ChatBskyConvoDefs.MessageView 97 + | ChatBskyConvoDefs.DeletedMessageView 98 + | ChatBskyConvoDefs.SystemMessageView 75 99 > = new Map() 76 100 private pendingMessages: Map< 77 101 string, ··· 680 704 for (const message of messages) { 681 705 if ( 682 706 ChatBskyConvoDefs.isMessageView(message) || 683 - ChatBskyConvoDefs.isDeletedMessageView(message) 707 + ChatBskyConvoDefs.isDeletedMessageView(message) || 708 + ChatBskyConvoDefs.isSystemMessageView(message) 684 709 ) { 685 710 /* 686 711 * If this message is already in new messages, it was added by the ··· 830 855 } 831 856 if (this.newMessages.has(ev.message.id)) { 832 857 this.newMessages.set(ev.message.id, ev.message) 858 + needsCommit = true 859 + } 860 + } else { 861 + const systemView = toSystemMessageView(ev) 862 + if (systemView) { 863 + this.newMessages.set(systemView.id, systemView) 833 864 needsCommit = true 834 865 } 835 866 } ··· 1119 1150 nextMessage: null, 1120 1151 prevMessage: null, 1121 1152 }) 1153 + } else if (ChatBskyConvoDefs.isSystemMessageView(m)) { 1154 + items.unshift({ 1155 + type: 'system-message', 1156 + key: m.id, 1157 + message: m, 1158 + }) 1122 1159 } 1123 1160 }) 1124 1161 ··· 1149 1186 message: m, 1150 1187 nextMessage: null, 1151 1188 prevMessage: null, 1189 + }) 1190 + } else if (ChatBskyConvoDefs.isSystemMessageView(m)) { 1191 + items.push({ 1192 + type: 'system-message', 1193 + key: m.id, 1194 + message: m, 1152 1195 }) 1153 1196 } 1154 1197 })
+5
src/state/messages/convo/types.ts
··· 127 127 | null 128 128 } 129 129 | { 130 + type: 'system-message' 131 + key: string 132 + message: ChatBskyConvoDefs.SystemMessageView 133 + } 134 + | { 130 135 type: 'error' 131 136 key: string 132 137 code: ConvoItemError
+4 -4
yarn.lock
··· 20 20 "@jridgewell/gen-mapping" "^0.3.0" 21 21 "@jridgewell/trace-mapping" "^0.3.9" 22 22 23 - "@atproto/api@^0.19.9": 24 - version "0.19.9" 25 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.9.tgz#f09ed8412159d6878eeaf25a0a8b4445c62fa9eb" 26 - integrity sha512-+sUYNuiA1Rv8HemMCURHwRkMp2D7cq6nNquefjosu6UB54IzkD0MLK3YY383poLRShiApouOxRse2OKK25dbQw== 23 + "@atproto/api@^0.19.10": 24 + version "0.19.10" 25 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.10.tgz#311a3fb8c642ee8bb1eea96acadfedbf500e9f08" 26 + integrity sha512-HrHWjL6aEEfUKBSZFZ7Fz+MF3WcGOdhTlX6f71j4fgf91pMhwxgdo2K13Qjn2CxIh8/iHAJi+oiLWKOgZnOLNA== 27 27 dependencies: 28 28 "@atproto/common-web" "^0.4.21" 29 29 "@atproto/lexicon" "^0.6.2"