@linktr.ee/messaging-react 3.10.0 → 3.11.0-rc-1783756377

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 (88) hide show
  1. package/dist/Card-415OfRhw.cjs +2 -0
  2. package/dist/Card-415OfRhw.cjs.map +1 -0
  3. package/dist/Card-BnbNynLQ.js +694 -0
  4. package/dist/Card-BnbNynLQ.js.map +1 -0
  5. package/dist/Card-UnwKmYzt.cjs +2 -0
  6. package/dist/Card-UnwKmYzt.cjs.map +1 -0
  7. package/dist/Card-qS17qnN0.js +103 -0
  8. package/dist/Card-qS17qnN0.js.map +1 -0
  9. package/dist/LoadingDots-Ct3WmVlT.js +59 -0
  10. package/dist/LoadingDots-Ct3WmVlT.js.map +1 -0
  11. package/dist/LoadingDots-D9GGphgM.cjs +2 -0
  12. package/dist/LoadingDots-D9GGphgM.cjs.map +1 -0
  13. package/dist/assets/index.css +1 -1
  14. package/dist/index-CyHyftBi.cjs +2 -0
  15. package/dist/index-CyHyftBi.cjs.map +1 -0
  16. package/dist/{index-BxzzWK4K.js → index-DJXjS2H9.js} +1397 -1358
  17. package/dist/index-DJXjS2H9.js.map +1 -0
  18. package/dist/index.cjs +1 -1
  19. package/dist/index.d.ts +196 -101
  20. package/dist/index.js +1 -1
  21. package/dist/testing.d.ts +1 -0
  22. package/package.json +2 -2
  23. package/src/components/ChannelList/CustomChannelPreview.test.tsx +95 -0
  24. package/src/components/ChannelList/CustomChannelPreview.tsx +21 -9
  25. package/src/components/ChannelList/index.test.tsx +46 -0
  26. package/src/components/ChannelList/index.tsx +12 -0
  27. package/src/components/CustomMessage/CustomMessage.stories.tsx +54 -1
  28. package/src/components/CustomMessage/LockedAttachment/LockedAttachment.stories.tsx +541 -0
  29. package/src/components/CustomMessage/LockedAttachment/LockedAttachment.test.tsx +117 -0
  30. package/src/components/CustomMessage/LockedAttachment/components/Media/Card.tsx +358 -0
  31. package/src/components/{LockedAttachment/components/Received → CustomMessage/LockedAttachment/components/Media}/CardActions.tsx +9 -2
  32. package/src/components/CustomMessage/LockedAttachment/components/Media/types.ts +79 -0
  33. package/src/components/CustomMessage/LockedAttachment/components/Text/Card.tsx +153 -0
  34. package/src/components/CustomMessage/LockedAttachment/components/Text/types.ts +58 -0
  35. package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/CardBody.tsx +8 -7
  36. package/src/components/CustomMessage/LockedAttachment/components/_shared/CardThumbnail.tsx +92 -0
  37. package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/GalleryThumbnail.tsx +31 -39
  38. package/src/components/CustomMessage/LockedAttachment/components/_shared/LoadingDots.tsx +36 -0
  39. package/src/components/CustomMessage/LockedAttachment/components/_shared/LockBadge.tsx +63 -0
  40. package/src/components/{LockedAttachment/components/_shared/LockedCardShell.tsx → CustomMessage/LockedAttachment/components/_shared/MediaShell.tsx} +8 -8
  41. package/src/components/CustomMessage/LockedAttachment/components/_shared/PreviewToggle.tsx +58 -0
  42. package/src/components/CustomMessage/LockedAttachment/components/_shared/PriceLockRow.tsx +21 -0
  43. package/src/components/CustomMessage/LockedAttachment/components/_shared/PurchaseStatusBadge.tsx +74 -0
  44. package/src/components/{LockedAttachment/components/_shared/LockedThumbnail.tsx → CustomMessage/LockedAttachment/components/_shared/SingleThumbnail.tsx} +11 -7
  45. package/src/components/CustomMessage/LockedAttachment/components/_shared/SkeletonTextLines.tsx +29 -0
  46. package/src/components/CustomMessage/LockedAttachment/components/_shared/TextShell.tsx +28 -0
  47. package/src/components/CustomMessage/LockedAttachment/components/_shared/usePreviewToggle.test.ts +67 -0
  48. package/src/components/CustomMessage/LockedAttachment/components/_shared/usePreviewToggle.ts +89 -0
  49. package/src/components/CustomMessage/LockedAttachment/components/_shared/useReceivedSource.ts +83 -0
  50. package/src/components/CustomMessage/LockedAttachment/index.tsx +105 -0
  51. package/src/components/CustomMessage/LockedAttachment/types.ts +44 -0
  52. package/src/components/CustomMessage/MessageTag.tsx +8 -0
  53. package/src/components/CustomMessage/context.tsx +1 -1
  54. package/src/components/CustomMessage/index.tsx +76 -42
  55. package/src/components/CustomMessageInput/CustomMessageInput.stories.tsx +5 -3
  56. package/src/index.ts +3 -8
  57. package/src/stream-custom-data.ts +1 -0
  58. package/src/styles.css +12 -0
  59. package/src/types.ts +35 -2
  60. package/dist/Card-B-L2MST4.js +0 -107
  61. package/dist/Card-B-L2MST4.js.map +0 -1
  62. package/dist/Card-B56QxJW3.cjs +0 -2
  63. package/dist/Card-B56QxJW3.cjs.map +0 -1
  64. package/dist/Card-Bqh-BQaX.js +0 -132
  65. package/dist/Card-Bqh-BQaX.js.map +0 -1
  66. package/dist/Card-ZCE6Lhfc.cjs +0 -2
  67. package/dist/Card-ZCE6Lhfc.cjs.map +0 -1
  68. package/dist/Card-g4JD-2ES.js +0 -163
  69. package/dist/Card-g4JD-2ES.js.map +0 -1
  70. package/dist/Card-wac6AMNc.cjs +0 -2
  71. package/dist/Card-wac6AMNc.cjs.map +0 -1
  72. package/dist/LockedThumbnail-BAQmoIff.js +0 -220
  73. package/dist/LockedThumbnail-BAQmoIff.js.map +0 -1
  74. package/dist/LockedThumbnail-Cr0Gqs3Y.cjs +0 -2
  75. package/dist/LockedThumbnail-Cr0Gqs3Y.cjs.map +0 -1
  76. package/dist/index-BPhZyaxT.cjs +0 -2
  77. package/dist/index-BPhZyaxT.cjs.map +0 -1
  78. package/dist/index-BxzzWK4K.js.map +0 -1
  79. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +0 -450
  80. package/src/components/LockedAttachment/components/Composer/Card.tsx +0 -221
  81. package/src/components/LockedAttachment/components/Composer/index.ts +0 -2
  82. package/src/components/LockedAttachment/components/Received/Card.tsx +0 -191
  83. package/src/components/LockedAttachment/components/Received/index.ts +0 -2
  84. package/src/components/LockedAttachment/components/Sent/Card.tsx +0 -177
  85. package/src/components/LockedAttachment/components/Sent/index.ts +0 -2
  86. package/src/components/LockedAttachment/components/_shared/LockBadge.tsx +0 -39
  87. package/src/components/LockedAttachment/index.tsx +0 -63
  88. package/src/components/LockedAttachment/types.ts +0 -51
