@linktr.ee/messaging-react 2.1.0 → 2.2.0-rc-1778753733

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 (58) hide show
  1. package/dist/{Card-CsJvUF_b.js → Card-BdTueeyk.js} +2 -2
  2. package/dist/{Card-CsJvUF_b.js.map → Card-BdTueeyk.js.map} +1 -1
  3. package/dist/{Card-DlMSDSdm.js → Card-ChR37pLZ.js} +2 -2
  4. package/dist/{Card-DlMSDSdm.js.map → Card-ChR37pLZ.js.map} +1 -1
  5. package/dist/{Card-CFFNq49v.js → Card-EKxCn56j.js} +3 -3
  6. package/dist/{Card-CFFNq49v.js.map → Card-EKxCn56j.js.map} +1 -1
  7. package/dist/{LockedThumbnail-DpJx169C.js → LockedThumbnail-B16qP3eH.js} +2 -2
  8. package/dist/{LockedThumbnail-DpJx169C.js.map → LockedThumbnail-B16qP3eH.js.map} +1 -1
  9. package/dist/index-Dn7BC9xK.js +4748 -0
  10. package/dist/index-Dn7BC9xK.js.map +1 -0
  11. package/dist/index.d.ts +591 -25
  12. package/dist/index.js +24 -19
  13. package/package.json +1 -1
  14. package/src/components/CustomMessage/MessageAttachmentConversations.stories.tsx +841 -0
  15. package/src/components/LinkAttachment/LinkAttachment.stories.tsx +7 -92
  16. package/src/components/LinkAttachment/LinkAttachment.test.tsx +69 -0
  17. package/src/components/LinkAttachment/components/Received/Card.tsx +10 -30
  18. package/src/components/LinkAttachment/components/_shared/CardShell.tsx +5 -1
  19. package/src/components/LinkAttachment/index.tsx +24 -50
  20. package/src/components/LinkAttachment/types.ts +12 -5
  21. package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +203 -0
  22. package/src/components/MessageAttachment/Audio/index.tsx +189 -0
  23. package/src/components/MessageAttachment/File/FileAttachment.stories.tsx +352 -0
  24. package/src/components/MessageAttachment/File/index.tsx +240 -0
  25. package/src/components/MessageAttachment/Image/ImageAttachment.stories.tsx +288 -0
  26. package/src/components/MessageAttachment/Image/index.tsx +257 -0
  27. package/src/components/MessageAttachment/MessageAttachment.test.tsx +783 -0
  28. package/src/components/MessageAttachment/Pdf/PdfAttachment.stories.tsx +292 -0
  29. package/src/components/MessageAttachment/Pdf/index.tsx +228 -0
  30. package/src/components/MessageAttachment/Video/VideoAttachment.stories.tsx +272 -0
  31. package/src/components/MessageAttachment/Video/index.tsx +281 -0
  32. package/src/components/MessageAttachment/_shared/Bubble.tsx +173 -0
  33. package/src/components/MessageAttachment/_shared/CompactDocumentRow.tsx +152 -0
  34. package/src/components/MessageAttachment/_shared/DismissButton.tsx +39 -0
  35. package/src/components/MessageAttachment/_shared/DownloadAction.tsx +175 -0
  36. package/src/components/MessageAttachment/_shared/ImageViewer.tsx +314 -0
  37. package/src/components/MessageAttachment/_shared/MediaStackGrid.tsx +139 -0
  38. package/src/components/MessageAttachment/_shared/PdfViewer.tsx +100 -0
  39. package/src/components/MessageAttachment/_shared/VideoViewer.tsx +171 -0
  40. package/src/components/MessageAttachment/_shared/ViewerShell.tsx +159 -0
  41. package/src/components/MessageAttachment/_shared/fileMeta.test.ts +82 -0
  42. package/src/components/MessageAttachment/_shared/fileMeta.ts +95 -0
  43. package/src/components/MessageAttachment/_shared/triggerDownload.ts +54 -0
  44. package/src/components/MessageAttachment/_shared/useViewer.ts +53 -0
  45. package/src/components/MessageAttachment/index.tsx +149 -0
  46. package/src/components/MessageAttachment/stories/StoryTable.tsx +72 -0
  47. package/src/components/MessageAttachment/types.ts +178 -0
  48. package/src/index.ts +32 -0
  49. package/dist/Card-D32U6KfZ.js +0 -85
  50. package/dist/Card-D32U6KfZ.js.map +0 -1
  51. package/dist/Card-DlSSJPip.js +0 -60
  52. package/dist/Card-DlSSJPip.js.map +0 -1
  53. package/dist/Card-zGbhRBwv.js +0 -48
  54. package/dist/Card-zGbhRBwv.js.map +0 -1
  55. package/dist/CardThumbnail-DTBuRQHF.js +0 -239
  56. package/dist/CardThumbnail-DTBuRQHF.js.map +0 -1
  57. package/dist/index-DfcRe-Hj.js +0 -3103
  58. package/dist/index-DfcRe-Hj.js.map +0 -1
