@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.
Files changed (91) hide show
  1. package/README.md +4 -4
  2. package/dist/Card-415OfRhw.cjs.map +1 -1
  3. package/dist/{Card-D4PPD5qa.js → Card-DVMzYquF.js} +11 -8
  4. package/dist/Card-DVMzYquF.js.map +1 -0
  5. package/dist/{Card-qS17qnN0.js → Card-DaJtDL7m.js} +15 -6
  6. package/dist/Card-DaJtDL7m.js.map +1 -0
  7. package/dist/Card-Dxk_Ggxw.cjs.map +1 -1
  8. package/dist/LoadingDots-Ct3WmVlT.js.map +1 -1
  9. package/dist/LoadingDots-D9GGphgM.cjs.map +1 -1
  10. package/dist/{index-DGmVyxHy.js → index-BOBIiAuD.js} +87 -62
  11. package/dist/index-BOBIiAuD.js.map +1 -0
  12. package/dist/index-D7A8U8GQ.cjs.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +1 -1
  15. package/package.json +3 -2
  16. package/src/components/ActionButton/ActionButton.stories.tsx +0 -2
  17. package/src/components/ActionButton/ActionButton.test.tsx +60 -55
  18. package/src/components/ActionButton/index.tsx +13 -14
  19. package/src/components/AttachmentCard/AttachmentCard.stories.tsx +1 -3
  20. package/src/components/AttachmentCard/MediaPlayer.tsx +9 -7
  21. package/src/components/AttachmentCard/index.tsx +6 -2
  22. package/src/components/Avatar/Avatar.stories.tsx +17 -5
  23. package/src/components/Avatar/index.tsx +13 -23
  24. package/src/components/ChannelView.test.tsx +6 -10
  25. package/src/components/CustomMessage/CustomMessage.stories.tsx +7 -2
  26. package/src/components/CustomMessage/CustomMessage.test.tsx +5 -3
  27. package/src/components/CustomMessage/LockedAttachment/LockedAttachment.test.tsx +5 -4
  28. package/src/components/CustomMessage/LockedAttachment/components/Text/Card.tsx +2 -4
  29. package/src/components/CustomMessage/LockedAttachment/components/_shared/CardBody.tsx +9 -6
  30. package/src/components/CustomMessage/LockedAttachment/components/_shared/CardThumbnail.tsx +1 -3
  31. package/src/components/CustomMessage/LockedAttachment/components/_shared/GalleryThumbnail.tsx +1 -4
  32. package/src/components/CustomMessage/LockedAttachment/components/_shared/PurchaseStatusBadge.tsx +0 -1
  33. package/src/components/CustomMessage/LockedAttachment/components/_shared/SingleThumbnail.tsx +1 -4
  34. package/src/components/CustomMessage/LockedAttachment/components/_shared/SkeletonTextLines.tsx +7 -2
  35. package/src/components/CustomMessage/LockedAttachment/types.ts +4 -3
  36. package/src/components/CustomMessage/MessageAttachmentConversations.stories.tsx +89 -86
  37. package/src/components/CustomMessage/MessageVoteButtons.tsx +4 -1
  38. package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +13 -10
  39. package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +1 -4
  40. package/src/components/CustomMessage/StreamAttachmentMessage.stories.tsx +4 -4
  41. package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +6 -7
  42. package/src/components/CustomMessage/context.tsx +7 -2
  43. package/src/components/CustomMessage/index.tsx +1 -4
  44. package/src/components/CustomMessageInput/CustomMessageInput.stories.tsx +33 -11
  45. package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +3 -3
  46. package/src/components/CustomSystemMessage/CustomSystemMessage.test.tsx +6 -9
  47. package/src/components/CustomTypingIndicator/CustomTypingIndicator.stories.tsx +4 -1
  48. package/src/components/IconButton/index.tsx +21 -17
  49. package/src/components/LinkAttachment/LinkAttachment.test.tsx +4 -1
  50. package/src/components/LinkAttachment/components/_shared/CardCta.tsx +4 -1
  51. package/src/components/Loading/Loading.stories.tsx +1 -4
  52. package/src/components/MediaMessage/MediaMessage.stories.tsx +6 -18
  53. package/src/components/MediaMessage/MediaMessage.test.tsx +30 -16
  54. package/src/components/MediaMessage/index.tsx +51 -14
  55. package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +4 -2
  56. package/src/components/MessageAttachment/File/FileAttachment.stories.tsx +3 -1
  57. package/src/components/MessageAttachment/Image/ImageAttachment.stories.tsx +15 -4
  58. package/src/components/MessageAttachment/Image/index.tsx +6 -12
  59. package/src/components/MessageAttachment/MessageAttachment.test.tsx +59 -27
  60. package/src/components/MessageAttachment/Pdf/PdfAttachment.stories.tsx +3 -1
  61. package/src/components/MessageAttachment/Pdf/index.tsx +2 -3
  62. package/src/components/MessageAttachment/Video/VideoAttachment.stories.tsx +11 -3
  63. package/src/components/MessageAttachment/Video/index.tsx +9 -14
  64. package/src/components/MessageAttachment/_shared/CompactDocumentRow.tsx +1 -3
  65. package/src/components/MessageAttachment/_shared/ImageViewer.tsx +1 -3
  66. package/src/components/MessageAttachment/_shared/MediaStackGrid.tsx +5 -1
  67. package/src/components/MessageAttachment/_shared/PdfViewer.tsx +1 -3
  68. package/src/components/MessageAttachment/_shared/VideoViewer.tsx +1 -3
  69. package/src/components/MessageAttachment/_shared/fileMeta.test.ts +4 -6
  70. package/src/components/MessageAttachment/stories/StoryTable.tsx +1 -3
  71. package/src/components/MessageBubble/MessageBubble.stories.tsx +5 -1
  72. package/src/components/MessageBubble/MessageBubble.test.tsx +1 -3
  73. package/src/components/MessageBubble/components/_shared/bubbleTail.tsx +2 -1
  74. package/src/components/MessagingShell/LoadingState.stories.tsx +0 -1
  75. package/src/components/RichCard/RichCard.stories.tsx +14 -3
  76. package/src/components/RichCard/RichCard.test.tsx +16 -4
  77. package/src/components/RichCard/RichCardBody.tsx +11 -3
  78. package/src/components/RichCard/RichCardCard.tsx +1 -3
  79. package/src/components/RichCard/RichCardImages.tsx +2 -4
  80. package/src/components/SearchInput/SearchInput.stories.tsx +0 -1
  81. package/src/components/SearchInput/SearchInput.test.tsx +94 -65
  82. package/src/hooks/useChannelStar.ts +3 -1
  83. package/src/hooks/useMessaging.ts +4 -4
  84. package/src/stories/decorators/storyUser.tsx +2 -2
  85. package/src/stories/mocks.tsx +4 -1
  86. package/src/styles.css +23 -8
  87. package/src/types.ts +1 -2
  88. package/src/utils/cdnImageUrl.test.ts +3 -1
  89. package/dist/Card-D4PPD5qa.js.map +0 -1
  90. package/dist/Card-qS17qnN0.js.map +0 -1
  91. 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('data-source', 'https://cdn.example.com/clip.mp4')
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('data-user-id', 'user-1')
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(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument()
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(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument()
394
- expect(screen.queryByRole('button', { name: 'View full screen' })).not.toBeInTheDocument()
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(container.querySelector('.str-chat__message')).not.toBeInTheDocument()
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(container.querySelector('.str-chat__message')).not.toBeInTheDocument()
433
- expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument()
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.spyOn(global, 'fetch').mockRejectedValue(new Error('fail'))
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 { CircleNotchIcon, DownloadSimpleIcon, LinkIcon } from '@phosphor-icons/react'
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 className={`truncate text-[14px] font-medium leading-5 ${linkPrimaryText(isMyMessage)}`}>
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 className={`truncate text-[12px] leading-4 ${linkSecondaryText(isMyMessage)}`}>{text}</p>
83
+ <p
84
+ className={`truncate text-[12px] leading-4 ${linkSecondaryText(isMyMessage)}`}
85
+ >
86
+ {text}
87
+ </p>
78
88
  )}