@@ -1,450 +0,0 @@
1
- import type { Meta, StoryFn } from '@storybook/react'
2
- import React, { useState } from 'react'
3
-
4
- import type { LockedAttachmentGalleryItem } from './types'
5
-
6
- import LockedAttachment from '.'
7
-
8
- const VIDEO_THUMBNAIL_BLURRED = '/video-thumbnail-blurred.jpg'
9
- const VIDEO_THUMBNAIL = '/video-thumbnail.jpg'
10
- const VIDEO_SOURCE = '/video-source.mp4'
11
-
12
- const IMAGE_THUMBNAIL_BLURRED = '/image-thumbnail-blurred.jpg'
13
- const IMAGE_THUMBNAIL = '/image-thumbnail.jpg'
14
- const IMAGE_SOURCE = '/image-source.jpg'
15
-
16
- const DOCUMENT_THUMBNAIL_BLURRED = '/document-thumbnail-blurred.jpg'
17
- const DOCUMENT_THUMBNAIL = '/document-thumbnail.jpg'
18
- const DOCUMENT_SOURCE = '/document-source.pdf'
19
-
20
- const AUDIO_THUMBNAIL_BLURRED = '/audio-thumbnail-blurred.jpg'
21
- const AUDIO_THUMBNAIL = '/audio-thumbnail.jpg'
22
- const AUDIO_SOURCE = '/audio-source.mp3'
23
-
24
- const meta: Meta = {
25
- title: 'LockedAttachment',
26
- parameters: { layout: 'fullscreen' },
27
- }
28
- export default meta
29
-
30
- const VARIANTS = [
31
- {
32
- label: 'Video',
33
- title: "Alicia's Workout Plan",
34
- mimeType: 'video/mp4',
35
- detail: '1:20',
36
- thumbnailUrl: VIDEO_THUMBNAIL_BLURRED,
37
- thumbnailUnlockedUrl: VIDEO_THUMBNAIL,
38
- sourceUrl: VIDEO_SOURCE,
39
- },
40
- {
41
- label: 'Audio',
42
- title: 'Morning Meditation',
43
- mimeType: 'audio/mpeg',
44
- detail: '4:35',
45
- thumbnailUrl: AUDIO_THUMBNAIL_BLURRED,
46
- thumbnailUnlockedUrl: AUDIO_THUMBNAIL,
47
- sourceUrl: AUDIO_SOURCE,
48
- },
49
- {
50
- label: 'Image',
51
- title: 'Picture of my cat',
52
- mimeType: 'image/jpeg',
53
- detail: '3.2 MB',
54
- thumbnailUrl: IMAGE_THUMBNAIL_BLURRED,
55
- thumbnailUnlockedUrl: IMAGE_THUMBNAIL,
56
- sourceUrl: IMAGE_SOURCE,
57
- },
58
- {
59
- label: 'Document',
60
- title: 'Strength Training Guide',
61
- mimeType: 'application/zip',
62
- detail: '14 files',
63
- thumbnailUrl: DOCUMENT_THUMBNAIL_BLURRED,
64
- thumbnailUnlockedUrl: DOCUMENT_THUMBNAIL,
65
- sourceUrl: DOCUMENT_SOURCE,
66
- },
67
- {
68
- label: 'Unknown',
69
- title: 'Unknown Attachment',
70
- mimeType: 'application/octet-stream',
71
- detail: '2.4 MB',
72
- thumbnailUrl: undefined,
73
- thumbnailUnlockedUrl: undefined,
74
- sourceUrl: DOCUMENT_SOURCE,
75
- },
76
- ]
77
-
78
- /**
79
- * A mixed-media collection (a video + 3 photos) used by the Gallery stories
80
- * below. Each `LockedAttachment.*` card accepts a `gallery` prop and renders
81
- * a carousel of these items when 2+ items are provided.
82
- */
83
- const GALLERY_ITEMS: LockedAttachmentGalleryItem[] = [
84
- {
85
- mimeType: 'video/mp4',
86
- thumbnailUrl: VIDEO_THUMBNAIL_BLURRED,
87
- sourceUrl: VIDEO_SOURCE,
88
- },
89
- {
90
- mimeType: 'image/jpeg',
91
- thumbnailUrl: IMAGE_THUMBNAIL_BLURRED,
92
- sourceUrl: IMAGE_SOURCE,
93
- },
94
- {
95
- mimeType: 'image/jpeg',
96
- thumbnailUrl: DOCUMENT_THUMBNAIL_BLURRED,
97
- sourceUrl: DOCUMENT_SOURCE,
98
- },
99
- {
100
- mimeType: 'image/jpeg',
101
- thumbnailUrl: AUDIO_THUMBNAIL_BLURRED,
102
- sourceUrl: AUDIO_SOURCE,
103
- },
104
- ]
105
-
106
- const GALLERY_DETAIL = '1 video, 3 photos'
107
- const GALLERY_TITLE = 'Workout Guide'
108
-
109
- const Table = ({ children }: { children: React.ReactNode }) => (
110
- <div className="min-h-screen w-full p-12 bg-[#F9F7F4]">
111
- <table className="border-separate border-spacing-4">{children}</table>
112
- </div>
113
- )
114
-
115
- const TableHead = ({
116
- variants,
117
- }: {
118
- variants: { label: string; mimeType: string }[]
119
- }) => (
120
- <thead>
121
- <tr>
122
- <th className="text-left text-xs font-medium text-black/40 pb-2" />
123
- {variants.map(({ label, mimeType }) => (
124
- <th
125
- key={mimeType}
126
- className="text-left text-xs font-medium text-black/40 pb-2"
127
- >
128
- {label}
129
- </th>
130
- ))}
131
- </tr>
132
- </thead>
133
- )
134
-
135
- export const Composer: StoryFn = () => (
136
- <Table>
137
- <TableHead variants={VARIANTS} />
138
- <tbody>
139
- <tr>
140
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
141
- Preview
142
- </td>
143
- {VARIANTS.map(
144
- ({
145
- mimeType,
146
- detail,
147
- thumbnailUrl,
148
- thumbnailUnlockedUrl,
149
- sourceUrl,
150
- }) => (
151
- <td key={mimeType} className="align-top">
152
- <LockedAttachment.Composer
153
- placeholderTitle="Attachment title"
154
- placeholderAmountText="AU$0.00"
155
- thumbnailUrl={thumbnailUrl}
156
- mimeType={mimeType}
157
- detail={detail}
158
- onFetchSource={async () => {
159
- await new Promise((resolve) => setTimeout(resolve, 1500))
160
- return {
161
- sourceUrl: sourceUrl,
162
- thumbnailUrl: thumbnailUnlockedUrl,
163
- }
164
- }}
165
- />
166
- </td>
167
- )
168
- )}
169
- </tr>
170
- <tr>
171
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
172
- Pending
173
- </td>
174
- {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
175
- <td key={mimeType} className="align-top">
176
- <LockedAttachment.Composer
177
- title={title}
178
- thumbnailUrl={thumbnailUrl}
179
- mimeType={mimeType}
180
- detail={detail}
181
- amountText="AU$9.99"
182
- onDismiss={() => alert('Dismissed')}
183
- />
184
- </td>
185
- ))}
186
- </tr>
187
- </tbody>
188
- </Table>
189
- )
190
-
191
- export const Sent: StoryFn = () => (
192
- <Table>
193
- <TableHead variants={VARIANTS} />
194
- <tbody>
195
- <tr>
196
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
197
- Sent
198
- </td>
199
- {VARIANTS.map(
200
- ({
201
- title,
202
- mimeType,
203
- detail,
204
- thumbnailUrl,
205
- thumbnailUnlockedUrl,
206
- sourceUrl,
207
- }) => (
208
- <td key={mimeType} className="align-top">
209
- <LockedAttachment.Sent
210
- title={title}
211
- thumbnailUrl={thumbnailUrl}
212
- mimeType={mimeType}
213
- detail={detail}
214
- amountText="AU$9.99"
215
- onFetchSource={async () => {
216
- await new Promise((resolve) => setTimeout(resolve, 1500))
217
- return {
218
- sourceUrl: sourceUrl,
219
- thumbnailUrl: thumbnailUnlockedUrl,
220
- }
221
- }}
222
- />
223
- </td>
224
- )
225
- )}
226
- </tr>
227
- <tr>
228
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
229
- Sold
230
- </td>
231
- {VARIANTS.map(
232
- ({
233
- title,
234
- mimeType,
235
- detail,
236
- thumbnailUrl,
237
- thumbnailUnlockedUrl,
238
- sourceUrl,
239
- }) => (
240
- <td key={mimeType} className="align-top">
241
- <LockedAttachment.Sent
242
- title={title}
243
- thumbnailUrl={thumbnailUrl}
244
- mimeType={mimeType}
245
- detail={detail}
246
- amountText="AU$9.99"
247
- paymentStatus="paid"
248
- onFetchSource={async () => {
249
- await new Promise((resolve) => setTimeout(resolve, 1500))
250
- return {
251
- sourceUrl: sourceUrl,
252
- thumbnailUrl: thumbnailUnlockedUrl,
253
- }
254
- }}
255
- />
256
- </td>
257
- )
258
- )}
259
- </tr>
260
- </tbody>
261
- </Table>
262
- )
263
-
264
- export const Gallery: StoryFn = () => {
265
- const [isPaid, setPaid] = useState(false)
266
- const [isUnlocking, setUnlocking] = useState(false)
267
-
268
- return (
269
- <Table>
270
- <thead>
271
- <tr>
272
- <th className="text-left text-xs font-medium text-black/40 pb-2" />
273
- <th className="text-left text-xs font-medium text-black/40 pb-2">
274
- Composer
275
- </th>
276
- <th className="text-left text-xs font-medium text-black/40 pb-2">
277
- Sent
278
- </th>
279
- <th className="text-left text-xs font-medium text-black/40 pb-2">
280
- Received
281
- </th>
282
- </tr>
283
- </thead>
284
- <tbody>
285
- <tr>
286
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
287
- Locked
288
- </td>
289
- <td className="align-top">
290
- <LockedAttachment.Composer
291
- title={GALLERY_TITLE}
292
- detail={GALLERY_DETAIL}
293
- amountText="AU$50.00"
294
- gallery={GALLERY_ITEMS}
295
- onDismiss={() => alert('Dismissed gallery')}
296
- onEditClick={() => alert('Edit gallery')}
297
- />
298
- </td>
299
- <td className="align-top">
300
- <LockedAttachment.Sent
301
- title={GALLERY_TITLE}
302
- detail={GALLERY_DETAIL}
303
- amountText="AU$50.00"
304
- gallery={GALLERY_ITEMS}
305
- />
306
- </td>
307
- <td className="align-top">
308
- <LockedAttachment.Received
309
- title={GALLERY_TITLE}
310
- detail={GALLERY_DETAIL}
311
- amountText="AU$50.00"
312
- gallery={GALLERY_ITEMS}
313
- paymentStatus={isPaid ? 'paid' : undefined}
314
- isUnlocking={isUnlocking}
315
- onUnlockClick={() => {
316
- setUnlocking(true)
317
- setTimeout(() => {
318
- setUnlocking(false)
319
- setPaid(true)
320
- }, 1500)
321
- }}
322
- onDownloadClick={() => alert('Download gallery')}
323
- />
324
- </td>
325
- </tr>
326
- <tr>
327
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
328
- Sold / Purchased
329
- </td>
330
- <td className="align-top">
331
- <LockedAttachment.Composer
332
- title={GALLERY_TITLE}
333
- detail={GALLERY_DETAIL}
334
- amountText="AU$50.00"
335
- gallery={GALLERY_ITEMS}
336
- onEditClick={() => alert('Edit gallery')}
337
- />
338
- </td>
339
- <td className="align-top">
340
- <LockedAttachment.Sent
341
- title={GALLERY_TITLE}
342
- detail={GALLERY_DETAIL}
343
- amountText="AU$50.00"
344
- paymentStatus="paid"
345
- gallery={GALLERY_ITEMS}
346
- />
347
- </td>
348
- <td className="align-top">
349
- <LockedAttachment.Received
350
- title={GALLERY_TITLE}
351
- detail={GALLERY_DETAIL}
352
- amountText="AU$50.00"
353
- paymentStatus="paid"
354
- gallery={GALLERY_ITEMS}
355
- onDownloadClick={() => alert('Download gallery')}
356
- />
357
- </td>
358
- </tr>
359
- </tbody>
360
- </Table>
361
- )
362
- }
363
-
364
- export const Received: StoryFn = () => {
365
- const [isPaid, setPaid] = useState<string | undefined>()
366
- const [isUnlocking, setUnlocking] = useState<string | undefined>()
367
-
368
- return (
369
- <Table>
370
- <TableHead variants={VARIANTS} />
371
- <tbody>
372
- <tr>
373
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
374
- Locked
375
- </td>
376
- {VARIANTS.map(
377
- ({
378
- title,
379
- mimeType,
380
- detail,
381
- thumbnailUrl,
382
- thumbnailUnlockedUrl,
383
- sourceUrl,
384
- }) => (
385
- <td key={mimeType} className="align-top">
386
- <LockedAttachment.Received
387
- title={title}
388
- thumbnailUrl={thumbnailUrl}
389
- mimeType={mimeType}
390
- detail={detail}
391
- paymentStatus={isPaid === mimeType ? 'paid' : undefined}
392
- amountText="AU$9.99"
393
- isUnlocking={isUnlocking === mimeType}
394
- onUnlockClick={() => {
395
- setUnlocking(mimeType)
396
- setTimeout(() => {
397
- setUnlocking(undefined)
398
- setPaid(mimeType)
399
- }, 1500)
400
- }}
401
- onDownloadClick={() => alert('Download clicked')}
402
- onFetchSource={async () => ({
403
- sourceUrl: sourceUrl,
404
- thumbnailUrl: thumbnailUnlockedUrl,
405
- })}
406
- />
407
- </td>
408
- )
409
- )}
410
- </tr>
411
- <tr>
412
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
413
- Unlocked
414
- </td>
415
- {VARIANTS.map(
416
- ({
417
- title,
418
- mimeType,
419
- detail,
420
- thumbnailUrl,
421
- thumbnailUnlockedUrl,
422
- sourceUrl,
423
- }) => (
424
- <td key={mimeType} className="align-top">
425
- <LockedAttachment.Received
426
- title={title}
427
- thumbnailUrl={thumbnailUrl}
428
- mimeType={mimeType}
429
- detail={detail}
430
- amountText="AU$9.99"
431
- paymentStatus="paid"
432
- onUnlockClick={() => alert('Unlock clicked')}
433
- onDownloadClick={() => alert('Download clicked')}
434
- onFetchSource={async () => ({
435
- sourceUrl: sourceUrl,
436
- thumbnailUrl: thumbnailUnlockedUrl,
437
- })}
438
- />
439
- </td>
440
- )
441
- )}
442
- </tr>
443
- </tbody>
444
- </Table>
445
- )
446
- }
447
- // LockedAttachment.Received autoplays the video preview once paid; even with
448
- // our SMIL/CSS animation freeze the <video> element advances frames
449
- // independently and produces a non-deterministic snapshot. Skip in Chromatic.
450
- Received.parameters = { chromatic: { disableSnapshot: true } }
@@ -1,221 +0,0 @@
1
- import { ImagesIcon, PencilSimpleIcon, XIcon } from '@phosphor-icons/react'
2
- import React, { useCallback, useRef, useState } from 'react'
3
-
4
- import type {
5
- LockedAttachmentBaseProps,
6
- LockedAttachmentSource,
7
- } from '../../types'
8
- import CardBody from '../_shared/CardBody'
9
- import GalleryThumbnail from '../_shared/GalleryThumbnail'
10
- import LockedCardShell from '../_shared/LockedCardShell'
11
- import LockedThumbnail from '../_shared/LockedThumbnail'
12
-
13
- export interface ComposerCardProps extends LockedAttachmentBaseProps {
14
- /** Placeholder shown in the title slot before the composer types one. */
15
- placeholderTitle?: string
16
- /** Placeholder shown in the amount slot before one is configured. */
17
- placeholderAmountText?: string
18
- /**
19
- * When provided, renders a dismiss X in the thumbnail corner. Called when
20
- * the composer clicks it to remove the attachment.
21
- */
22
- onDismiss?: () => void
23
- /** Fired the first time the composer taps the thumbnail to preview. */
24
- onPreviewClick?: () => void
25
- /**
26
- * Lazily loads the underlying source so the composer can preview the
27
- * attachment they're about to send. Called the first time the thumbnail is
28
- * tapped; the returned source is cached and reused on subsequent toggles.
29
- */
30
- onFetchSource?: () => Promise<LockedAttachmentSource | void>
31
- /**
32
- * When provided, renders a pencil button in the body bottom-right that the
33
- * composer can use to edit the attachment metadata (e.g. open the price /
34
- * gallery editor). Matches the Composer "Button" instance in Figma.
35
- */
36
- onEditClick?: () => void
37
- }
38
-
39
- /**
40
- * The card the composer sees while drafting a paid attachment.
41
- * Matches the Composer column of the messaging design system in Figma.
42
- */
43
- const ComposerCard: React.FC<ComposerCardProps> = ({
44
- title,
45
- mimeType = 'application/octet-stream',
46
- thumbnailUrl,
47
- detail,
48
- amountText,
49
- placeholderTitle = 'Attachment title',
50
- placeholderAmountText,
51
- gallery,
52
- onDismiss,
53
- onPreviewClick,
54
- onFetchSource,
55
- onEditClick,
56
- }) => {
57
- const [source, setSource] = useState<LockedAttachmentSource | undefined>()
58
- const [isPreviewVisible, setIsPreviewVisible] = useState(false)
59
- const [isLoadingPreview, setIsLoadingPreview] = useState(false)
60
- const fetchingRef = useRef(false)
61
-
62
- const isGallery = (gallery?.length ?? 0) >= 2
63
-
64
- const handleToggle = useCallback(async () => {
65
- onPreviewClick?.()
66
- if (isPreviewVisible) {
67
- setIsPreviewVisible(false)
68
- return
69
- }
70
- // Gallery items carry their own per-item sources on the `gallery` prop, so
71
- // we just flip visibility — no async source fetch is needed.
72
- if (isGallery) {
73
- setIsPreviewVisible(true)
74
- return
75
- }
76
- if (source) {
77
- setIsPreviewVisible(true)
78
- return
79
- }
80
- if (!onFetchSource) return
81
- if (fetchingRef.current) return
82
- fetchingRef.current = true
83
- setIsLoadingPreview(true)
84
- try {
85
- const result = await onFetchSource()
86
- if (result) {
87
- setSource(result)
88
- setIsPreviewVisible(true)
89
- }
90
- } finally {
91
- fetchingRef.current = false
92
- setIsLoadingPreview(false)
93
- }
94
- }, [isPreviewVisible, isGallery, source, onPreviewClick, onFetchSource])
95
-
96
- // Gallery is always previewable in the composer because each item's source
97
- // is already provided up-front via `gallery[*].sourceUrl`.
98
- const togglePreview =
99
- isGallery || onFetchSource || onPreviewClick ? handleToggle : undefined
100
- const showLocked = !isPreviewVisible
101
- const isBusy = isLoadingPreview
102
-
103
- const statusBadge = (
104
- <React.Fragment>
105
- <span className="text-xs font-medium text-white/55">&bull;</span>
106
- <span
107
- className={
108
- amountText
109
- ? 'text-xs font-medium text-white/55'
110
- : 'text-xs font-medium text-white/30'
111
- }
112
- >
113
- {amountText || placeholderAmountText}
114
- </span>
115
- </React.Fragment>
116
- )
117
-
118
- const dismissButton = onDismiss ? (
119
- <button
120
- type="button"
121
- onClick={(e) => {
122
- // Stop the click from bubbling up into the outer preview-toggle
123
- // wrapper when both are wired up at the same time.
124
- e.stopPropagation()
125
- onDismiss()
126
- }}
127
- className="flex size-6 items-center justify-center rounded-full bg-[#121110] text-white"
128
- aria-label="Dismiss attachment"
129
- >
130
- <XIcon className="size-3" weight="bold" />
131
- </button>
132
- ) : undefined
133
-
134
- const editButton = onEditClick ? (
135
- <button
136
- type="button"
137
- onClick={onEditClick}
138
- aria-label="Edit attachment"
139
- className="flex size-10 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/15"
140
- >
141
- <PencilSimpleIcon className="size-5" weight="regular" />
142
- </button>
143
- ) : undefined
144
-
145
- const thumbnail = isGallery ? (
146
- <GalleryThumbnail
147
- variant="dark"
148
- gallery={gallery!}
149
- title={title}
150
- showLocked={showLocked}
151
- topRight={dismissButton}
152
- />
153
- ) : (
154
- <LockedThumbnail
155
- variant="dark"
156
- mimeType={mimeType}
157
- thumbnailUrl={thumbnailUrl}
158
- title={title}
159
- source={source}
160
- showLocked={showLocked}
161
- topRight={dismissButton}
162
- />
163
- )
164
-
165
- return (
166
- <LockedCardShell variant="dark">
167
- {togglePreview ? (
168
- // Uses a `<div role="button">` rather than a native `<button>` so the
169
- // dismiss control rendered inside `thumbnail` (also a `<button>`)
170
- // doesn't produce invalid nested interactive elements.
171
- <div
172
- role="button"
173
- tabIndex={isBusy ? -1 : 0}
174
- aria-label="Toggle preview"
175
- aria-busy={isBusy}
176
- aria-pressed={!showLocked}
177
- aria-disabled={isBusy || undefined}
178
- onClick={isBusy ? undefined : togglePreview}
179
- onKeyDown={(e) => {
180
- if (isBusy) return
181
- // Only handle keys that originate on the wrapper itself, not on
182
- // inner interactive elements (dismiss X, carousel arrows). Without
183
- // this guard, pressing Enter/Space on a focused child would bubble
184
- // up and toggle the preview in addition to activating the child.
185
- if (e.target !== e.currentTarget) return
186
- if (e.key === 'Enter' || e.key === ' ') {
187
- e.preventDefault()
188
- void togglePreview()
189
- }
190
- }}
191
- className={
192
- !isBusy
193
- ? 'block w-full cursor-pointer text-left'
194
- : 'block w-full text-left'
195
- }
196
- >
197
- {thumbnail}
198
- </div>
199
- ) : (
200
- thumbnail
201
- )}
202
-
203
- <CardBody
204
- variant="dark"
205
- title={title}
206
- placeholderTitle={placeholderTitle}
207
- mimeType={mimeType}
208
- detail={detail}
209
- statusBadge={statusBadge}
210
- icon={
211
- isGallery ? (
212
- <ImagesIcon className="size-5 shrink-0 text-white/55" />
213
- ) : undefined
214
- }
215
- trailingAction={editButton}
216
- />
217
- </LockedCardShell>
218
- )
219
- }
220
-
221
- export default ComposerCard
@@ -1,2 +0,0 @@
1
- export { default as ComposerCard } from './Card'
2
- export type { ComposerCardProps } from './Card'