@linktr.ee/messaging-react 3.37.0-rc-1786190741 → 3.37.0

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