@@ -0,0 +1,841 @@
1
+ import type { Meta, StoryFn } from '@storybook/react'
2
+ import classNames from 'classnames'
3
+ import React from 'react'
4
+
5
+ import {
6
+ currentUserArgType,
7
+ StoryUser,
8
+ storyUsers,
9
+ } from '../../stories/decorators/storyUser'
10
+ import { Avatar } from '../Avatar'
11
+ import MessageAttachment, {
12
+ type BubbleGroupPosition,
13
+ } from '../MessageAttachment'
14
+
15
+ /* ──────────────────────────────────────────────────────────────────
16
+ * Lightweight conversation mock used only by these stories.
17
+ *
18
+ * The production `CustomMessage` flow needs the full stream-chat-react
19
+ * machinery (Channel + MessageList + the `Attachment` override pipe).
20
+ * That's overkill for a visual showcase, so these stories render a
21
+ * bare conversation timeline that mimics the same layout primitives:
22
+ *
23
+ * - Sender (current user) bubbles align right, no avatar.
24
+ * - Recipient bubbles align left with a 28px circle avatar.
25
+ * - Adjacent bubbles from the same user gap by 2px (matches the
26
+ * `.str-chat__message-bubble-wrapper { gap: 2px }` rule in
27
+ * `styles.css`), separate-author boundaries get more breathing
28
+ * room.
29
+ * - Text bubbles inherit the same `MessageAttachment.Bubble` chrome
30
+ * (background, padding, radius, font) so attachments and text sit
31
+ * consistently inside the same conversation column.
32
+ * ────────────────────────────────────────────────────────────────── */
33
+
34
+ type ChatRole = 'sender' | 'receiver'
35
+
36
+ interface ChatNode {
37
+ id: string
38
+ role: ChatRole
39
+ /** Either a string (rendered as a text bubble) or a custom React node
40
+ * (e.g. `<MessageAttachment.Image.Sent ... />`). */
41
+ content: React.ReactNode
42
+ }
43
+
44
+ const TEXT_BG_BY_ROLE: Record<ChatRole, string> = {
45
+ sender: 'bg-[#121110] text-white',
46
+ receiver: 'bg-[#e9eaed] text-[#080707]',
47
+ }
48
+
49
+ const TextBubble: React.FC<{ role: ChatRole; children: React.ReactNode }> = ({
50
+ role,
51
+ children,
52
+ }) => (
53
+ <div
54
+ className={classNames(
55
+ // Same 18px radius + 8px / 16px padding as
56
+ // `MessageAttachment.Bubble`, so text bubbles share the same
57
+ // chrome as attachment bubbles.
58
+ 'max-w-[280px] whitespace-pre-wrap break-words rounded-[18px] px-4 py-2',
59
+ TEXT_BG_BY_ROLE[role]
60
+ )}
61
+ >
62
+ {children}
63
+ </div>
64
+ )
65
+
66
+ const ChatRow: React.FC<{
67
+ role: ChatRole
68
+ showAvatar: boolean
69
+ user: StoryUser
70
+ children: React.ReactNode
71
+ }> = ({ role, showAvatar, user, children }) => (
72
+ <div
73
+ className={classNames(
74
+ 'flex items-end gap-2',
75
+ role === 'sender' ? 'justify-end' : 'justify-start'
76
+ )}
77
+ >
78
+ {role === 'receiver' ? (
79
+ <div className="w-7 shrink-0">
80
+ {showAvatar ? (
81
+ <Avatar
82
+ id={user.id}
83
+ name={user.name}
84
+ image={user.image}
85
+ size={28}
86
+ shape="circle"
87
+ />
88
+ ) : null}
89
+ </div>
90
+ ) : null}
91
+ <div className={role === 'sender' ? 'flex justify-end' : 'flex justify-start'}>
92
+ {children}
93
+ </div>
94
+ </div>
95
+ )
96
+
97
+ const ChatMock: React.FC<{
98
+ currentUser: StoryUser
99
+ receiver: StoryUser
100
+ messages: ChatNode[]
101
+ }> = ({ currentUser, receiver, messages }) => {
102
+ // Render an avatar on the *last* message of each receiver run — same
103
+ // grouping rule the real chat uses so consecutive bubbles from one
104
+ // sender share avatar real estate.
105
+ const isLastInRun = (index: number): boolean => {
106
+ const next = messages[index + 1]
107
+ return !next || next.role !== messages[index].role
108
+ }
109
+
110
+ return (
111
+ <div className="mx-auto flex max-w-2xl flex-col gap-1 p-12">
112
+ {messages.map((msg, index) => {
113
+ const prev = messages[index - 1]
114
+ const newAuthorBoundary = !prev || prev.role !== msg.role
115
+ return (
116
+ <div
117
+ key={msg.id}
118
+ className={newAuthorBoundary && index > 0 ? 'mt-3' : undefined}
119
+ >
120
+ <ChatRow
121
+ role={msg.role}
122
+ user={msg.role === 'sender' ? currentUser : receiver}
123
+ showAvatar={msg.role === 'receiver' && isLastInRun(index)}
124
+ >
125
+ {typeof msg.content === 'string' ? (
126
+ <TextBubble role={msg.role}>{msg.content}</TextBubble>
127
+ ) : (
128
+ msg.content
129
+ )}
130
+ </ChatRow>
131
+ </div>
132
+ )
133
+ })}
134
+ </div>
135
+ )
136
+ }
137
+
138
+ /* ──────────────────────────────────────────────────────────────────
139
+ * Multi-attachment message wrapper.
140
+ *
141
+ * Real chat UIs let one logical "message" carry several attachments
142
+ * of different kinds — e.g. a voice memo + a PDF, or two photos + an
143
+ * audio note. Each attachment renders as its own `MessageAttachment.*`
144
+ * bubble, but the whole set should read as a single visual cluster
145
+ * from the same author. To do that we:
146
+ *
147
+ * - Stack the bubbles vertically with `gap-[2px]` (the same gap
148
+ * `.str-chat__message-bubble-wrapper` uses between grouped text
149
+ * bubbles in `styles.css`).
150
+ * - Inject `groupPosition='first' | 'middle' | 'end'` on each child
151
+ * so the corners flatten on the edges that face siblings inside
152
+ * the cluster — same merging rule the `Bubble` component already
153
+ * applies to consecutive text messages.
154
+ * - Right-align for sender clusters / left-align for receiver
155
+ * clusters so the run anchors to the correct chat edge.
156
+ *
157
+ * The wrapper is intentionally local to this stories file: the real
158
+ * `CustomMessage` integration derives `groupPosition` from
159
+ * stream-chat-react's `firstOfGroup` / `endOfGroup` flags via the
160
+ * `bubbleGroupPositionFromStream` helper instead. This wrapper just
161
+ * gives the story author a clean way to express "one message, many
162
+ * attachments" without wiring up the full message-context plumbing.
163
+ * ────────────────────────────────────────────────────────────────── */
164
+
165
+ interface MultiAttachmentMessageProps {
166
+ align: 'sender' | 'receiver'
167
+ children: React.ReactNode
168
+ }
169
+
170
+ const MultiAttachmentMessage: React.FC<MultiAttachmentMessageProps> = ({
171
+ align,
172
+ children,
173
+ }) => {
174
+ const items = React.Children.toArray(children).filter(
175
+ React.isValidElement
176
+ ) as React.ReactElement<{ groupPosition?: BubbleGroupPosition }>[]
177
+ const total = items.length
178
+
179
+ return (
180
+ <div
181
+ className={classNames(
182
+ 'flex flex-col gap-[2px]',
183
+ align === 'sender' ? 'items-end' : 'items-start'
184
+ )}
185
+ >
186
+ {items.map((child, index) => {
187
+ const groupPosition: BubbleGroupPosition =
188
+ total === 1
189
+ ? 'single'
190
+ : index === 0
191
+ ? 'first'
192
+ : index === total - 1
193
+ ? 'end'
194
+ : 'middle'
195
+ return React.cloneElement(child, {
196
+ key: child.key ?? index,
197
+ groupPosition,
198
+ })
199
+ })}
200
+ </div>
201
+ )
202
+ }
203
+
204
+ const meta: Meta = {
205
+ title: 'CustomMessage',
206
+ argTypes: currentUserArgType,
207
+ args: { currentUser: storyUsers.creator },
208
+ parameters: {
209
+ layout: 'fullscreen',
210
+ // The render functions build a `messages` array containing JSX
211
+ // attachment elements (with live React fiber `_owner` refs once
212
+ // mounted) and hand it to `<ChatMock messages={…}>`. Storybook's
213
+ // default `dynamic` source extractor recurses through that prop
214
+ // via `react-element-to-jsx-string` → `prettyPrint`, walks each
215
+ // element's fiber graph, and blows the call stack. Pin source
216
+ // generation to the static story code instead.
217
+ docs: { source: { type: 'code' } },
218
+ },
219
+ }
220
+ export default meta
221
+
222
+ interface ConversationStoryArgs {
223
+ currentUser: StoryUser
224
+ }
225
+
226
+ const HERO_PHOTO = 'https://picsum.photos/seed/portrait/720/720'
227
+ const STUDIO_PHOTOS = [
228
+ { src: 'https://picsum.photos/seed/studio-1/720/720', alt: 'Studio shot 1' },
229
+ { src: 'https://picsum.photos/seed/studio-2/720/720', alt: 'Studio shot 2' },
230
+ { src: 'https://picsum.photos/seed/studio-3/720/720', alt: 'Studio shot 3' },
231
+ ]
232
+ const VIDEO_SRC = '/video-source.mp4'
233
+ const VIDEO_POSTER = '/video-thumbnail.jpg'
234
+ const AUDIO_SRC =
235
+ 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
236
+ const PDF_SRC = 'https://www.w3.org/WAI/WCAG21/wcag21.pdf'
237
+
238
+ /**
239
+ * Image attachments inside a conversation — the receiver shares a
240
+ * single hero shot (with caption) and the sender replies with a
241
+ * stacked album. Click any bubble to open the lightbox viewer.
242
+ */
243
+ export const ConversationWithImages: StoryFn<ConversationStoryArgs> = ({
244
+ currentUser,
245
+ }) => {
246
+ const isSenderTheCreator = currentUser.id === storyUsers.creator.id
247
+ const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
248
+ return (
249
+ <ChatMock
250
+ currentUser={currentUser}
251
+ receiver={receiver}
252
+ messages={[
253
+ {
254
+ id: 'msg-1',
255
+ role: 'receiver',
256
+ content: 'Got the new headshots back — thoughts?',
257
+ },
258
+ {
259
+ id: 'msg-2',
260
+ role: 'receiver',
261
+ content: (
262
+ <MessageAttachment.Image.Received
263
+ src={HERO_PHOTO}
264
+ alt="Headshot pick"
265
+ filename="headshot-pick.jpg"
266
+ text="This one's my favorite"
267
+ />
268
+ ),
269
+ },
270
+ {
271
+ id: 'msg-3',
272
+ role: 'sender',
273
+ content: 'Love it! Here are a few more from the studio set.',
274
+ },
275
+ {
276
+ id: 'msg-4',
277
+ role: 'sender',
278
+ content: (
279
+ <MessageAttachment.Image.Sent
280
+ items={STUDIO_PHOTOS}
281
+ filename="studio-album"
282
+ />
283
+ ),
284
+ },
285
+ {
286
+ id: 'msg-5',
287
+ role: 'receiver',
288
+ content: 'These are great — let me pick two for the launch page.',
289
+ },
290
+ ]}
291
+ />
292
+ )
293
+ }
294
+
295
+ /**
296
+ * Video attachments — receiver sends a single clip with a caption,
297
+ * sender replies and shares back a stacked set.
298
+ */
299
+ export const ConversationWithVideos: StoryFn<ConversationStoryArgs> = ({
300
+ currentUser,
301
+ }) => {
302
+ const isSenderTheCreator = currentUser.id === storyUsers.creator.id
303
+ const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
304
+ const stack = Array.from({ length: 3 }, () => ({
305
+ src: VIDEO_SRC,
306
+ poster: VIDEO_POSTER,
307
+ mimeType: 'video/mp4',
308
+ }))
309
+ return (
310
+ <ChatMock
311
+ currentUser={currentUser}
312
+ receiver={receiver}
313
+ messages={[
314
+ {
315
+ id: 'msg-1',
316
+ role: 'receiver',
317
+ content: (
318
+ <MessageAttachment.Image.Received
319
+ src={VIDEO_POSTER}
320
+ alt="Reference still"
321
+ />
322
+ ),
323
+ },
324
+ {
325
+ id: 'msg-2',
326
+ role: 'receiver',
327
+ content: 'And here is the rough cut from yesterday.',
328
+ },
329
+ {
330
+ id: 'msg-3',
331
+ role: 'receiver',
332
+ content: (
333
+ <MessageAttachment.Video.Received
334
+ src={VIDEO_SRC}
335
+ poster={VIDEO_POSTER}
336
+ mimeType="video/mp4"
337
+ filename="rough-cut.mp4"
338
+ text="Open ending — tell me which version lands."
339
+ />
340
+ ),
341
+ },
342
+ {
343
+ id: 'msg-4',
344
+ role: 'sender',
345
+ content: 'On it. Sending three angles back.',
346
+ },
347
+ {
348
+ id: 'msg-5',
349
+ role: 'sender',
350
+ content: (
351
+ <MessageAttachment.Video.Sent items={stack} filename="angles" />
352
+ ),
353
+ },
354
+ ]}
355
+ />
356
+ )
357
+ }
358
+
359
+ /**
360
+ * Audio attachments — voice memo back-and-forth, plus a stacked
361
+ * studio takes message.
362
+ */
363
+ export const ConversationWithAudio: StoryFn<ConversationStoryArgs> = ({
364
+ currentUser,
365
+ }) => {
366
+ const isSenderTheCreator = currentUser.id === storyUsers.creator.id
367
+ const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
368
+ return (
369
+ <ChatMock
370
+ currentUser={currentUser}
371
+ receiver={receiver}
372
+ messages={[
373
+ {
374
+ id: 'msg-1',
375
+ role: 'receiver',
376
+ content: 'Quick voice memo with the new hook idea ↓',
377
+ },
378
+ {
379
+ id: 'msg-2',
380
+ role: 'receiver',
381
+ content: (
382
+ <MessageAttachment.Audio.Received
383
+ src={AUDIO_SRC}
384
+ mimeType="audio/mpeg"
385
+ filename="hook-idea.mp3"
386
+ />
387
+ ),
388
+ },
389
+ {
390
+ id: 'msg-3',
391
+ role: 'sender',
392
+ content: 'Love that. Here are three takes I cut tonight.',
393
+ },
394
+ {
395
+ id: 'msg-4',
396
+ role: 'sender',
397
+ content: (
398
+ <MessageAttachment.Audio.Sent
399
+ items={[
400
+ {
401
+ src: AUDIO_SRC,
402
+ mimeType: 'audio/mpeg',
403
+ filename: 'Take 1 — full mix.mp3',
404
+ },
405
+ {
406
+ src: AUDIO_SRC,
407
+ mimeType: 'audio/mpeg',
408
+ filename: 'Take 2 — vocals isolated.mp3',
409
+ },
410
+ {
411
+ src: AUDIO_SRC,
412
+ mimeType: 'audio/mpeg',
413
+ filename: 'Take 3 — instrumental only.mp3',
414
+ },
415
+ ]}
416
+ text="Pick whichever lands; happy to keep iterating."
417
+ />
418
+ ),
419
+ },
420
+ {
421
+ id: 'msg-5',
422
+ role: 'receiver',
423
+ content: 'Take 2 is the one. Shipping it.',
424
+ },
425
+ ]}
426
+ />
427
+ )
428
+ }
429
+
430
+ /**
431
+ * PDF attachments — single document with a caption and a stacked
432
+ * package of related documents.
433
+ */
434
+ export const ConversationWithPdfs: StoryFn<ConversationStoryArgs> = ({
435
+ currentUser,
436
+ }) => {
437
+ const isSenderTheCreator = currentUser.id === storyUsers.creator.id
438
+ const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
439
+ return (
440
+ <ChatMock
441
+ currentUser={currentUser}
442
+ receiver={receiver}
443
+ messages={[
444
+ {
445
+ id: 'msg-1',
446
+ role: 'receiver',
447
+ content: 'Can you send the ESOP summary before the call?',
448
+ },
449
+ {
450
+ id: 'msg-2',
451
+ role: 'sender',
452
+ content: 'Yep, here it is.',
453
+ },
454
+ {
455
+ id: 'msg-3',
456
+ role: 'sender',
457
+ content: (
458
+ <MessageAttachment.Pdf.Sent
459
+ src={PDF_SRC}
460
+ filename="ESOP-summary.pdf"
461
+ fileSize={388_658}
462
+ text="Section 4 has the vesting cliff details."
463
+ />
464
+ ),
465
+ },
466
+ {
467
+ id: 'msg-4',
468
+ role: 'receiver',
469
+ content: 'Got it. Sending back the full package for tonight.',
470
+ },
471
+ {
472
+ id: 'msg-5',
473
+ role: 'receiver',
474
+ content: (
475
+ <MessageAttachment.Pdf.Received
476
+ items={[
477
+ {
478
+ src: PDF_SRC,
479
+ filename: 'ESOP-summary.pdf',
480
+ fileSize: 388_658,
481
+ },
482
+ {
483
+ src: PDF_SRC,
484
+ filename: 'Vesting-schedule.pdf',
485
+ fileSize: 142_336,
486
+ },
487
+ {
488
+ src: PDF_SRC,
489
+ filename: 'Cap-table-snapshot.pdf',
490
+ fileSize: 96_021,
491
+ },
492
+ ]}
493
+ text="Full package — review whatever is most relevant."
494
+ />
495
+ ),
496
+ },
497
+ ]}
498
+ />
499
+ )
500
+ }
501
+
502
+ /**
503
+ * Generic file attachments — non-PDF documents with a download
504
+ * affordance on each row. Includes a stacked-files example where
505
+ * three documents share a single bubble with an 8px gap between
506
+ * rows, matching the stacked PDF / Audio treatment.
507
+ */
508
+ export const ConversationWithFiles: StoryFn<ConversationStoryArgs> = ({
509
+ currentUser,
510
+ }) => {
511
+ const isSenderTheCreator = currentUser.id === storyUsers.creator.id
512
+ const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
513
+ return (
514
+ <ChatMock
515
+ currentUser={currentUser}
516
+ receiver={receiver}
517
+ messages={[
518
+ {
519
+ id: 'msg-1',
520
+ role: 'receiver',
521
+ content: 'Do you still have the workout plan archive?',
522
+ },
523
+ {
524
+ id: 'msg-2',
525
+ role: 'sender',
526
+ content:
527
+ 'Sending it now — let me know if you need the source files too.',
528
+ },
529
+ {
530
+ id: 'msg-3',
531
+ role: 'sender',
532
+ content: (
533
+ <MessageAttachment.File.Sent
534
+ src="https://cdn.example.com/workout-plan.zip"
535
+ filename="workout-plan.zip"
536
+ mimeType="application/zip"
537
+ fileSize={2_457_600}
538
+ />
539
+ ),
540
+ },
541
+ {
542
+ id: 'msg-4',
543
+ role: 'receiver',
544
+ content: 'Perfect — also got the brand kit handy?',
545
+ },
546
+ {
547
+ id: 'msg-5',
548
+ role: 'sender',
549
+ content: (
550
+ <MessageAttachment.File.Sent
551
+ items={[
552
+ {
553
+ src: 'https://cdn.example.com/brand-guidelines.zip',
554
+ filename: 'brand-guidelines.zip',
555
+ mimeType: 'application/zip',
556
+ fileSize: 11_500_000,
557
+ },
558
+ {
559
+ src: 'https://cdn.example.com/Q4-revenue.xlsx',
560
+ filename: 'Q4-revenue.xlsx',
561
+ mimeType:
562
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
563
+ fileSize: 146_432,
564
+ },
565
+ {
566
+ src: 'https://cdn.example.com/meeting-notes.docx',
567
+ filename: 'meeting-notes.docx',
568
+ mimeType:
569
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
570
+ fileSize: 28_672,
571
+ },
572
+ ]}
573
+ text="Here's everything in one go."
574
+ />
575
+ ),
576
+ },
577
+ {
578
+ id: 'msg-6',
579
+ role: 'receiver',
580
+ content: 'Amazing, thanks!',
581
+ },
582
+ ]}
583
+ />
584
+ )
585
+ }
586
+
587
+ /**
588
+ * Mixed conversation — a realistic chat where the same thread carries
589
+ * a text exchange, a single image, a stacked video set, an audio memo,
590
+ * and a PDF handoff. Useful for sanity-checking spacing and bubble
591
+ * rhythm when multiple attachment types coexist.
592
+ */
593
+ export const ConversationWithMixedAttachments: StoryFn<ConversationStoryArgs> =
594
+ ({ currentUser }) => {
595
+ const isSenderTheCreator = currentUser.id === storyUsers.creator.id
596
+ const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
597
+ return (
598
+ <ChatMock
599
+ currentUser={currentUser}
600
+ receiver={receiver}
601
+ messages={[
602
+ {
603
+ id: 'msg-1',
604
+ role: 'receiver',
605
+ content: 'Hey! Got a sec to look at the launch package?',
606
+ },
607
+ {
608
+ id: 'msg-2',
609
+ role: 'sender',
610
+ content: 'Yep, send it over.',
611
+ },
612
+ {
613
+ id: 'msg-3',
614
+ role: 'receiver',
615
+ content: 'Here is the hero shot for the page',
616
+ },
617
+ {
618
+ id: 'msg-4',
619
+ role: 'receiver',
620
+ content: (
621
+ <MessageAttachment.Image.Received
622
+ src={HERO_PHOTO}
623
+ alt="Hero shot"
624
+ filename="hero.jpg"
625
+ />
626
+ ),
627
+ },
628
+ {
629
+ id: 'msg-5',
630
+ role: 'receiver',
631
+ content: (
632
+ <MessageAttachment.Video.Received
633
+ src={VIDEO_SRC}
634
+ poster={VIDEO_POSTER}
635
+ mimeType="video/mp4"
636
+ filename="trailer.mp4"
637
+ text="And the rough trailer ↑"
638
+ />
639
+ ),
640
+ },
641
+ {
642
+ id: 'msg-6',
643
+ role: 'sender',
644
+ content: 'Looks great. Quick voice memo with notes ↓',
645
+ },
646
+ {
647
+ id: 'msg-7',
648
+ role: 'sender',
649
+ content: (
650
+ <MessageAttachment.Audio.Sent
651
+ src={AUDIO_SRC}
652
+ mimeType="audio/mpeg"
653
+ filename="notes.mp3"
654
+ />
655
+ ),
656
+ },
657
+ {
658
+ id: 'msg-8',
659
+ role: 'sender',
660
+ content: (
661
+ <MessageAttachment.Pdf.Sent
662
+ src={PDF_SRC}
663
+ filename="launch-checklist.pdf"
664
+ fileSize={142_336}
665
+ text="And the launch checklist for the dry run."
666
+ />
667
+ ),
668
+ },
669
+ {
670
+ id: 'msg-9',
671
+ role: 'receiver',
672
+ content: 'Got everything. Will sync back in the morning.',
673
+ },
674
+ ]}
675
+ />
676
+ )
677
+ }
678
+
679
+ /**
680
+ * Multi-attachment message — audio + PDF.
681
+ *
682
+ * Mirrors the mobile picker UX where the composer can hold a single
683
+ * attachment but the recipient sees them sent together as one
684
+ * cluster. The voice memo sits on top (`groupPosition='first'`, flat
685
+ * bottom-right) and the PDF carries the caption + the cluster's tail
686
+ * corner (`groupPosition='end'`, flat top-right).
687
+ */
688
+ export const ConversationWithAudioAndPdfAttached: StoryFn<
689
+ ConversationStoryArgs
690
+ > = ({ currentUser }) => {
691
+ const isSenderTheCreator = currentUser.id === storyUsers.creator.id
692
+ const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
693
+ return (
694
+ <ChatMock
695
+ currentUser={currentUser}
696
+ receiver={receiver}
697
+ messages={[
698
+ {
699
+ id: 'msg-1',
700
+ role: 'receiver',
701
+ content: 'Did you get a chance to record the walkthrough?',
702
+ },
703
+ {
704
+ id: 'msg-2',
705
+ role: 'sender',
706
+ content: (
707
+ <MultiAttachmentMessage align="sender">
708
+ <MessageAttachment.Audio.Sent
709
+ src={AUDIO_SRC}
710
+ mimeType="audio/mpeg"
711
+ filename="walkthrough.mp3"
712
+ />
713
+ <MessageAttachment.Pdf.Sent
714
+ src={PDF_SRC}
715
+ filename="walkthrough-notes.pdf"
716
+ fileSize={142_336}
717
+ text="Voice memo + the written notes that go with it."
718
+ />
719
+ </MultiAttachmentMessage>
720
+ ),
721
+ },
722
+ {
723
+ id: 'msg-3',
724
+ role: 'receiver',
725
+ content: 'Perfect — listening now.',
726
+ },
727
+ ]}
728
+ />
729
+ )
730
+ }
731
+
732
+ /**
733
+ * Multi-attachment message — two photos + a voice memo.
734
+ *
735
+ * Demonstrates a mixed sensory recap: the receiver shares two
736
+ * reference photos in one stacked image bubble (`groupPosition='first'`)
737
+ * paired with an audio clip explaining what to look at
738
+ * (`groupPosition='end'`, carries the caption). All three pieces feel
739
+ * like one unit instead of three loose bubbles.
740
+ */
741
+ export const ConversationWithPhotosAndAudioAttached: StoryFn<
742
+ ConversationStoryArgs
743
+ > = ({ currentUser }) => {
744
+ const isSenderTheCreator = currentUser.id === storyUsers.creator.id
745
+ const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
746
+ return (
747
+ <ChatMock
748
+ currentUser={currentUser}
749
+ receiver={receiver}
750
+ messages={[
751
+ {
752
+ id: 'msg-1',
753
+ role: 'sender',
754
+ content: 'Anything stand out from the studio set?',
755
+ },
756
+ {
757
+ id: 'msg-2',
758
+ role: 'receiver',
759
+ content: (
760
+ <MultiAttachmentMessage align="receiver">
761
+ <MessageAttachment.Image.Received
762
+ items={STUDIO_PHOTOS.slice(0, 2)}
763
+ filename="studio-picks"
764
+ />
765
+ <MessageAttachment.Audio.Received
766
+ src={AUDIO_SRC}
767
+ mimeType="audio/mpeg"
768
+ filename="reactions.mp3"
769
+ text="Two favorites — voice note has the why."
770
+ />
771
+ </MultiAttachmentMessage>
772
+ ),
773
+ },
774
+ {
775
+ id: 'msg-3',
776
+ role: 'sender',
777
+ content: 'Great picks. Locking those in.',
778
+ },
779
+ ]}
780
+ />
781
+ )
782
+ }
783
+
784
+ /**
785
+ * Multi-attachment message — image + video + PDF (3 different types).
786
+ *
787
+ * Stress-tests the cluster with three different bubble shapes back to
788
+ * back. Image first (cropped poster, flat bottom-right), video middle
789
+ * (poster tile with play badge, flats on top-right + bottom-right),
790
+ * PDF last (compact row with download trailing button + caption,
791
+ * flat top-right). The whole stack reads as one message run.
792
+ */
793
+ export const ConversationWithImageVideoPdfAttached: StoryFn<
794
+ ConversationStoryArgs
795
+ > = ({ currentUser }) => {
796
+ const isSenderTheCreator = currentUser.id === storyUsers.creator.id
797
+ const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
798
+ return (
799
+ <ChatMock
800
+ currentUser={currentUser}
801
+ receiver={receiver}
802
+ messages={[
803
+ {
804
+ id: 'msg-1',
805
+ role: 'receiver',
806
+ content: 'Send everything you have for the launch deck.',
807
+ },
808
+ {
809
+ id: 'msg-2',
810
+ role: 'sender',
811
+ content: (
812
+ <MultiAttachmentMessage align="sender">
813
+ <MessageAttachment.Image.Sent
814
+ src={HERO_PHOTO}
815
+ alt="Hero shot"
816
+ filename="hero.jpg"
817
+ />
818
+ <MessageAttachment.Video.Sent
819
+ src={VIDEO_SRC}
820
+ poster={VIDEO_POSTER}
821
+ mimeType="video/mp4"
822
+ filename="trailer.mp4"
823
+ />
824
+ <MessageAttachment.Pdf.Sent
825
+ src={PDF_SRC}
826
+ filename="launch-deck.pdf"
827
+ fileSize={428_032}
828
+ text="Hero, trailer, deck — everything in one drop."
829
+ />
830
+ </MultiAttachmentMessage>
831
+ ),
832
+ },
833
+ {
834
+ id: 'msg-3',
835
+ role: 'receiver',
836
+ content: 'Got it. Reviewing now.',
837
+ },
838
+ ]}
839
+ />
840
+ )
841
+ }