79
89
  {url && (
80
- <p className={`mt-1 truncate text-[12px] leading-4 ${linkTertiaryText(isMyMessage)}`}>
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 href={url} target="_blank" rel="noopener noreferrer" className="block no-underline">
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 { name = filename ?? new URL(url).pathname.split('/').pop() ?? 'download' }
112
- catch { name = filename ?? 'download' }
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(() => { fallback?.close() })
139
- .catch(() => { if (fallback) fallback.location.href = url })
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 className="size-4 animate-spin text-white" weight="bold" />
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)?.file_size
209
- const thumbnailUrl = (videoAttachment as { thumb_url?: string } | undefined)?.thumb_url
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={{ padding: 0, borderRadius: 0, overflow: 'visible', background: 'transparent' }}
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 attachment={linkAttachment} isMyMessage={isMyMessage} />
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={<FileAttachment.Sent {...STACK_FILES[0]} groupPosition="single" />}
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 items={STACK_IMAGES.slice(0, 2)} filename="album" />
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 items={STACK_IMAGES.slice(0, 3)} filename="album" />
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 items={STACK_IMAGES.slice(0, 4)} filename="album" />
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={<ImageAttachment.Received items={STACK_IMAGES} filename="album" />}
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
- type ImageViewerItem,
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 } = useViewer(
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 } = useViewer(
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
- Number(mediaBox('Open image 1 of 1').style.aspectRatio)
312
- ).toBeCloseTo(326 / 246, 4)
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
- Number(mediaBox('Open image 1 of 1').style.aspectRatio)
331
- ).toBeCloseTo(0.5625, 4)
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
- { src: 'https://cdn.example.com/a.jpg', alt: 'A', loading: 'eager' },
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
- { groupedByUser: false, firstOfGroup: true, endOfGroup: true, expected: 'single' },
1015
- { groupedByUser: true, firstOfGroup: true, endOfGroup: true, expected: 'single' },
1016
- { groupedByUser: true, firstOfGroup: true, endOfGroup: false, expected: 'first' },
1017
- { groupedByUser: true, firstOfGroup: false, endOfGroup: true, expected: 'end' },
1018
- { groupedByUser: true, firstOfGroup: false, endOfGroup: false, expected: 'middle' },
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({ groupedByUser, firstOfGroup, endOfGroup })
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 {...FILE_PROPS} groupPosition={groupPosition} />
1080
+ <MessageAttachment.File.Sent
1081
+ {...FILE_PROPS}
1082
+ groupPosition={groupPosition}
1083
+ />
1054
1084
  )
1055
1085
  expect(
1056
- screen.getByTestId('file-attachment').getAttribute('data-group-position')
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={<PdfAttachment.Received src={PDF_SRC} filename="contract.pdf" />}
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 } = useViewer(
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 items={STACK_VIDEOS.slice(0, 2)} filename="clips" />
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 items={STACK_VIDEOS.slice(0, 3)} filename="clips" />
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={<VideoAttachment.Received items={STACK_VIDEOS} filename="clips" />}
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 ? items.map((it) => ({ ...it, preload: it.preload ?? preload })) : items
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 } = useViewer(
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 } = useViewer(
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 = (tile: MediaStackTile, index: number, extra?: React.ReactNode) => {
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