@linktr.ee/messaging-react 3.6.1 → 3.6.2-rc-1783304704
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Card-d0_xEfdM.cjs → Card-6FOd4_j_.cjs} +2 -2
- package/dist/{Card-d0_xEfdM.cjs.map → Card-6FOd4_j_.cjs.map} +1 -1
- package/dist/{Card-DcUPqd79.cjs → Card-BhIG7T7c.cjs} +2 -2
- package/dist/{Card-DcUPqd79.cjs.map → Card-BhIG7T7c.cjs.map} +1 -1
- package/dist/{Card-BuR6mbUu.cjs → Card-CA63InbA.cjs} +2 -2
- package/dist/{Card-BuR6mbUu.cjs.map → Card-CA63InbA.cjs.map} +1 -1
- package/dist/{Card-B0lzpuYf.js → Card-C_TfROjB.js} +3 -3
- package/dist/{Card-B0lzpuYf.js.map → Card-C_TfROjB.js.map} +1 -1
- package/dist/{Card-BrC7mKtr.js → Card-DN8P8IeT.js} +2 -2
- package/dist/{Card-BrC7mKtr.js.map → Card-DN8P8IeT.js.map} +1 -1
- package/dist/{Card-CxS7GNqp.js → Card-DgWCIgWw.js} +2 -2
- package/dist/{Card-CxS7GNqp.js.map → Card-DgWCIgWw.js.map} +1 -1
- package/dist/{LockedThumbnail-BaDgF8qJ.js → LockedThumbnail-DNy4CoCm.js} +2 -2
- package/dist/{LockedThumbnail-BaDgF8qJ.js.map → LockedThumbnail-DNy4CoCm.js.map} +1 -1
- package/dist/{LockedThumbnail-BDtKMJjz.cjs → LockedThumbnail-DPlNLdpK.cjs} +2 -2
- package/dist/{LockedThumbnail-BDtKMJjz.cjs.map → LockedThumbnail-DPlNLdpK.cjs.map} +1 -1
- package/dist/index-ClREqfMm.cjs +2 -0
- package/dist/index-ClREqfMm.cjs.map +1 -0
- package/dist/{index-BPfehHDD.js → index-CxGOKFnx.js} +3786 -3876
- package/dist/index-CxGOKFnx.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/components/ChannelHeaderRedesign.tsx +231 -0
- package/src/components/ChannelView.stories.tsx +68 -11
- package/src/components/ChannelView.test.tsx +54 -13
- package/src/components/ChannelView.tsx +74 -28
- package/src/components/CustomMessage/index.tsx +0 -67
- package/dist/index-BPfehHDD.js.map +0 -1
- package/dist/index-G4pnfm4m.cjs +0 -2
- package/dist/index-G4pnfm4m.cjs.map +0 -1
- package/src/components/CustomMessage/StreamAttachmentMessage.stories.tsx +0 -178
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +0 -186
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +0 -417
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import type { Attachment } from 'stream-chat'
|
|
3
|
-
|
|
4
|
-
import LinkAttachment from '../LinkAttachment'
|
|
5
|
-
import { isPlayableMedia } from '../LinkAttachment/components/_shared/CardThumbnail'
|
|
6
|
-
import MessageAttachment from '../MessageAttachment'
|
|
7
|
-
import type { BubbleGroupPosition } from '../MessageAttachment/types'
|
|
8
|
-
|
|
9
|
-
type StreamMediaKind = 'image' | 'video' | 'audio' | 'pdf' | 'file'
|
|
10
|
-
|
|
11
|
-
export type AttachmentSegment =
|
|
12
|
-
| { type: 'link'; attachments: Attachment[] }
|
|
13
|
-
| { type: 'media'; kind: StreamMediaKind; attachments: Attachment[] }
|
|
14
|
-
|
|
15
|
-
type StreamThreadMessage = {
|
|
16
|
-
attachments?: Attachment[]
|
|
17
|
-
text?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const SENT_CAPTION_CLASS =
|
|
21
|
-
'mt-[2px] max-w-[280px] whitespace-pre-wrap break-words rounded-[18px] bg-[#121110] px-4 py-2 text-white'
|
|
22
|
-
const RECEIVED_CAPTION_CLASS =
|
|
23
|
-
'mt-[2px] max-w-[280px] whitespace-pre-wrap break-words rounded-[18px] bg-[#e9eaed] px-4 py-2 text-[#080707]'
|
|
24
|
-
|
|
25
|
-
function trimToUndefined(value?: string | null): string | undefined {
|
|
26
|
-
const trimmed = value?.trim()
|
|
27
|
-
return trimmed ? trimmed : undefined
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function normalizeLinkUrl(url: string): string {
|
|
31
|
-
return url
|
|
32
|
-
.trim()
|
|
33
|
-
.replace(/^https?:\/\//i, '')
|
|
34
|
-
.replace(/\/+$/, '')
|
|
35
|
-
.toLowerCase()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function linkCaptionDuplicatesAttachmentUrl(
|
|
39
|
-
caption: string | undefined,
|
|
40
|
-
attachmentUrl?: string
|
|
41
|
-
): boolean {
|
|
42
|
-
if (!caption?.trim() || !attachmentUrl?.trim()) {
|
|
43
|
-
return false
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const normalizedCaption = normalizeLinkUrl(caption)
|
|
47
|
-
const normalizedUrl = normalizeLinkUrl(attachmentUrl)
|
|
48
|
-
|
|
49
|
-
if (normalizedCaption === normalizedUrl) {
|
|
50
|
-
return true
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (!normalizedCaption.includes(normalizedUrl)) {
|
|
54
|
-
return false
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const remainder = normalizedCaption.split(normalizedUrl).join('')
|
|
58
|
-
return !/[^\p{P}\p{Z}\p{C}]/u.test(remainder)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function linkCardPropsFromStreamAttachment(a: Attachment) {
|
|
62
|
-
const url = trimToUndefined(a.og_scrape_url) ?? trimToUndefined(a.title_link)
|
|
63
|
-
const thumbnailUrl =
|
|
64
|
-
trimToUndefined((a as { image_url?: string }).image_url) ??
|
|
65
|
-
trimToUndefined((a as { thumb_url?: string }).thumb_url)
|
|
66
|
-
const sourceUrl = trimToUndefined(a.asset_url)
|
|
67
|
-
const mimeType = trimToUndefined(a.mime_type)
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
title: trimToUndefined((a as { title?: string }).title),
|
|
71
|
-
description: trimToUndefined((a as { text?: string }).text),
|
|
72
|
-
url,
|
|
73
|
-
thumbnailUrl,
|
|
74
|
-
// Playable clip embedded in a link preview — lets LinkAttachment render its
|
|
75
|
-
// native video/audio player in the hero instead of a static thumbnail.
|
|
76
|
-
sourceUrl,
|
|
77
|
-
layout:
|
|
78
|
-
thumbnailUrl || isPlayableMedia(mimeType, sourceUrl)
|
|
79
|
-
? 'featured'
|
|
80
|
-
: 'classic',
|
|
81
|
-
mimeType,
|
|
82
|
-
} as const
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function getAttachmentMediaKind(
|
|
86
|
-
attachment: Attachment
|
|
87
|
-
): StreamMediaKind | null {
|
|
88
|
-
if (isLinkAttachment(attachment)) return null
|
|
89
|
-
|
|
90
|
-
if (attachment.type === 'image') {
|
|
91
|
-
const imageUrl = (attachment as { image_url?: string }).image_url
|
|
92
|
-
if (imageUrl || attachment.asset_url) return 'image'
|
|
93
|
-
return null
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (attachment.type === 'video' && attachment.asset_url) return 'video'
|
|
97
|
-
if (attachment.type === 'audio' && attachment.asset_url) return 'audio'
|
|
98
|
-
|
|
99
|
-
if (attachment.type === 'file' && attachment.asset_url) {
|
|
100
|
-
if (attachment.mime_type === 'application/pdf') return 'pdf'
|
|
101
|
-
if (attachment.mime_type?.startsWith('audio/')) return 'audio'
|
|
102
|
-
return 'file'
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return null
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function isLinkAttachment(a: Attachment): boolean {
|
|
109
|
-
const ogScrapeUrl = trimToUndefined(a.og_scrape_url)
|
|
110
|
-
return a.type === 'link' || (!!ogScrapeUrl && !a.asset_url)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function buildOrderedAttachmentSegments(
|
|
114
|
-
attachments: Attachment[] | undefined
|
|
115
|
-
): AttachmentSegment[] {
|
|
116
|
-
if (!attachments?.length) return []
|
|
117
|
-
|
|
118
|
-
const segments: AttachmentSegment[] = []
|
|
119
|
-
|
|
120
|
-
const pushOrMergeMedia = (kind: StreamMediaKind, attachment: Attachment) => {
|
|
121
|
-
const last = segments[segments.length - 1]
|
|
122
|
-
if (last?.type === 'media' && last.kind === kind) {
|
|
123
|
-
last.attachments.push(attachment)
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
segments.push({ type: 'media', kind, attachments: [attachment] })
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const pushOrMergeLink = (attachment: Attachment) => {
|
|
131
|
-
const last = segments[segments.length - 1]
|
|
132
|
-
if (last?.type === 'link') {
|
|
133
|
-
last.attachments.push(attachment)
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
segments.push({ type: 'link', attachments: [attachment] })
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
for (const attachment of attachments) {
|
|
141
|
-
if (isLinkAttachment(attachment)) {
|
|
142
|
-
pushOrMergeLink(attachment)
|
|
143
|
-
continue
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const kind = getAttachmentMediaKind(attachment)
|
|
147
|
-
if (!kind) continue
|
|
148
|
-
|
|
149
|
-
pushOrMergeMedia(kind, attachment)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return segments
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function countAttachmentSegmentBubbles(
|
|
156
|
-
segments: AttachmentSegment[]
|
|
157
|
-
): number {
|
|
158
|
-
let count = 0
|
|
159
|
-
|
|
160
|
-
for (const segment of segments) {
|
|
161
|
-
count += segment.type === 'link' ? segment.attachments.length : 1
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return count
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function countMediaSegmentBubbles(segments: AttachmentSegment[]): number {
|
|
168
|
-
return segments.filter((segment) => segment.type === 'media').length
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function bubblePositionInRun(
|
|
172
|
-
index: number,
|
|
173
|
-
total: number,
|
|
174
|
-
streamPosition: BubbleGroupPosition
|
|
175
|
-
): BubbleGroupPosition {
|
|
176
|
-
if (total <= 1) return streamPosition
|
|
177
|
-
if (index === 0) return 'first'
|
|
178
|
-
if (index === total - 1) return 'end'
|
|
179
|
-
return 'middle'
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function imageSrcFromAttachment(attachment: Attachment): string {
|
|
183
|
-
return (
|
|
184
|
-
trimToUndefined((attachment as { image_url?: string }).image_url) ??
|
|
185
|
-
trimToUndefined(attachment.asset_url) ??
|
|
186
|
-
''
|
|
187
|
-
)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function messageAttachmentNamespaceForKind(kind: StreamMediaKind) {
|
|
191
|
-
switch (kind) {
|
|
192
|
-
case 'image':
|
|
193
|
-
return MessageAttachment.Image
|
|
194
|
-
case 'video':
|
|
195
|
-
return MessageAttachment.Video
|
|
196
|
-
case 'audio':
|
|
197
|
-
return MessageAttachment.Audio
|
|
198
|
-
case 'pdf':
|
|
199
|
-
return MessageAttachment.Pdf
|
|
200
|
-
default:
|
|
201
|
-
return MessageAttachment.File
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function buildMediaClusterRenderProps(
|
|
206
|
-
kind: StreamMediaKind,
|
|
207
|
-
attachments: Attachment[]
|
|
208
|
-
): Record<string, unknown> {
|
|
209
|
-
if (attachments.length > 1) {
|
|
210
|
-
switch (kind) {
|
|
211
|
-
case 'image':
|
|
212
|
-
return {
|
|
213
|
-
items: attachments.map((attachment) => ({
|
|
214
|
-
src: imageSrcFromAttachment(attachment),
|
|
215
|
-
alt: trimToUndefined((attachment as { title?: string }).title),
|
|
216
|
-
})),
|
|
217
|
-
}
|
|
218
|
-
case 'video':
|
|
219
|
-
return {
|
|
220
|
-
items: attachments.map((attachment) => ({
|
|
221
|
-
src: trimToUndefined(attachment.asset_url) ?? '',
|
|
222
|
-
poster: trimToUndefined(
|
|
223
|
-
(attachment as { thumb_url?: string }).thumb_url
|
|
224
|
-
),
|
|
225
|
-
mimeType: trimToUndefined(attachment.mime_type),
|
|
226
|
-
})),
|
|
227
|
-
}
|
|
228
|
-
case 'audio':
|
|
229
|
-
return {
|
|
230
|
-
items: attachments.map((attachment) => ({
|
|
231
|
-
src: trimToUndefined(attachment.asset_url) ?? '',
|
|
232
|
-
mimeType:
|
|
233
|
-
trimToUndefined(attachment.mime_type) ?? 'audio/mpeg',
|
|
234
|
-
filename: trimToUndefined(
|
|
235
|
-
(attachment as { title?: string }).title
|
|
236
|
-
),
|
|
237
|
-
})),
|
|
238
|
-
}
|
|
239
|
-
case 'pdf':
|
|
240
|
-
return {
|
|
241
|
-
items: attachments.map((attachment) => ({
|
|
242
|
-
src: trimToUndefined(attachment.asset_url) ?? '',
|
|
243
|
-
filename: trimToUndefined(
|
|
244
|
-
(attachment as { title?: string }).title
|
|
245
|
-
),
|
|
246
|
-
fileSize: (attachment as { file_size?: number }).file_size,
|
|
247
|
-
})),
|
|
248
|
-
}
|
|
249
|
-
case 'file':
|
|
250
|
-
return {
|
|
251
|
-
items: attachments.map((attachment) => ({
|
|
252
|
-
src: trimToUndefined(attachment.asset_url) ?? '',
|
|
253
|
-
filename: trimToUndefined(
|
|
254
|
-
(attachment as { title?: string }).title
|
|
255
|
-
),
|
|
256
|
-
fileSize: (attachment as { file_size?: number }).file_size,
|
|
257
|
-
mimeType:
|
|
258
|
-
trimToUndefined(attachment.mime_type) ??
|
|
259
|
-
'application/octet-stream',
|
|
260
|
-
})),
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const attachment = attachments[0]
|
|
266
|
-
if (!attachment) return {}
|
|
267
|
-
|
|
268
|
-
switch (kind) {
|
|
269
|
-
case 'image':
|
|
270
|
-
return {
|
|
271
|
-
src: imageSrcFromAttachment(attachment),
|
|
272
|
-
alt: trimToUndefined((attachment as { title?: string }).title),
|
|
273
|
-
}
|
|
274
|
-
case 'video':
|
|
275
|
-
return {
|
|
276
|
-
src: trimToUndefined(attachment.asset_url) ?? '',
|
|
277
|
-
poster: trimToUndefined((attachment as { thumb_url?: string }).thumb_url),
|
|
278
|
-
mimeType: trimToUndefined(attachment.mime_type),
|
|
279
|
-
}
|
|
280
|
-
case 'audio':
|
|
281
|
-
return {
|
|
282
|
-
src: trimToUndefined(attachment.asset_url) ?? '',
|
|
283
|
-
mimeType: trimToUndefined(attachment.mime_type) ?? 'audio/mpeg',
|
|
284
|
-
filename: trimToUndefined((attachment as { title?: string }).title),
|
|
285
|
-
}
|
|
286
|
-
case 'pdf':
|
|
287
|
-
return {
|
|
288
|
-
src: trimToUndefined(attachment.asset_url) ?? '',
|
|
289
|
-
filename: trimToUndefined((attachment as { title?: string }).title),
|
|
290
|
-
fileSize: (attachment as { file_size?: number }).file_size,
|
|
291
|
-
}
|
|
292
|
-
case 'file':
|
|
293
|
-
return {
|
|
294
|
-
src: trimToUndefined(attachment.asset_url) ?? '',
|
|
295
|
-
filename: trimToUndefined((attachment as { title?: string }).title),
|
|
296
|
-
fileSize: (attachment as { file_size?: number }).file_size,
|
|
297
|
-
mimeType:
|
|
298
|
-
trimToUndefined(attachment.mime_type) ?? 'application/octet-stream',
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function MediaClusterBubble({
|
|
304
|
-
groupPosition,
|
|
305
|
-
isMyMessage,
|
|
306
|
-
segment,
|
|
307
|
-
text,
|
|
308
|
-
}: {
|
|
309
|
-
groupPosition: BubbleGroupPosition
|
|
310
|
-
isMyMessage: boolean
|
|
311
|
-
segment: Extract<AttachmentSegment, { type: 'media' }>
|
|
312
|
-
text?: string
|
|
313
|
-
}) {
|
|
314
|
-
const TypeNamespace = messageAttachmentNamespaceForKind(segment.kind)
|
|
315
|
-
const props = {
|
|
316
|
-
...buildMediaClusterRenderProps(segment.kind, segment.attachments),
|
|
317
|
-
groupPosition,
|
|
318
|
-
...(text ? { text } : {}),
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (isMyMessage) {
|
|
322
|
-
return <TypeNamespace.Sent {...props} />
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return <TypeNamespace.Received {...props} />
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export interface StreamAttachmentMessageProps {
|
|
329
|
-
groupPosition: BubbleGroupPosition
|
|
330
|
-
isMyMessage: boolean
|
|
331
|
-
message: StreamThreadMessage
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const StreamAttachmentMessage: React.FC<StreamAttachmentMessageProps> = ({
|
|
335
|
-
groupPosition,
|
|
336
|
-
isMyMessage,
|
|
337
|
-
message,
|
|
338
|
-
}) => {
|
|
339
|
-
const segments = buildOrderedAttachmentSegments(message.attachments)
|
|
340
|
-
if (segments.length === 0) return null
|
|
341
|
-
|
|
342
|
-
const totalBubbles = countAttachmentSegmentBubbles(segments)
|
|
343
|
-
const totalMediaBubbles = countMediaSegmentBubbles(segments)
|
|
344
|
-
const rawCaption = message.text?.trim()
|
|
345
|
-
const linkSegment = segments.find((segment) => segment.type === 'link')
|
|
346
|
-
const primaryLinkUrl =
|
|
347
|
-
linkSegment?.type === 'link'
|
|
348
|
-
? linkCardPropsFromStreamAttachment(linkSegment.attachments[0] ?? {}).url
|
|
349
|
-
: undefined
|
|
350
|
-
const caption =
|
|
351
|
-
rawCaption &&
|
|
352
|
-
linkCaptionDuplicatesAttachmentUrl(rawCaption, primaryLinkUrl)
|
|
353
|
-
? undefined
|
|
354
|
-
: rawCaption
|
|
355
|
-
|
|
356
|
-
let bubbleIndex = 0
|
|
357
|
-
let mediaBubbleIndex = 0
|
|
358
|
-
const rows: React.ReactNode[] = []
|
|
359
|
-
|
|
360
|
-
const pushLinkRow = (attachment: Attachment, key: string) => {
|
|
361
|
-
const isLastBubble = bubbleIndex === totalBubbles - 1
|
|
362
|
-
bubbleIndex += 1
|
|
363
|
-
|
|
364
|
-
const Card = isMyMessage ? LinkAttachment.Sent : LinkAttachment.Received
|
|
365
|
-
|
|
366
|
-
rows.push(
|
|
367
|
-
<React.Fragment key={key}>
|
|
368
|
-
<Card {...linkCardPropsFromStreamAttachment(attachment)} />
|
|
369
|
-
{isLastBubble && caption ? (
|
|
370
|
-
<div className={isMyMessage ? SENT_CAPTION_CLASS : RECEIVED_CAPTION_CLASS}>
|
|
371
|
-
{caption}
|
|
372
|
-
</div>
|
|
373
|
-
) : null}
|
|
374
|
-
</React.Fragment>
|
|
375
|
-
)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const pushMediaCluster = (
|
|
379
|
-
segment: Extract<AttachmentSegment, { type: 'media' }>,
|
|
380
|
-
key: string
|
|
381
|
-
) => {
|
|
382
|
-
const isLastBubble = bubbleIndex === totalBubbles - 1
|
|
383
|
-
const position = bubblePositionInRun(
|
|
384
|
-
mediaBubbleIndex,
|
|
385
|
-
totalMediaBubbles,
|
|
386
|
-
groupPosition
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
bubbleIndex += 1
|
|
390
|
-
mediaBubbleIndex += 1
|
|
391
|
-
|
|
392
|
-
rows.push(
|
|
393
|
-
<MediaClusterBubble
|
|
394
|
-
key={key}
|
|
395
|
-
groupPosition={position}
|
|
396
|
-
isMyMessage={isMyMessage}
|
|
397
|
-
segment={segment}
|
|
398
|
-
text={isLastBubble ? caption : undefined}
|
|
399
|
-
/>
|
|
400
|
-
)
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
segments.forEach((segment, segmentIndex) => {
|
|
404
|
-
if (segment.type === 'link') {
|
|
405
|
-
segment.attachments.forEach((attachment, attachmentIndex) => {
|
|
406
|
-
pushLinkRow(attachment, `link-${segmentIndex}-${attachmentIndex}`)
|
|
407
|
-
})
|
|
408
|
-
return
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
pushMediaCluster(segment, `media-${segmentIndex}`)
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
return <>{rows}</>
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
export default StreamAttachmentMessage
|