Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Display sender name in last message for group clip clops (#10320)

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

authored by

DS Boyce
Samuel Newman
and committed by
GitHub
dcc06a90 c7e9efbf

+253 -134
+1 -1
package.json
··· 81 81 "icons:optimize": "svgo -f ./assets/icons" 82 82 }, 83 83 "dependencies": { 84 - "@atproto/api": "^0.19.10", 84 + "@atproto/api": "^0.19.11", 85 85 "@atproto/syntax": "0.5.2", 86 86 "@bitdrift/react-native": "^0.6.8", 87 87 "@braintree/sanitize-url": "^6.0.2",
+6 -1
src/components/AvatarBubbles.tsx
··· 45 45 : size === 'medium' 46 46 ? 56 / 120 47 47 : 1 48 - const marginOffset = size === 'small' || size === 'medium' ? -2 : 0 48 + const marginOffset = 49 + (typeof size === 'number' && size < 120) || 50 + size === 'small' || 51 + size === 'medium' 52 + ? -2 53 + : 0 49 54 50 55 const initialValue = animate ? 0 : 1 51 56 const p0 = useSharedValue(initialValue)
+3 -2
src/components/dms/SystemMessageItem.tsx
··· 3 3 4 4 import {type ConvoItem} from '#/state/messages/convo/types' 5 5 import {atoms as a, useTheme} from '#/alf' 6 - import {getSystemMessageInfo} from '#/components/dms/systemMessage' 6 + import {getSystemMessageInfo} from '#/components/dms/getSystemMessageInfo' 7 7 import {Text} from '#/components/Typography' 8 8 9 9 export function SystemMessageItem({ ··· 14 14 const t = useTheme() 15 15 const {i18n} = useLingui() 16 16 17 - const info = getSystemMessageInfo(item.message.data) 17 + const info = getSystemMessageInfo(item.message.data, item.relatedProfiles) 18 18 if (!info) return null 19 19 20 20 const {Icon, message} = info ··· 28 28 a.justify_center, 29 29 a.px_md, 30 30 a.mt_md, 31 + a.mb_xs, 31 32 ]}> 32 33 <Icon size="xs" style={[a.mr_2xs, t.atoms.text_contrast_medium]} /> 33 34 <Text
+93
src/components/dms/getMessageInfo.ts
··· 1 + import {AppBskyEmbedRecord, ChatBskyConvoDefs} from '@atproto/api' 2 + import {type I18n} from '@lingui/core' 3 + import {msg} from '@lingui/core/macro' 4 + 5 + import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 6 + import { 7 + postUriToRelativePath, 8 + toBskyAppUrl, 9 + toShortUrl, 10 + } from '#/lib/strings/url-helpers' 11 + 12 + export type UserMessageInfo = { 13 + message: string | null 14 + sentAt: string 15 + reportableMessage?: ChatBskyConvoDefs.MessageView 16 + } 17 + 18 + export function getMessageInfo({ 19 + convo, 20 + currentAccountDid, 21 + i18n, 22 + }: { 23 + convo: ChatBskyConvoDefs.ConvoView 24 + currentAccountDid: string | undefined 25 + i18n: I18n 26 + }): UserMessageInfo | null { 27 + if (!ChatBskyConvoDefs.isMessageView(convo.lastMessage)) { 28 + return null 29 + } 30 + 31 + const lastMessage = convo.lastMessage 32 + const isFromMe = lastMessage.sender?.did === currentAccountDid 33 + const senderDid = lastMessage.sender?.did 34 + const sender = convo.members.find(m => m.did === senderDid) 35 + const name = sender ? createSanitizedDisplayName(sender) : null 36 + const isGroup = ChatBskyConvoDefs.isGroupConvo(convo.kind) 37 + 38 + const reportableMessage = isFromMe ? undefined : lastMessage 39 + 40 + const prefix = (message: string) => { 41 + if (isFromMe) { 42 + return i18n._( 43 + msg({ 44 + message: `You: ${message}`, 45 + comment: 'When the last message in a chat was made by you.', 46 + }), 47 + ) 48 + } else if (isGroup && name) { 49 + return i18n._( 50 + msg({ 51 + message: `${name}: ${message}`, 52 + comment: 53 + 'When the last message in a group chat came from someone other than you.', 54 + }), 55 + ) 56 + } 57 + return message 58 + } 59 + 60 + let message: string | null = null 61 + 62 + if (lastMessage.text) { 63 + message = prefix(lastMessage.text) 64 + } else if (lastMessage.embed) { 65 + const defaultEmbeddedContentMessage = i18n._( 66 + msg`(contains embedded content)`, 67 + ) 68 + 69 + if (AppBskyEmbedRecord.isView(lastMessage.embed)) { 70 + const embed = lastMessage.embed 71 + 72 + if (AppBskyEmbedRecord.isViewRecord(embed.record)) { 73 + const record = embed.record 74 + const path = postUriToRelativePath(record.uri, { 75 + handle: record.author.handle, 76 + }) 77 + const href = path ? toBskyAppUrl(path) : undefined 78 + const short = href ? toShortUrl(href) : defaultEmbeddedContentMessage 79 + message = prefix(short) 80 + } else { 81 + message = prefix(defaultEmbeddedContentMessage) 82 + } 83 + } else { 84 + message = prefix(defaultEmbeddedContentMessage) 85 + } 86 + } 87 + 88 + return { 89 + message, 90 + sentAt: lastMessage.sentAt, 91 + reportableMessage, 92 + } 93 + }
+54
src/components/dms/getReactionInfo.ts
··· 1 + import {ChatBskyConvoDefs} from '@atproto/api' 2 + import {type I18n} from '@lingui/core' 3 + import {msg} from '@lingui/core/macro' 4 + 5 + import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 6 + 7 + export type UserReactionInfo = { 8 + message: string 9 + createdAt: string 10 + } 11 + 12 + export function getReactionInfo({ 13 + convo, 14 + currentAccountDid, 15 + i18n, 16 + }: { 17 + convo: ChatBskyConvoDefs.ConvoView 18 + currentAccountDid: string | undefined 19 + i18n: I18n 20 + }): UserReactionInfo | null { 21 + if (!ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction)) { 22 + return null 23 + } 24 + 25 + const {reaction, message: reactedTo} = convo.lastReaction 26 + const isFromMe = reaction.sender.did === currentAccountDid 27 + const senderDid = reaction.sender.did 28 + const sender = convo.members.find(m => m.did === senderDid) 29 + const name = sender ? createSanitizedDisplayName(sender) : null 30 + 31 + const lastMessageText = reactedTo.text 32 + const fallbackMessage = i18n._( 33 + msg({ 34 + message: 'a message', 35 + comment: 36 + 'If last message does not contain text, fall back to "{user} reacted to {a message}"', 37 + }), 38 + ) 39 + const target = lastMessageText ? `"${lastMessageText}"` : fallbackMessage 40 + 41 + let message: string 42 + if (isFromMe) { 43 + message = i18n._(msg`You reacted ${reaction.value} to ${target}`) 44 + } else if (name) { 45 + message = i18n._(msg`${name} reacted ${reaction.value} to ${target}`) 46 + } else { 47 + message = i18n._(msg`Someone reacted ${reaction.value} to ${target}`) 48 + } 49 + 50 + return { 51 + message, 52 + createdAt: reaction.createdAt, 53 + } 54 + }
+27 -6
src/components/dms/systemMessage.ts src/components/dms/getSystemMessageInfo.ts
··· 1 - import {ChatBskyConvoDefs} from '@atproto/api' 1 + import {type ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api' 2 2 import {type MessageDescriptor} from '@lingui/core' 3 3 import {msg} from '@lingui/core/macro' 4 4 ··· 21 21 Icon: React.ComponentType<SVGIconProps> 22 22 } 23 23 24 + function getReferredDisplayName( 25 + user: ChatBskyConvoDefs.SystemMessageReferredUser, 26 + relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[], 27 + ): string | null { 28 + const profile = relatedProfiles.find(p => p.did === user.did) 29 + return profile ? createSanitizedDisplayName(profile) : null 30 + } 31 + 24 32 export function getSystemMessageInfo( 25 33 data: ChatBskyConvoDefs.SystemMessageView['data'], 34 + relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[], 26 35 ): SystemMessageInfo | null { 27 36 if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) { 37 + const name = getReferredDisplayName(data.member, relatedProfiles) 28 38 return { 29 39 Icon: JoinIcon, 30 - message: msg`${createSanitizedDisplayName(data.member)} was added to the group`, 40 + message: name 41 + ? msg`${name} was added to the group` 42 + : msg`Someone was added to the group`, 31 43 } 32 44 } else if (ChatBskyConvoDefs.isSystemMessageDataRemoveMember(data)) { 45 + const name = getReferredDisplayName(data.member, relatedProfiles) 33 46 return { 34 47 Icon: LeaveIcon, 35 - message: msg`${createSanitizedDisplayName(data.member)} was removed from the group`, 48 + message: name 49 + ? msg`${name} was removed from the group` 50 + : msg`Someone was removed from the group`, 36 51 } 37 52 } else if (ChatBskyConvoDefs.isSystemMessageDataMemberJoin(data)) { 53 + const name = getReferredDisplayName(data.member, relatedProfiles) 38 54 return { 39 55 Icon: JoinIcon, 40 - message: msg`${createSanitizedDisplayName(data.member)} joined the group`, 56 + message: name 57 + ? msg`${name} joined the group` 58 + : msg`Someone joined the group`, 41 59 } 42 60 } else if (ChatBskyConvoDefs.isSystemMessageDataMemberLeave(data)) { 61 + const name = getReferredDisplayName(data.member, relatedProfiles) 43 62 return { 44 63 Icon: LeaveIcon, 45 - message: msg`${createSanitizedDisplayName(data.member)} left the group`, 64 + message: name ? msg`${name} left the group` : msg`Someone left the group`, 46 65 } 47 66 } else if (ChatBskyConvoDefs.isSystemMessageDataLockConvo(data)) { 48 67 return {Icon: LockIcon, message: msg`Chat locked`} ··· 53 72 } else if (ChatBskyConvoDefs.isSystemMessageDataEditGroup(data)) { 54 73 return { 55 74 Icon: PencilIcon, 56 - message: msg`Chat title changed to ${data.newName ?? ''}`, 75 + message: data.newName 76 + ? msg`Chat title changed to ${data.newName}` 77 + : msg`Chat title changed`, 57 78 } 58 79 } else if (ChatBskyConvoDefs.isSystemMessageDataCreateJoinLink(data)) { 59 80 return {Icon: ChainLinkIcon, message: msg`Invite link created`}
+6 -3
src/components/dms/util.ts
··· 110 110 owner = member as GroupConvoMember 111 111 } 112 112 } else { 113 - throw new Error( 113 + logger.warn( 114 114 'Expected a GroupConvoMember, got an unknown kind of member', 115 115 ) 116 + return null 116 117 } 117 118 } 118 119 119 120 if (!owner) { 120 - throw new Error('No owner found in group convo') 121 + logger.warn('No owner found in group convo') 122 + return null 121 123 } 122 124 123 125 return { ··· 136 138 const otherUser = convoView.members.find(m => m.did !== ownDid) 137 139 138 140 if (!otherUser) { 139 - throw new Error('No other user found in direct convo') 141 + logger.warn('No other user found in direct convo') 142 + return null 140 143 } 141 144 142 145 return {
+36 -116
src/screens/Messages/components/ChatListItem.tsx
··· 1 1 import {useCallback, useMemo, useState} from 'react' 2 2 import {type GestureResponderEvent, View} from 'react-native' 3 3 import { 4 - AppBskyEmbedRecord, 5 4 ChatBskyConvoDefs, 6 5 moderateProfile, 7 6 type ModerationDecision, ··· 14 13 import {useHaptics} from '#/lib/haptics' 15 14 import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 16 15 import {decrementBadgeCount} from '#/lib/notifications/notifications' 17 - import {sanitizeDisplayName} from '#/lib/strings/display-names' 18 16 import {sanitizeHandle} from '#/lib/strings/handles' 19 - import { 20 - postUriToRelativePath, 21 - toBskyAppUrl, 22 - toShortUrl, 23 - } from '#/lib/strings/url-helpers' 24 17 import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow' 25 18 import {useModerationOpts} from '#/state/preferences/moderation-opts' 26 19 import { ··· 36 29 import {AvatarBubbles} from '#/components/AvatarBubbles' 37 30 import {useDialogControl} from '#/components/Dialog' 38 31 import {ConvoMenu} from '#/components/dms/ConvoMenu' 32 + import {getMessageInfo} from '#/components/dms/getMessageInfo' 33 + import {getReactionInfo} from '#/components/dms/getReactionInfo' 34 + import {getSystemMessageInfo} from '#/components/dms/getSystemMessageInfo' 39 35 import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt' 40 - import {getSystemMessageInfo} from '#/components/dms/systemMessage' 41 36 import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util' 42 37 import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2' 43 38 import {Envelope_Open_Stroke2_Corner0_Rounded as EnvelopeOpen} from '#/components/icons/EnveopeOpen' ··· 53 48 import type * as bsky from '#/types/bsky' 54 49 55 50 export const ChatListItemPortal = createPortalGroup() 56 - 57 - /** 58 - * IMPORTANT NOTE: THIS IS CURRENTLY JANKY AF AND PROBABLY BROKEN, JUST WANTED TO ADD GROUPCHAT SUPPPORT 59 - * 60 - * TAKE A SECOND PASS PLEASE -sfn 61 - */ 62 51 63 52 export function ChatListItem({ 64 53 convo: convoView, ··· 94 83 <GroupChatItem 95 84 convo={convo} 96 85 moderationOpts={moderationOpts} 97 - showMenu={showMenu} 98 - /> 86 + showMenu={showMenu}> 87 + {children} 88 + </GroupChatItem> 99 89 ) 100 90 } 101 91 default: { ··· 189 179 return ( 190 180 <BaseChatItem 191 181 convo={convo.view} 192 - avatar={<AvatarBubbles profiles={convo.members} size="medium" />} 182 + avatar={<AvatarBubbles profiles={convo.members} size={52} />} 193 183 title={chatName} 194 184 accessibilityHint={l`Go to the group chat named "${chatName}"`} 195 185 primaryProfile={groupOwner} ··· 243 233 244 234 const playHaptic = useHaptics() 245 235 const queryClient = useQueryClient() 246 - const isUnread = convo.unreadCount > 0 236 + const hasUnread = convo.unreadCount > 0 && !isDeletedAccount 247 237 248 238 const blockInfo = useMemo(() => { 249 239 const modui = primaryProfileModeration.ui('profileView') ··· 266 256 267 257 let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined 268 258 269 - // Message 270 - if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) { 271 - const isFromMe = convo.lastMessage.sender?.did === currentAccount?.did 272 - 273 - if (!isFromMe) { 274 - latestReportableMessage = convo.lastMessage 275 - } 276 - 277 - if (convo.lastMessage.text) { 278 - if (isFromMe) { 279 - lastMessage = l`You: ${convo.lastMessage.text}` 280 - } else { 281 - lastMessage = convo.lastMessage.text 282 - } 283 - } else if (convo.lastMessage.embed) { 284 - const defaultEmbeddedContentMessage = l`(contains embedded content)` 285 - 286 - if (AppBskyEmbedRecord.isView(convo.lastMessage.embed)) { 287 - const embed = convo.lastMessage.embed 288 - 289 - if (AppBskyEmbedRecord.isViewRecord(embed.record)) { 290 - const record = embed.record 291 - const path = postUriToRelativePath(record.uri, { 292 - handle: record.author.handle, 293 - }) 294 - const href = path ? toBskyAppUrl(path) : undefined 295 - const short = href 296 - ? toShortUrl(href) 297 - : defaultEmbeddedContentMessage 298 - if (isFromMe) { 299 - lastMessage = l`You: ${short}` 300 - } else { 301 - lastMessage = short 302 - } 303 - } 304 - } else { 305 - if (isFromMe) { 306 - lastMessage = l`You: ${defaultEmbeddedContentMessage}` 307 - } else { 308 - lastMessage = defaultEmbeddedContentMessage 309 - } 310 - } 311 - } 312 - 313 - lastMessageSentAt = convo.lastMessage.sentAt 314 - } 315 - 316 259 // Deleted message 317 260 if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) { 318 261 lastMessageSentAt = convo.lastMessage.sentAt ··· 322 265 : l`Message deleted` 323 266 } 324 267 268 + // Message 269 + if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) { 270 + const info = getMessageInfo({ 271 + convo, 272 + currentAccountDid: currentAccount?.did, 273 + i18n, 274 + }) 275 + if (info) { 276 + lastMessage = info.message ?? lastMessage 277 + lastMessageSentAt = info.sentAt 278 + latestReportableMessage = info.reportableMessage 279 + } 280 + } 281 + 325 282 // Reaction 326 283 if (ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction)) { 284 + const info = getReactionInfo({ 285 + convo, 286 + currentAccountDid: currentAccount?.did, 287 + i18n, 288 + }) 327 289 if ( 328 - !lastMessageSentAt || 329 - new Date(lastMessageSentAt) < 330 - new Date(convo.lastReaction.reaction.createdAt) 290 + info && 291 + (!lastMessageSentAt || 292 + new Date(lastMessageSentAt) < new Date(info.createdAt)) 331 293 ) { 332 - const isFromMe = 333 - convo.lastReaction.reaction.sender.did === currentAccount?.did 334 - const lastMessageText = convo.lastReaction.message.text 335 - const fallbackMessage = l({ 336 - message: 'a message', 337 - comment: `If last message does not contain text, fall back to "{user} reacted to {a message}"`, 338 - }) 339 - 340 - if (isFromMe) { 341 - lastMessage = l`You reacted ${convo.lastReaction.reaction.value} to ${ 342 - lastMessageText 343 - ? `"${convo.lastReaction.message.text}"` 344 - : fallbackMessage 345 - }` 346 - } else { 347 - const senderDid = convo.lastReaction.reaction.sender.did 348 - const sender = convo.members.find( 349 - member => member.did === senderDid, 350 - ) 351 - if (sender) { 352 - lastMessage = l`${sanitizeDisplayName( 353 - sender.displayName || sender.handle, 354 - )} reacted ${convo.lastReaction.reaction.value} to ${ 355 - lastMessageText 356 - ? `"${convo.lastReaction.message.text}"` 357 - : fallbackMessage 358 - }` 359 - } else { 360 - lastMessage = l`Someone reacted ${convo.lastReaction.reaction.value} to ${ 361 - lastMessageText 362 - ? `"${convo.lastReaction.message.text}"` 363 - : fallbackMessage 364 - }` 365 - } 366 - } 294 + lastMessage = info.message 295 + lastMessageSentAt = info.createdAt 367 296 } 368 297 } 369 298 370 299 // System message 371 300 if (ChatBskyConvoDefs.isSystemMessageView(convo.lastMessage)) { 372 - const info = getSystemMessageInfo(convo.lastMessage.data) 301 + const info = getSystemMessageInfo(convo.lastMessage.data, convo.members) 373 302 if (info) { 374 303 lastMessage = i18n._(info.message) 304 + lastMessageSentAt = convo.lastMessage.sentAt 375 305 } 376 306 } 377 307 ··· 380 310 lastMessageSentAt, 381 311 latestReportableMessage, 382 312 } 383 - }, [ 384 - l, 385 - i18n, 386 - convo.lastMessage, 387 - convo.lastReaction, 388 - currentAccount?.did, 389 - isDeletedAccount, 390 - convo.members, 391 - ]) 313 + }, [l, convo, currentAccount?.did, isDeletedAccount, i18n]) 392 314 393 315 const [showActions, setShowActions] = useState(false) 394 316 ··· 448 370 }, 449 371 } 450 372 451 - const actions = isUnread 373 + const actions = hasUnread 452 374 ? { 453 375 leftFirst: markReadAction, 454 376 leftSecond: deleteAction, ··· 456 378 : { 457 379 leftFirst: deleteAction, 458 380 } 459 - 460 - const hasUnread = convo.unreadCount > 0 && !isDeletedAccount 461 381 462 382 return ( 463 383 <ChatListItemPortal.Provider>
+22 -1
src/state/messages/convo/agent.ts
··· 102 102 {id: string; message: ChatBskyConvoSendMessage.InputSchema['message']} 103 103 > = new Map() 104 104 private deletedMessages: Set<string> = new Set() 105 + private systemMessageProfiles: Map< 106 + string, 107 + ChatBskyActorDefs.ProfileViewBasic 108 + > = new Map() 105 109 106 110 private isProcessingPendingMessages = false 107 111 ··· 469 473 this.newMessages = new Map() 470 474 this.pendingMessages = new Map() 471 475 this.deletedMessages = new Set() 476 + this.systemMessageProfiles = new Map() 472 477 473 478 this.pendingMessageFailure = null 474 479 this.fetchMessageHistoryError = undefined ··· 689 694 {headers: DM_SERVICE_HEADERS}, 690 695 ) 691 696 }) 692 - const {cursor, messages} = response.data 697 + const {cursor, messages, relatedProfiles} = response.data 693 698 694 699 this.oldestRev = cursor ?? null 695 700 701 + if (relatedProfiles) { 702 + for (const profile of relatedProfiles) { 703 + this.systemMessageProfiles.set(profile.did, profile) 704 + } 705 + } 706 + 696 707 /* 697 708 * If the response contained fewer messages than the limit, we know 698 709 * there are no more pages, regardless of whether a cursor was returned. ··· 861 872 const systemView = toSystemMessageView(ev) 862 873 if (systemView) { 863 874 this.newMessages.set(systemView.id, systemView) 875 + if ( 876 + 'relatedProfiles' in ev && 877 + Array.isArray(ev.relatedProfiles) 878 + ) { 879 + for (const profile of ev.relatedProfiles) { 880 + this.systemMessageProfiles.set(profile.did, profile) 881 + } 882 + } 864 883 needsCommit = true 865 884 } 866 885 } ··· 1155 1174 type: 'system-message', 1156 1175 key: m.id, 1157 1176 message: m, 1177 + relatedProfiles: Array.from(this.systemMessageProfiles.values()), 1158 1178 }) 1159 1179 } 1160 1180 }) ··· 1192 1212 type: 'system-message', 1193 1213 key: m.id, 1194 1214 message: m, 1215 + relatedProfiles: Array.from(this.systemMessageProfiles.values()), 1195 1216 }) 1196 1217 } 1197 1218 })
+1
src/state/messages/convo/types.ts
··· 130 130 type: 'system-message' 131 131 key: string 132 132 message: ChatBskyConvoDefs.SystemMessageView 133 + relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[] 133 134 } 134 135 | { 135 136 type: 'error'
+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.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== 23 + "@atproto/api@^0.19.11": 24 + version "0.19.11" 25 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.11.tgz#73885a47959907f22b68d671011ee70f80afd44b" 26 + integrity sha512-7V4Sg6hcv/UxoXobjfvy/Ox2ioKQtZ3DzbsiFndYCcBfsZ5GO8rNEroHPq3hT0CFBJK1NAD6JfOtTBN2z267Xg== 27 27 dependencies: 28 28 "@atproto/common-web" "^0.4.21" 29 29 "@atproto/lexicon" "^0.6.2"