@linktr.ee/messaging-react 3.37.0 → 3.37.1
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/README.md +4 -4
- package/dist/Card-415OfRhw.cjs.map +1 -1
- package/dist/{Card-D4PPD5qa.js → Card-DVMzYquF.js} +11 -8
- package/dist/Card-DVMzYquF.js.map +1 -0
- package/dist/{Card-qS17qnN0.js → Card-DaJtDL7m.js} +15 -6
- package/dist/Card-DaJtDL7m.js.map +1 -0
- package/dist/Card-Dxk_Ggxw.cjs.map +1 -1
- package/dist/LoadingDots-Ct3WmVlT.js.map +1 -1
- package/dist/LoadingDots-D9GGphgM.cjs.map +1 -1
- package/dist/{index-DGmVyxHy.js → index-BOBIiAuD.js} +87 -62
- package/dist/index-BOBIiAuD.js.map +1 -0
- package/dist/index-D7A8U8GQ.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -2
- package/src/components/ActionButton/ActionButton.stories.tsx +0 -2
- package/src/components/ActionButton/ActionButton.test.tsx +60 -55
- package/src/components/ActionButton/index.tsx +13 -14
- package/src/components/AttachmentCard/AttachmentCard.stories.tsx +1 -3
- package/src/components/AttachmentCard/MediaPlayer.tsx +9 -7
- package/src/components/AttachmentCard/index.tsx +6 -2
- package/src/components/Avatar/Avatar.stories.tsx +17 -5
- package/src/components/Avatar/index.tsx +13 -23
- package/src/components/ChannelView.test.tsx +6 -10
- package/src/components/CustomMessage/CustomMessage.stories.tsx +7 -2
- package/src/components/CustomMessage/CustomMessage.test.tsx +5 -3
- package/src/components/CustomMessage/LockedAttachment/LockedAttachment.test.tsx +5 -4
- package/src/components/CustomMessage/LockedAttachment/components/Text/Card.tsx +2 -4
- package/src/components/CustomMessage/LockedAttachment/components/_shared/CardBody.tsx +9 -6
- package/src/components/CustomMessage/LockedAttachment/components/_shared/CardThumbnail.tsx +1 -3
- package/src/components/CustomMessage/LockedAttachment/components/_shared/GalleryThumbnail.tsx +1 -4
- package/src/components/CustomMessage/LockedAttachment/components/_shared/PurchaseStatusBadge.tsx +0 -1
- package/src/components/CustomMessage/LockedAttachment/components/_shared/SingleThumbnail.tsx +1 -4
- package/src/components/CustomMessage/LockedAttachment/components/_shared/SkeletonTextLines.tsx +7 -2
- package/src/components/CustomMessage/LockedAttachment/types.ts +4 -3
- package/src/components/CustomMessage/MessageAttachmentConversations.stories.tsx +89 -86
- package/src/components/CustomMessage/MessageVoteButtons.tsx +4 -1
- package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +13 -10
- package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +1 -4
- package/src/components/CustomMessage/StreamAttachmentMessage.stories.tsx +4 -4
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +6 -7
- package/src/components/CustomMessage/context.tsx +7 -2
- package/src/components/CustomMessage/index.tsx +1 -4
- package/src/components/CustomMessageInput/CustomMessageInput.stories.tsx +33 -11
- package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +3 -3
- package/src/components/CustomSystemMessage/CustomSystemMessage.test.tsx +6 -9
- package/src/components/CustomTypingIndicator/CustomTypingIndicator.stories.tsx +4 -1
- package/src/components/IconButton/index.tsx +21 -17
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +4 -1
- package/src/components/LinkAttachment/components/_shared/CardCta.tsx +4 -1
- package/src/components/Loading/Loading.stories.tsx +1 -4
- package/src/components/MediaMessage/MediaMessage.stories.tsx +6 -18
- package/src/components/MediaMessage/MediaMessage.test.tsx +30 -16
- package/src/components/MediaMessage/index.tsx +51 -14
- package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +4 -2
- package/src/components/MessageAttachment/File/FileAttachment.stories.tsx +3 -1
- package/src/components/MessageAttachment/Image/ImageAttachment.stories.tsx +15 -4
- package/src/components/MessageAttachment/Image/index.tsx +6 -12
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +59 -27
- package/src/components/MessageAttachment/Pdf/PdfAttachment.stories.tsx +3 -1
- package/src/components/MessageAttachment/Pdf/index.tsx +2 -3
- package/src/components/MessageAttachment/Video/VideoAttachment.stories.tsx +11 -3
- package/src/components/MessageAttachment/Video/index.tsx +9 -14
- package/src/components/MessageAttachment/_shared/CompactDocumentRow.tsx +1 -3
- package/src/components/MessageAttachment/_shared/ImageViewer.tsx +1 -3
- package/src/components/MessageAttachment/_shared/MediaStackGrid.tsx +5 -1
- package/src/components/MessageAttachment/_shared/PdfViewer.tsx +1 -3
- package/src/components/MessageAttachment/_shared/VideoViewer.tsx +1 -3
- package/src/components/MessageAttachment/_shared/fileMeta.test.ts +4 -6
- package/src/components/MessageAttachment/stories/StoryTable.tsx +1 -3
- package/src/components/MessageBubble/MessageBubble.stories.tsx +5 -1
- package/src/components/MessageBubble/MessageBubble.test.tsx +1 -3
- package/src/components/MessageBubble/components/_shared/bubbleTail.tsx +2 -1
- package/src/components/MessagingShell/LoadingState.stories.tsx +0 -1
- package/src/components/RichCard/RichCard.stories.tsx +14 -3
- package/src/components/RichCard/RichCard.test.tsx +16 -4
- package/src/components/RichCard/RichCardBody.tsx +11 -3
- package/src/components/RichCard/RichCardCard.tsx +1 -3
- package/src/components/RichCard/RichCardImages.tsx +2 -4
- package/src/components/SearchInput/SearchInput.stories.tsx +0 -1
- package/src/components/SearchInput/SearchInput.test.tsx +94 -65
- package/src/hooks/useChannelStar.ts +3 -1
- package/src/hooks/useMessaging.ts +4 -4
- package/src/stories/decorators/storyUser.tsx +2 -2
- package/src/stories/mocks.tsx +4 -1
- package/src/styles.css +23 -8
- package/src/types.ts +1 -2
- package/src/utils/cdnImageUrl.test.ts +3 -1
- package/dist/Card-D4PPD5qa.js.map +0 -1
- package/dist/Card-qS17qnN0.js.map +0 -1
- package/dist/index-DGmVyxHy.js.map +0 -1
|
@@ -13,13 +13,7 @@ vi.mock('../Avatar', () => ({
|
|
|
13
13
|
}))
|
|
14
14
|
|
|
15
15
|
vi.mock('../AttachmentCard/MediaPlayer', () => ({
|
|
16
|
-
default: ({
|
|
17
|
-
source,
|
|
18
|
-
mimeType,
|
|
19
|
-
}: {
|
|
20
|
-
source: string
|
|
21
|
-
mimeType: string
|
|
22
|
-
}) => (
|
|
16
|
+
default: ({ source, mimeType }: { source: string; mimeType: string }) => (
|
|
23
17
|
<div
|
|
24
18
|
role="button"
|
|
25
19
|
tabIndex={0}
|
|
@@ -82,7 +76,10 @@ describe('MediaMessage', () => {
|
|
|
82
76
|
|
|
83
77
|
const player = screen.getByTestId('media-player')
|
|
84
78
|
expect(player).toBeInTheDocument()
|
|
85
|
-
expect(player).toHaveAttribute(
|
|
79
|
+
expect(player).toHaveAttribute(
|
|
80
|
+
'data-source',
|
|
81
|
+
'https://cdn.example.com/clip.mp4'
|
|
82
|
+
)
|
|
86
83
|
})
|
|
87
84
|
|
|
88
85
|
it('renders MediaPlayer for an audio attachment', () => {
|
|
@@ -185,7 +182,10 @@ describe('MediaMessage', () => {
|
|
|
185
182
|
)
|
|
186
183
|
|
|
187
184
|
expect(screen.getByTestId('avatar')).toBeInTheDocument()
|
|
188
|
-
expect(screen.getByTestId('avatar')).toHaveAttribute(
|
|
185
|
+
expect(screen.getByTestId('avatar')).toHaveAttribute(
|
|
186
|
+
'data-user-id',
|
|
187
|
+
'user-1'
|
|
188
|
+
)
|
|
189
189
|
})
|
|
190
190
|
|
|
191
191
|
it('does not render Avatar for own messages', () => {
|
|
@@ -308,7 +308,9 @@ describe('MediaMessage', () => {
|
|
|
308
308
|
/>
|
|
309
309
|
)
|
|
310
310
|
|
|
311
|
-
expect(
|
|
311
|
+
expect(
|
|
312
|
+
screen.queryByRole('button', { name: 'Download' })
|
|
313
|
+
).not.toBeInTheDocument()
|
|
312
314
|
})
|
|
313
315
|
|
|
314
316
|
it('shows Download action for received audio attachment', () => {
|
|
@@ -390,8 +392,12 @@ describe('MediaMessage', () => {
|
|
|
390
392
|
/>
|
|
391
393
|
)
|
|
392
394
|
|
|
393
|
-
expect(
|
|
394
|
-
|
|
395
|
+
expect(
|
|
396
|
+
screen.queryByRole('button', { name: 'Download' })
|
|
397
|
+
).not.toBeInTheDocument()
|
|
398
|
+
expect(
|
|
399
|
+
screen.queryByRole('button', { name: 'View full screen' })
|
|
400
|
+
).not.toBeInTheDocument()
|
|
395
401
|
})
|
|
396
402
|
|
|
397
403
|
it('MediaMessage.Visitor renders card without chat shell', () => {
|
|
@@ -410,7 +416,9 @@ describe('MediaMessage', () => {
|
|
|
410
416
|
/>
|
|
411
417
|
)
|
|
412
418
|
|
|
413
|
-
expect(
|
|
419
|
+
expect(
|
|
420
|
+
container.querySelector('.str-chat__message')
|
|
421
|
+
).not.toBeInTheDocument()
|
|
414
422
|
expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument()
|
|
415
423
|
})
|
|
416
424
|
|
|
@@ -429,8 +437,12 @@ describe('MediaMessage', () => {
|
|
|
429
437
|
/>
|
|
430
438
|
)
|
|
431
439
|
|
|
432
|
-
expect(
|
|
433
|
-
|
|
440
|
+
expect(
|
|
441
|
+
container.querySelector('.str-chat__message')
|
|
442
|
+
).not.toBeInTheDocument()
|
|
443
|
+
expect(
|
|
444
|
+
screen.queryByRole('button', { name: 'Download' })
|
|
445
|
+
).not.toBeInTheDocument()
|
|
434
446
|
})
|
|
435
447
|
|
|
436
448
|
it('resolveLinkAttachment ignores empty-string og_scrape_url without asset_url', () => {
|
|
@@ -448,7 +460,9 @@ describe('MediaMessage', () => {
|
|
|
448
460
|
})
|
|
449
461
|
|
|
450
462
|
it('Download button triggers fetch and uses fallback on failure', async () => {
|
|
451
|
-
const fetchSpy = vi
|
|
463
|
+
const fetchSpy = vi
|
|
464
|
+
.spyOn(global, 'fetch')
|
|
465
|
+
.mockRejectedValue(new Error('fail'))
|
|
452
466
|
const openSpy = vi.spyOn(window, 'open').mockReturnValue({
|
|
453
467
|
location: { href: '' },
|
|
454
468
|
close: vi.fn(),
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CircleNotchIcon,
|
|
3
|
+
DownloadSimpleIcon,
|
|
4
|
+
LinkIcon,
|
|
5
|
+
} from '@phosphor-icons/react'
|
|
2
6
|
import React, { useState } from 'react'
|
|
3
7
|
import type { Attachment, LocalMessage } from 'stream-chat'
|
|
4
8
|
|
|
@@ -69,15 +73,23 @@ const LinkCard: React.FC<{
|
|
|
69
73
|
</div>
|
|
70
74
|
<div className="px-3 pb-3">
|
|
71
75
|
{title && (
|
|
72
|
-
<p
|
|
76
|
+
<p
|
|
77
|
+
className={`truncate text-[14px] font-medium leading-5 ${linkPrimaryText(isMyMessage)}`}
|
|
78
|
+
>
|
|
73
79
|
{title}
|
|
74
80
|
</p>
|
|
75
81
|
)}
|
|
76
82
|
{text && (
|
|
77
|
-
<p
|
|
83
|
+
<p
|
|
84
|
+
className={`truncate text-[12px] leading-4 ${linkSecondaryText(isMyMessage)}`}
|
|
85
|
+
>
|
|
86
|
+
{text}
|
|
87
|
+
</p>
|
|
78
88
|
)}
|
|
79
89
|
{url && (
|
|
80
|
-
<p
|
|
90
|
+
<p
|
|
91
|
+
className={`mt-1 truncate text-[12px] leading-4 ${linkTertiaryText(isMyMessage)}`}
|
|
92
|
+
>
|
|
81
93
|
{url}
|
|
82
94
|
</p>
|
|
83
95
|
)}
|
|
@@ -87,7 +99,12 @@ const LinkCard: React.FC<{
|
|
|
87
99
|
|
|
88
100
|
if (url) {
|
|
89
101
|
return (
|
|
90
|
-
<a
|
|
102
|
+
<a
|
|
103
|
+
href={url}
|
|
104
|
+
target="_blank"
|
|
105
|
+
rel="noopener noreferrer"
|
|
106
|
+
className="block no-underline"
|
|
107
|
+
>
|
|
91
108
|
{body}
|
|
92
109
|
</a>
|
|
93
110
|
)
|
|
@@ -108,8 +125,11 @@ export function resolveLinkAttachment(
|
|
|
108
125
|
|
|
109
126
|
async function triggerDownload(url: string, filename?: string): Promise<void> {
|
|
110
127
|
let name: string
|
|
111
|
-
try {
|
|
112
|
-
|
|
128
|
+
try {
|
|
129
|
+
name = filename ?? new URL(url).pathname.split('/').pop() ?? 'download'
|
|
130
|
+
} catch {
|
|
131
|
+
name = filename ?? 'download'
|
|
132
|
+
}
|
|
113
133
|
const res = await fetch(url, { mode: 'cors' })
|
|
114
134
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
115
135
|
const blob = await res.blob()
|
|
@@ -135,8 +155,12 @@ const DownloadAction: React.FC<{ url: string; filename?: string }> = ({
|
|
|
135
155
|
const fallback = window.open('', '_blank', 'noopener,noreferrer')
|
|
136
156
|
setBusy(true)
|
|
137
157
|
triggerDownload(url, filename)
|
|
138
|
-
.then(() => {
|
|
139
|
-
|
|
158
|
+
.then(() => {
|
|
159
|
+
fallback?.close()
|
|
160
|
+
})
|
|
161
|
+
.catch(() => {
|
|
162
|
+
if (fallback) fallback.location.href = url
|
|
163
|
+
})
|
|
140
164
|
.finally(() => setBusy(false))
|
|
141
165
|
}
|
|
142
166
|
|
|
@@ -148,7 +172,10 @@ const DownloadAction: React.FC<{ url: string; filename?: string }> = ({
|
|
|
148
172
|
className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-[#121110] px-4 text-sm font-medium leading-none text-white hover:bg-[#2a2928] disabled:opacity-70"
|
|
149
173
|
>
|
|
150
174
|
{busy ? (
|
|
151
|
-
<CircleNotchIcon
|
|
175
|
+
<CircleNotchIcon
|
|
176
|
+
className="size-4 animate-spin text-white"
|
|
177
|
+
weight="bold"
|
|
178
|
+
/>
|
|
152
179
|
) : (
|
|
153
180
|
<React.Fragment>
|
|
154
181
|
<DownloadSimpleIcon className="size-4 text-white" weight="bold" />
|
|
@@ -205,8 +232,10 @@ export function resolveMediaFromMessage(
|
|
|
205
232
|
: 'application/octet-stream')
|
|
206
233
|
|
|
207
234
|
const title = (activeAttachment as { title?: string } | undefined)?.title
|
|
208
|
-
const fileSize = (activeAttachment as { file_size?: number } | undefined)
|
|
209
|
-
|
|
235
|
+
const fileSize = (activeAttachment as { file_size?: number } | undefined)
|
|
236
|
+
?.file_size
|
|
237
|
+
const thumbnailUrl = (videoAttachment as { thumb_url?: string } | undefined)
|
|
238
|
+
?.thumb_url
|
|
210
239
|
|
|
211
240
|
return {
|
|
212
241
|
resolvedUrl,
|
|
@@ -326,11 +355,19 @@ const MediaMessageRoot: React.FC<MediaMessageProps> = ({
|
|
|
326
355
|
// tail tinted from the text-bubble surface (#f2f1ef) hangs off
|
|
327
356
|
// a white card and reads as a detached grey nub.
|
|
328
357
|
data-attachment-bubble="true"
|
|
329
|
-
style={{
|
|
358
|
+
style={{
|
|
359
|
+
padding: 0,
|
|
360
|
+
borderRadius: 0,
|
|
361
|
+
overflow: 'visible',
|
|
362
|
+
background: 'transparent',
|
|
363
|
+
}}
|
|
330
364
|
>
|
|
331
365
|
{linkAttachment ? (
|
|
332
366
|
<div className={linkCardShellClass(isMyMessage)}>
|
|
333
|
-
<LinkCard
|
|
367
|
+
<LinkCard
|
|
368
|
+
attachment={linkAttachment}
|
|
369
|
+
isMyMessage={isMyMessage}
|
|
370
|
+
/>
|
|
334
371
|
</div>
|
|
335
372
|
) : isMyMessage ? (
|
|
336
373
|
<Creator {...resolved!} />
|
|
@@ -145,7 +145,8 @@ export const Stacked: StoryFn = () => (
|
|
|
145
145
|
mimeType="audio/mpeg"
|
|
146
146
|
filename="Morning meditation.mp3"
|
|
147
147
|
onDismiss={handleDismiss}
|
|
148
|
-
/>
|
|
148
|
+
/>
|
|
149
|
+
}
|
|
149
150
|
sent={<AudioAttachment.Sent items={STACK_AUDIO.slice(0, 2)} />}
|
|
150
151
|
received={<AudioAttachment.Received items={STACK_AUDIO.slice(0, 2)} />}
|
|
151
152
|
/>
|
|
@@ -157,7 +158,8 @@ export const Stacked: StoryFn = () => (
|
|
|
157
158
|
mimeType="audio/mpeg"
|
|
158
159
|
filename="Morning meditation.mp3"
|
|
159
160
|
onDismiss={handleDismiss}
|
|
160
|
-
/>
|
|
161
|
+
/>
|
|
162
|
+
}
|
|
161
163
|
sent={<AudioAttachment.Sent items={STACK_AUDIO} />}
|
|
162
164
|
received={<AudioAttachment.Received items={STACK_AUDIO} />}
|
|
163
165
|
/>
|
|
@@ -275,7 +275,9 @@ export const Grouping: StoryFn = () => (
|
|
|
275
275
|
<StoryGrid>
|
|
276
276
|
<StoryRow
|
|
277
277
|
label="single"
|
|
278
|
-
sent={
|
|
278
|
+
sent={
|
|
279
|
+
<FileAttachment.Sent {...STACK_FILES[0]} groupPosition="single" />
|
|
280
|
+
}
|
|
279
281
|
received={
|
|
280
282
|
<FileAttachment.Received {...STACK_FILES[0]} groupPosition="single" />
|
|
281
283
|
}
|
|
@@ -245,7 +245,10 @@ export const Stacked: StoryFn = () => (
|
|
|
245
245
|
/>
|
|
246
246
|
}
|
|
247
247
|
sent={
|
|
248
|
-
<ImageAttachment.Sent
|
|
248
|
+
<ImageAttachment.Sent
|
|
249
|
+
items={STACK_IMAGES.slice(0, 2)}
|
|
250
|
+
filename="album"
|
|
251
|
+
/>
|
|
249
252
|
}
|
|
250
253
|
received={
|
|
251
254
|
<ImageAttachment.Received
|
|
@@ -265,7 +268,10 @@ export const Stacked: StoryFn = () => (
|
|
|
265
268
|
/>
|
|
266
269
|
}
|
|
267
270
|
sent={
|
|
268
|
-
<ImageAttachment.Sent
|
|
271
|
+
<ImageAttachment.Sent
|
|
272
|
+
items={STACK_IMAGES.slice(0, 3)}
|
|
273
|
+
filename="album"
|
|
274
|
+
/>
|
|
269
275
|
}
|
|
270
276
|
received={
|
|
271
277
|
<ImageAttachment.Received
|
|
@@ -285,7 +291,10 @@ export const Stacked: StoryFn = () => (
|
|
|
285
291
|
/>
|
|
286
292
|
}
|
|
287
293
|
sent={
|
|
288
|
-
<ImageAttachment.Sent
|
|
294
|
+
<ImageAttachment.Sent
|
|
295
|
+
items={STACK_IMAGES.slice(0, 4)}
|
|
296
|
+
filename="album"
|
|
297
|
+
/>
|
|
289
298
|
}
|
|
290
299
|
received={
|
|
291
300
|
<ImageAttachment.Received
|
|
@@ -297,7 +306,9 @@ export const Stacked: StoryFn = () => (
|
|
|
297
306
|
<StoryRow
|
|
298
307
|
label="6 photos (+overflow)"
|
|
299
308
|
sent={<ImageAttachment.Sent items={STACK_IMAGES} filename="album" />}
|
|
300
|
-
received={
|
|
309
|
+
received={
|
|
310
|
+
<ImageAttachment.Received items={STACK_IMAGES} filename="album" />
|
|
311
|
+
}
|
|
301
312
|
/>
|
|
302
313
|
</StoryGrid>
|
|
303
314
|
</StoryPage>
|
|
@@ -3,12 +3,8 @@ import React from 'react'
|
|
|
3
3
|
import { optimizeMessagingAttachmentUrl } from '../../../utils/cdnImageUrl'
|
|
4
4
|
import Bubble from '../_shared/Bubble'
|
|
5
5
|
import DismissButton from '../_shared/DismissButton'
|
|
6
|
-
import ImageViewer, {
|
|
7
|
-
|
|
8
|
-
} from '../_shared/ImageViewer'
|
|
9
|
-
import MediaStackGrid, {
|
|
10
|
-
type MediaStackTile,
|
|
11
|
-
} from '../_shared/MediaStackGrid'
|
|
6
|
+
import ImageViewer, { type ImageViewerItem } from '../_shared/ImageViewer'
|
|
7
|
+
import MediaStackGrid, { type MediaStackTile } from '../_shared/MediaStackGrid'
|
|
12
8
|
import { useViewer } from '../_shared/useViewer'
|
|
13
9
|
import {
|
|
14
10
|
bubbleVariantForState,
|
|
@@ -150,9 +146,8 @@ const ImageComposerInner: React.FC<{
|
|
|
150
146
|
onClick?: (index: number) => boolean | void
|
|
151
147
|
onDismiss?: () => void
|
|
152
148
|
}> = ({ src, alt, filename, loading = 'lazy', onClick, onDismiss }) => {
|
|
153
|
-
const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
|
|
154
|
-
onClick
|
|
155
|
-
)
|
|
149
|
+
const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
|
|
150
|
+
useViewer(onClick)
|
|
156
151
|
|
|
157
152
|
return (
|
|
158
153
|
<div className="relative w-fit">
|
|
@@ -268,9 +263,8 @@ const ImageBubbleRow: React.FC<InternalImageRowProps> = ({
|
|
|
268
263
|
}) => {
|
|
269
264
|
const resolvedItems = resolveItems({ src, alt, items })
|
|
270
265
|
const variant = bubbleVariantForState(state)
|
|
271
|
-
const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
|
|
272
|
-
onClick
|
|
273
|
-
)
|
|
266
|
+
const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
|
|
267
|
+
useViewer(onClick)
|
|
274
268
|
const isSingle = resolvedItems.length === 1
|
|
275
269
|
const single = useSingleImageRatio(isSingle ? resolvedItems[0] : undefined)
|
|
276
270
|
|
|
@@ -307,9 +307,10 @@ describe('MessageAttachment.Image single-image aspect ratio', () => {
|
|
|
307
307
|
renderWithProviders(
|
|
308
308
|
<MessageAttachment.Image.Sent src="https://cdn.example.com/unsized.jpg" />
|
|
309
309
|
)
|
|
310
|
-
expect(
|
|
311
|
-
|
|
312
|
-
|
|
310
|
+
expect(Number(mediaBox('Open image 1 of 1').style.aspectRatio)).toBeCloseTo(
|
|
311
|
+
326 / 246,
|
|
312
|
+
4
|
|
313
|
+
)
|
|
313
314
|
|
|
314
315
|
// Scoped to the tile: the always-mounted `ImageViewer` renders a
|
|
315
316
|
// second `<img>` for the same source.
|
|
@@ -326,9 +327,10 @@ describe('MessageAttachment.Image single-image aspect ratio', () => {
|
|
|
326
327
|
})
|
|
327
328
|
fireEvent.load(img)
|
|
328
329
|
|
|
329
|
-
expect(
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
expect(Number(mediaBox('Open image 1 of 1').style.aspectRatio)).toBeCloseTo(
|
|
331
|
+
0.5625,
|
|
332
|
+
4
|
|
333
|
+
)
|
|
332
334
|
})
|
|
333
335
|
|
|
334
336
|
it('leaves a stacked grid on its fixed aspect', () => {
|
|
@@ -575,9 +577,7 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
575
577
|
expect(() =>
|
|
576
578
|
rerender(
|
|
577
579
|
<MessageAttachment.Pdf.Received
|
|
578
|
-
items={[
|
|
579
|
-
{ src: 'https://cdn.example.com/a.pdf', filename: 'a.pdf' },
|
|
580
|
-
]}
|
|
580
|
+
items={[{ src: 'https://cdn.example.com/a.pdf', filename: 'a.pdf' }]}
|
|
581
581
|
/>
|
|
582
582
|
)
|
|
583
583
|
).not.toThrow()
|
|
@@ -731,9 +731,7 @@ describe('MessageAttachment.Audio', () => {
|
|
|
731
731
|
expect(screen.queryByText('song.mp3')).toBeNull()
|
|
732
732
|
// Native player handles its own download in the kebab menu, so
|
|
733
733
|
// no extra Download button should be rendered.
|
|
734
|
-
expect(
|
|
735
|
-
screen.queryByRole('button', { name: /download/i })
|
|
736
|
-
).toBeNull()
|
|
734
|
+
expect(screen.queryByRole('button', { name: /download/i })).toBeNull()
|
|
737
735
|
})
|
|
738
736
|
|
|
739
737
|
it('shows an inline dismiss button next to the player on Composer state', () => {
|
|
@@ -885,7 +883,11 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
885
883
|
renderWithProviders(
|
|
886
884
|
<MessageAttachment.Image.Sent
|
|
887
885
|
items={[
|
|
888
|
-
{
|
|
886
|
+
{
|
|
887
|
+
src: 'https://cdn.example.com/a.jpg',
|
|
888
|
+
alt: 'A',
|
|
889
|
+
loading: 'eager',
|
|
890
|
+
},
|
|
889
891
|
{ src: 'https://cdn.example.com/b.jpg', alt: 'B' },
|
|
890
892
|
]}
|
|
891
893
|
/>
|
|
@@ -927,9 +929,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
927
929
|
mimeType="video/mp4"
|
|
928
930
|
/>
|
|
929
931
|
)
|
|
930
|
-
const poster = screen
|
|
931
|
-
.getByTestId('video-attachment')
|
|
932
|
-
.querySelector('img')
|
|
932
|
+
const poster = screen.getByTestId('video-attachment').querySelector('img')
|
|
933
933
|
expect(poster?.getAttribute('src')).toBe(
|
|
934
934
|
'https://cdn.example.com/poster.jpg'
|
|
935
935
|
)
|
|
@@ -947,9 +947,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
947
947
|
/>
|
|
948
948
|
)
|
|
949
949
|
fireEvent.click(screen.getByLabelText('Play video 1 of 1'))
|
|
950
|
-
const video = screen
|
|
951
|
-
.getByTestId('video-viewer')
|
|
952
|
-
.querySelector('video')
|
|
950
|
+
const video = screen.getByTestId('video-viewer').querySelector('video')
|
|
953
951
|
expect(video?.getAttribute('preload')).toBe('metadata')
|
|
954
952
|
})
|
|
955
953
|
})
|
|
@@ -1011,16 +1009,45 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1011
1009
|
|
|
1012
1010
|
describe('bubbleGroupPositionFromStream', () => {
|
|
1013
1011
|
it.each([
|
|
1014
|
-
{
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1012
|
+
{
|
|
1013
|
+
groupedByUser: false,
|
|
1014
|
+
firstOfGroup: true,
|
|
1015
|
+
endOfGroup: true,
|
|
1016
|
+
expected: 'single',
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
groupedByUser: true,
|
|
1020
|
+
firstOfGroup: true,
|
|
1021
|
+
endOfGroup: true,
|
|
1022
|
+
expected: 'single',
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
groupedByUser: true,
|
|
1026
|
+
firstOfGroup: true,
|
|
1027
|
+
endOfGroup: false,
|
|
1028
|
+
expected: 'first',
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
groupedByUser: true,
|
|
1032
|
+
firstOfGroup: false,
|
|
1033
|
+
endOfGroup: true,
|
|
1034
|
+
expected: 'end',
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
groupedByUser: true,
|
|
1038
|
+
firstOfGroup: false,
|
|
1039
|
+
endOfGroup: false,
|
|
1040
|
+
expected: 'middle',
|
|
1041
|
+
},
|
|
1019
1042
|
])(
|
|
1020
1043
|
'maps groupedByUser=$groupedByUser firstOfGroup=$firstOfGroup endOfGroup=$endOfGroup → $expected',
|
|
1021
1044
|
({ groupedByUser, firstOfGroup, endOfGroup, expected }) => {
|
|
1022
1045
|
expect(
|
|
1023
|
-
bubbleGroupPositionFromStream({
|
|
1046
|
+
bubbleGroupPositionFromStream({
|
|
1047
|
+
groupedByUser,
|
|
1048
|
+
firstOfGroup,
|
|
1049
|
+
endOfGroup,
|
|
1050
|
+
})
|
|
1024
1051
|
).toBe(expected)
|
|
1025
1052
|
}
|
|
1026
1053
|
)
|
|
@@ -1050,10 +1077,15 @@ describe('MessageAttachment.Bubble grouping', () => {
|
|
|
1050
1077
|
'serializes groupPosition="%s" on the bubble',
|
|
1051
1078
|
(groupPosition) => {
|
|
1052
1079
|
renderWithProviders(
|
|
1053
|
-
<MessageAttachment.File.Sent
|
|
1080
|
+
<MessageAttachment.File.Sent
|
|
1081
|
+
{...FILE_PROPS}
|
|
1082
|
+
groupPosition={groupPosition}
|
|
1083
|
+
/>
|
|
1054
1084
|
)
|
|
1055
1085
|
expect(
|
|
1056
|
-
screen
|
|
1086
|
+
screen
|
|
1087
|
+
.getByTestId('file-attachment')
|
|
1088
|
+
.getAttribute('data-group-position')
|
|
1057
1089
|
).toBe(groupPosition)
|
|
1058
1090
|
}
|
|
1059
1091
|
)
|
|
@@ -102,7 +102,9 @@ export const Single: StoryFn = () => (
|
|
|
102
102
|
/>
|
|
103
103
|
}
|
|
104
104
|
sent={<PdfAttachment.Sent src={PDF_SRC} filename="contract.pdf" />}
|
|
105
|
-
received={
|
|
105
|
+
received={
|
|
106
|
+
<PdfAttachment.Received src={PDF_SRC} filename="contract.pdf" />
|
|
107
|
+
}
|
|
106
108
|
/>
|
|
107
109
|
</StoryGrid>
|
|
108
110
|
</StoryPage>
|
|
@@ -119,9 +119,8 @@ const PdfAttachmentRow: React.FC<InternalPdfRowProps> = ({
|
|
|
119
119
|
}) => {
|
|
120
120
|
const variant = bubbleVariantForState(state)
|
|
121
121
|
const resolvedItems = resolveItems({ src, filename, fileSize, title, items })
|
|
122
|
-
const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
|
|
123
|
-
onClick
|
|
124
|
-
)
|
|
122
|
+
const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
|
|
123
|
+
useViewer(onClick)
|
|
125
124
|
const showDismiss = state === 'composer' && !!onDismiss
|
|
126
125
|
|
|
127
126
|
if (resolvedItems.length === 0) {
|
|
@@ -183,7 +183,10 @@ export const Stacked: StoryFn = () => (
|
|
|
183
183
|
/>
|
|
184
184
|
}
|
|
185
185
|
sent={
|
|
186
|
-
<VideoAttachment.Sent
|
|
186
|
+
<VideoAttachment.Sent
|
|
187
|
+
items={STACK_VIDEOS.slice(0, 2)}
|
|
188
|
+
filename="clips"
|
|
189
|
+
/>
|
|
187
190
|
}
|
|
188
191
|
received={
|
|
189
192
|
<VideoAttachment.Received
|
|
@@ -204,7 +207,10 @@ export const Stacked: StoryFn = () => (
|
|
|
204
207
|
/>
|
|
205
208
|
}
|
|
206
209
|
sent={
|
|
207
|
-
<VideoAttachment.Sent
|
|
210
|
+
<VideoAttachment.Sent
|
|
211
|
+
items={STACK_VIDEOS.slice(0, 3)}
|
|
212
|
+
filename="clips"
|
|
213
|
+
/>
|
|
208
214
|
}
|
|
209
215
|
received={
|
|
210
216
|
<VideoAttachment.Received
|
|
@@ -225,7 +231,9 @@ export const Stacked: StoryFn = () => (
|
|
|
225
231
|
/>
|
|
226
232
|
}
|
|
227
233
|
sent={<VideoAttachment.Sent items={STACK_VIDEOS} filename="clips" />}
|
|
228
|
-
received={
|
|
234
|
+
received={
|
|
235
|
+
<VideoAttachment.Received items={STACK_VIDEOS} filename="clips" />
|
|
236
|
+
}
|
|
229
237
|
/>
|
|
230
238
|
</StoryGrid>
|
|
231
239
|
</StoryPage>
|
|
@@ -3,9 +3,7 @@ import React from 'react'
|
|
|
3
3
|
|
|
4
4
|
import Bubble from '../_shared/Bubble'
|
|
5
5
|
import DismissButton from '../_shared/DismissButton'
|
|
6
|
-
import MediaStackGrid, {
|
|
7
|
-
type MediaStackTile,
|
|
8
|
-
} from '../_shared/MediaStackGrid'
|
|
6
|
+
import MediaStackGrid, { type MediaStackTile } from '../_shared/MediaStackGrid'
|
|
9
7
|
import { useViewer } from '../_shared/useViewer'
|
|
10
8
|
import VideoViewer, { type VideoViewerItem } from '../_shared/VideoViewer'
|
|
11
9
|
import {
|
|
@@ -117,7 +115,9 @@ const resolveItems = ({
|
|
|
117
115
|
items?: VideoItem[]
|
|
118
116
|
}): VideoItem[] => {
|
|
119
117
|
if (items && items.length > 0) {
|
|
120
|
-
return preload
|
|
118
|
+
return preload
|
|
119
|
+
? items.map((it) => ({ ...it, preload: it.preload ?? preload }))
|
|
120
|
+
: items
|
|
121
121
|
}
|
|
122
122
|
if (src) return [{ src, poster, mimeType, preload }]
|
|
123
123
|
return []
|
|
@@ -167,9 +167,8 @@ const VideoComposerInner: React.FC<{
|
|
|
167
167
|
onClick?: (index: number) => boolean | void
|
|
168
168
|
onDismiss?: () => void
|
|
169
169
|
}> = ({ src, poster, mimeType, filename, preload, onClick, onDismiss }) => {
|
|
170
|
-
const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
|
|
171
|
-
onClick
|
|
172
|
-
)
|
|
170
|
+
const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
|
|
171
|
+
useViewer(onClick)
|
|
173
172
|
|
|
174
173
|
return (
|
|
175
174
|
<div className="relative w-fit">
|
|
@@ -189,10 +188,7 @@ const VideoComposerInner: React.FC<{
|
|
|
189
188
|
|
|
190
189
|
<VideoViewer
|
|
191
190
|
open={viewerOpen}
|
|
192
|
-
items={buildViewerItems(
|
|
193
|
-
[{ src, poster, mimeType, preload }],
|
|
194
|
-
filename
|
|
195
|
-
)}
|
|
191
|
+
items={buildViewerItems([{ src, poster, mimeType, preload }], filename)}
|
|
196
192
|
initialIndex={viewerIndex}
|
|
197
193
|
onClose={closeViewer}
|
|
198
194
|
/>
|
|
@@ -219,9 +215,8 @@ const VideoBubbleRow: React.FC<InternalVideoRowProps> = ({
|
|
|
219
215
|
}) => {
|
|
220
216
|
const resolvedItems = resolveItems({ src, poster, mimeType, preload, items })
|
|
221
217
|
const variant = bubbleVariantForState(state)
|
|
222
|
-
const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
|
|
223
|
-
onClick
|
|
224
|
-
)
|
|
218
|
+
const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
|
|
219
|
+
useViewer(onClick)
|
|
225
220
|
|
|
226
221
|
if (resolvedItems.length === 0) {
|
|
227
222
|
return null
|
|
@@ -126,9 +126,7 @@ const CompactDocumentRow: React.FC<CompactDocumentRowProps> = ({
|
|
|
126
126
|
aria-label={activateLabel}
|
|
127
127
|
className={classNames(
|
|
128
128
|
'flex min-w-0 flex-1 items-center gap-3 rounded-sm text-left transition-colors',
|
|
129
|
-
variant === 'dark'
|
|
130
|
-
? 'hover:bg-white/[0.04]'
|
|
131
|
-
: 'hover:bg-black/[0.04]'
|
|
129
|
+
variant === 'dark' ? 'hover:bg-white/[0.04]' : 'hover:bg-black/[0.04]'
|
|
132
130
|
)}
|
|
133
131
|
>
|
|
134
132
|
{iconTile}
|
|
@@ -67,9 +67,7 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
|
|
|
67
67
|
open={open}
|
|
68
68
|
onClose={onClose}
|
|
69
69
|
ariaLabel={filename}
|
|
70
|
-
counter={
|
|
71
|
-
items.length > 1 ? `${index + 1} / ${items.length}` : undefined
|
|
72
|
-
}
|
|
70
|
+
counter={items.length > 1 ? `${index + 1} / ${items.length}` : undefined}
|
|
73
71
|
actions={
|
|
74
72
|
<DownloadAction
|
|
75
73
|
url={item.src}
|
|
@@ -119,7 +119,11 @@ const MediaStackGrid: React.FC<MediaStackGridProps> = ({
|
|
|
119
119
|
const visible = tiles.slice(0, Math.min(total, maxVisible))
|
|
120
120
|
const overflow = total - visible.length
|
|
121
121
|
|
|
122
|
-
const renderTile = (
|
|
122
|
+
const renderTile = (
|
|
123
|
+
tile: MediaStackTile,
|
|
124
|
+
index: number,
|
|
125
|
+
extra?: React.ReactNode
|
|
126
|
+
) => {
|
|
123
127
|
const sharedClass = classNames(
|
|
124
128
|
TILE_SHELL,
|
|
125
129
|
'h-full w-full',
|
|
@@ -65,9 +65,7 @@ const PdfViewer: React.FC<PdfViewerProps> = ({
|
|
|
65
65
|
open={open}
|
|
66
66
|
onClose={onClose}
|
|
67
67
|
ariaLabel={filename}
|
|
68
|
-
counter={
|
|
69
|
-
items.length > 1 ? `${index + 1} / ${items.length}` : undefined
|
|
70
|
-
}
|
|
68
|
+
counter={items.length > 1 ? `${index + 1} / ${items.length}` : undefined}
|
|
71
69
|
data-testid="pdf-viewer"
|
|
72
70
|
>
|
|
73
71
|
<iframe
|