@linktr.ee/messaging-react 3.10.0-rc-1783660962 → 3.10.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 (86) hide show
  1. package/dist/Card-B-L2MST4.js +107 -0
  2. package/dist/Card-B-L2MST4.js.map +1 -0
  3. package/dist/Card-B56QxJW3.cjs +2 -0
  4. package/dist/Card-B56QxJW3.cjs.map +1 -0
  5. package/dist/Card-Bqh-BQaX.js +132 -0
  6. package/dist/Card-Bqh-BQaX.js.map +1 -0
  7. package/dist/Card-ZCE6Lhfc.cjs +2 -0
  8. package/dist/Card-ZCE6Lhfc.cjs.map +1 -0
  9. package/dist/Card-g4JD-2ES.js +163 -0
  10. package/dist/Card-g4JD-2ES.js.map +1 -0
  11. package/dist/Card-wac6AMNc.cjs +2 -0
  12. package/dist/Card-wac6AMNc.cjs.map +1 -0
  13. package/dist/LockedThumbnail-BAQmoIff.js +220 -0
  14. package/dist/LockedThumbnail-BAQmoIff.js.map +1 -0
  15. package/dist/LockedThumbnail-Cr0Gqs3Y.cjs +2 -0
  16. package/dist/LockedThumbnail-Cr0Gqs3Y.cjs.map +1 -0
  17. package/dist/assets/index.css +1 -1
  18. package/dist/index-BPhZyaxT.cjs +2 -0
  19. package/dist/index-BPhZyaxT.cjs.map +1 -0
  20. package/dist/{index-DFZrAiLB.js → index-BxzzWK4K.js} +1249 -1276
  21. package/dist/index-BxzzWK4K.js.map +1 -0
  22. package/dist/index.cjs +1 -1
  23. package/dist/index.d.ts +101 -164
  24. package/dist/index.js +1 -1
  25. package/dist/testing.d.ts +0 -1
  26. package/package.json +2 -2
  27. package/src/components/ChannelList/CustomChannelPreview.test.tsx +0 -95
  28. package/src/components/ChannelList/CustomChannelPreview.tsx +9 -21
  29. package/src/components/CustomMessage/CustomMessage.stories.tsx +1 -54
  30. package/src/components/CustomMessage/MessageTag.tsx +0 -8
  31. package/src/components/CustomMessage/context.tsx +1 -1
  32. package/src/components/CustomMessage/index.tsx +42 -76
  33. package/src/components/CustomMessageInput/CustomMessageInput.stories.tsx +3 -5
  34. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +450 -0
  35. package/src/components/LockedAttachment/components/Composer/Card.tsx +221 -0
  36. package/src/components/LockedAttachment/components/Composer/index.ts +2 -0
  37. package/src/components/LockedAttachment/components/Received/Card.tsx +191 -0
  38. package/src/components/{CustomMessage/LockedAttachment/components/Media → LockedAttachment/components/Received}/CardActions.tsx +2 -9
  39. package/src/components/LockedAttachment/components/Received/index.ts +2 -0
  40. package/src/components/LockedAttachment/components/Sent/Card.tsx +177 -0
  41. package/src/components/LockedAttachment/components/Sent/index.ts +2 -0
  42. package/src/components/{CustomMessage/LockedAttachment → LockedAttachment}/components/_shared/CardBody.tsx +7 -8
  43. package/src/components/{CustomMessage/LockedAttachment → LockedAttachment}/components/_shared/GalleryThumbnail.tsx +39 -31
  44. package/src/components/LockedAttachment/components/_shared/LockBadge.tsx +39 -0
  45. package/src/components/{CustomMessage/LockedAttachment/components/_shared/MediaShell.tsx → LockedAttachment/components/_shared/LockedCardShell.tsx} +8 -8
  46. package/src/components/{CustomMessage/LockedAttachment/components/_shared/SingleThumbnail.tsx → LockedAttachment/components/_shared/LockedThumbnail.tsx} +7 -11
  47. package/src/components/LockedAttachment/index.tsx +63 -0
  48. package/src/components/LockedAttachment/types.ts +51 -0
  49. package/src/index.ts +8 -3
  50. package/src/stream-custom-data.ts +0 -1
  51. package/src/styles.css +0 -12
  52. package/src/types.ts +1 -1
  53. package/dist/Card-415OfRhw.cjs +0 -2
  54. package/dist/Card-415OfRhw.cjs.map +0 -1
  55. package/dist/Card-CyWdUMCB.cjs +0 -2
  56. package/dist/Card-CyWdUMCB.cjs.map +0 -1
  57. package/dist/Card-V5d6qhDT.js +0 -694
  58. package/dist/Card-V5d6qhDT.js.map +0 -1
  59. package/dist/Card-qS17qnN0.js +0 -103
  60. package/dist/Card-qS17qnN0.js.map +0 -1
  61. package/dist/LoadingDots-Ct3WmVlT.js +0 -59
  62. package/dist/LoadingDots-Ct3WmVlT.js.map +0 -1
  63. package/dist/LoadingDots-D9GGphgM.cjs +0 -2
  64. package/dist/LoadingDots-D9GGphgM.cjs.map +0 -1
  65. package/dist/index-Bx_zjNMR.cjs +0 -2
  66. package/dist/index-Bx_zjNMR.cjs.map +0 -1
  67. package/dist/index-DFZrAiLB.js.map +0 -1
  68. package/src/components/CustomMessage/LockedAttachment/LockedAttachment.stories.tsx +0 -541
  69. package/src/components/CustomMessage/LockedAttachment/LockedAttachment.test.tsx +0 -117
  70. package/src/components/CustomMessage/LockedAttachment/components/Media/Card.tsx +0 -358
  71. package/src/components/CustomMessage/LockedAttachment/components/Media/types.ts +0 -79
  72. package/src/components/CustomMessage/LockedAttachment/components/Text/Card.tsx +0 -153
  73. package/src/components/CustomMessage/LockedAttachment/components/Text/types.ts +0 -58
  74. package/src/components/CustomMessage/LockedAttachment/components/_shared/CardThumbnail.tsx +0 -92
  75. package/src/components/CustomMessage/LockedAttachment/components/_shared/LoadingDots.tsx +0 -36
  76. package/src/components/CustomMessage/LockedAttachment/components/_shared/LockBadge.tsx +0 -63
  77. package/src/components/CustomMessage/LockedAttachment/components/_shared/PreviewToggle.tsx +0 -58
  78. package/src/components/CustomMessage/LockedAttachment/components/_shared/PriceLockRow.tsx +0 -21
  79. package/src/components/CustomMessage/LockedAttachment/components/_shared/PurchaseStatusBadge.tsx +0 -74
  80. package/src/components/CustomMessage/LockedAttachment/components/_shared/SkeletonTextLines.tsx +0 -29
  81. package/src/components/CustomMessage/LockedAttachment/components/_shared/TextShell.tsx +0 -28
  82. package/src/components/CustomMessage/LockedAttachment/components/_shared/usePreviewToggle.test.ts +0 -67
  83. package/src/components/CustomMessage/LockedAttachment/components/_shared/usePreviewToggle.ts +0 -89
  84. package/src/components/CustomMessage/LockedAttachment/components/_shared/useReceivedSource.ts +0 -83
  85. package/src/components/CustomMessage/LockedAttachment/index.tsx +0 -105
  86. package/src/components/CustomMessage/LockedAttachment/types.ts +0 -44
@@ -32,17 +32,17 @@ import {
32
32
  import { useMessageVote } from '../../hooks/useMessageVote'
33
33
  import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
34
34
  import { Avatar } from '../Avatar'
35
+ import LockedAttachment from '../LockedAttachment'
35
36
  import { isLinkAttachment } from '../MediaMessage'
36
- import { bubbleGroupPositionFromStream as messageAttachmentGroupPositionFromStream } from '../MessageAttachment'
37
+ import {
38
+ bubbleGroupPositionFromStream as messageAttachmentGroupPositionFromStream,
39
+ } from '../MessageAttachment'
37
40
 
38
41
  import { useCustomMessage } from './context'
39
- import LockedAttachment from './LockedAttachment'
40
42
  import {
41
43
  MessageTag,
42
44
  isAttachmentMessage,
43
45
  isChatbotMessage,
44
- isMediaAttachmentMessage,
45
- isTextAttachmentMessage,
46
46
  isTipOnlyMessage,
47
47
  } from './MessageTag'
48
48
  import { MessageVoteButtons } from './MessageVoteButtons'
@@ -145,12 +145,13 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
145
145
  (total, segment) => total + segment.attachments.length,
146
146
  0
147
147
  ) === (finalAttachments?.length ?? 0)
148
- const streamAttachmentGroupPosition =
149
- messageAttachmentGroupPositionFromStream({
148
+ const streamAttachmentGroupPosition = messageAttachmentGroupPositionFromStream(
149
+ {
150
150
  endOfGroup,
151
151
  firstOfGroup,
152
152
  groupedByUser,
153
- })
153
+ }
154
+ )
154
155
 
155
156
  if (isDateSeparatorMessage(message)) {
156
157
  return null
@@ -225,26 +226,6 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
225
226
  !isAIGenerated &&
226
227
  !isChatbot
227
228
 
228
- const sharedLockedTextProps = {
229
- contentType: 'text' as const,
230
- amountText: message.metadata?.amount_text,
231
- paymentStatus: message.metadata?.payment_status,
232
- renderedText: message.text && (
233
- <MessageText message={displayMessage} renderText={renderText} />
234
- ),
235
- }
236
- const sharedLockedMediaProps = {
237
- contentType: 'media' as const,
238
- title: message.metadata?.attachment_title,
239
- mimeType: message.metadata?.attachment_mime_type,
240
- thumbnailUrl: message.metadata?.attachment_thumbnail,
241
- amountText: message.metadata?.amount_text,
242
- detail: message.metadata?.attachment_detail,
243
- paymentStatus: message.metadata?.payment_status,
244
- onFetchSource: onFetchSource
245
- ? () => onFetchSource(message, channel)
246
- : undefined,
247
- }
248
229
  return (
249
230
  <>
250
231
  {editing && (
@@ -295,61 +276,43 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
295
276
  marginInlineStart: 0,
296
277
  }}
297
278
  >
298
- {isTipOnly ? (
299
- <MessageTag message={message} standalone />
300
- ) : isTextAttachmentMessage(message) ? (
279
+ {isAttachment ? (
301
280
  <div className="str-chat__message-bubble-wrapper">
302
- <div className="flex items-center gap-2">
303
- {isMine && MessageActions && <MessageActions />}
304
- {isMine ? (
305
- <LockedAttachment
306
- {...sharedLockedTextProps}
307
- isMine={true}
308
- />
309
- ) : (
310
- <LockedAttachment
311
- {...sharedLockedTextProps}
312
- isMine={false}
313
- isUnlocking={isUnlocking(message.id)}
314
- onUnlockClick={() => onUnlockClick?.(message, channel)}
315
- />
316
- )}
317
- {!isMine && MessageActions && <MessageActions />}
318
- </div>
319
- </div>
320
- ) : isMediaAttachmentMessage(message) ? (
321
- <div className="str-chat__message-bubble-wrapper">
322
- <div className="flex items-center gap-2">
323
- {isMine && MessageActions && <MessageActions />}
324
- {isMine ? (
325
- <LockedAttachment
326
- {...sharedLockedMediaProps}
327
- isMine={true}
328
- onPreviewClick={() =>
329
- onUnlockClick
330
- ? onUnlockClick(message, channel)
331
- : undefined
281
+ {isMine ? (
282
+ <div className="flex items-center gap-2">
283
+ {MessageActions && <MessageActions />}
284
+ <LockedAttachment.Sent
285
+ title={message.metadata?.attachment_title}
286
+ mimeType={message.metadata?.attachment_mime_type}
287
+ thumbnailUrl={message.metadata?.attachment_thumbnail}
288
+ amountText={message.metadata?.amount_text}
289
+ detail={message.metadata?.attachment_detail}
290
+ paymentStatus={message.metadata?.payment_status}
291
+ onPreviewClick={() => onUnlockClick?.(message, channel)}
292
+ onFetchSource={async () =>
293
+ await onFetchSource?.(message, channel)
332
294
  }
333
295
  />
334
- ) : (
335
- <LockedAttachment
336
- {...sharedLockedMediaProps}
337
- isMine={false}
296
+ </div>
297
+ ) : (
298
+ <div className="flex items-center gap-2">
299
+ <LockedAttachment.Received
300
+ title={message.metadata?.attachment_title}
301
+ mimeType={message.metadata?.attachment_mime_type}
302
+ thumbnailUrl={message.metadata?.attachment_thumbnail}
303
+ amountText={message.metadata?.amount_text}
304
+ detail={message.metadata?.attachment_detail}
305
+ paymentStatus={message.metadata?.payment_status}
338
306
  isUnlocking={isUnlocking(message.id)}
339
- onUnlockClick={() =>
340
- onUnlockClick
341
- ? onUnlockClick(message, channel)
342
- : undefined
343
- }
344
- onDownloadClick={() =>
345
- onDownloadClick
346
- ? onDownloadClick(message, channel)
347
- : undefined
307
+ onUnlockClick={() => onUnlockClick?.(message, channel)}
308
+ onFetchSource={async () =>
309
+ await onFetchSource?.(message, channel)
348
310
  }
311
+ onDownloadClick={() => onDownloadClick?.(message, channel)}
349
312
  />
350
- )}
351
- {!isMine && MessageActions && <MessageActions />}
352
- </div>
313
+ {MessageActions && <MessageActions />}
314
+ </div>
315
+ )}
353
316
  {message.text && (
354
317
  <div className="str-chat__message-bubble">
355
318
  <MessageText
@@ -359,6 +322,9 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
359
322
  </div>
360
323
  )}
361
324
  </div>
325
+ ) : isTipOnly ? (
326
+ /* Tip-only messages render as a standalone bubble */
327
+ <MessageTag message={message} standalone />
362
328
  ) : canRenderAttachmentsInToolkit ? (
363
329
  <StreamAttachmentMessage
364
330
  groupPosition={streamAttachmentGroupPosition}
@@ -5,7 +5,7 @@ import { QueryChannelAPIResponse, StreamChat } from 'stream-chat'
5
5
  import { Channel, Chat, type SendButtonProps } from 'stream-chat-react'
6
6
 
7
7
  import { createMockStreamChatClient } from '../../testing/createMockStreamChatClient'
8
- import LockedAttachment from '../CustomMessage/LockedAttachment'
8
+ import LockedAttachment from '../LockedAttachment'
9
9
 
10
10
  import { CustomMessageInput } from '.'
11
11
 
@@ -229,9 +229,7 @@ WithAttachments.parameters = {
229
229
  export const WithPaidAttachment: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
230
230
  WithPaidAttachment.args = {
231
231
  attachmentPreviewList: () => (
232
- <LockedAttachment
233
- isPreview
234
- contentType="media"
232
+ <LockedAttachment.Composer
235
233
  title="Exclusive Content"
236
234
  mimeType="image/jpeg"
237
235
  thumbnailUrl="https://picsum.photos/seed/paid/320/180"
@@ -268,7 +266,7 @@ WithPaidAttachment.parameters = {
268
266
  docs: {
269
267
  description: {
270
268
  story:
271
- "Message input with a paid attachment staged inside the input bubble. Uses `Channel`'s `AttachmentPreviewList` override to render a `LockedAttachment isPreview` card at the top of the input. `onDismiss` removes it; `onEditClick` reopens the paid content wizard.",
269
+ "Message input with a paid attachment staged inside the input bubble. Uses `Channel`'s `AttachmentPreviewList` override to render a `LockedAttachment.Composer` card at the top of the input. `onDismiss` removes it; `onEditClick` reopens the paid content wizard.",
272
270
  },
273
271
  },
274
272
  }
@@ -0,0 +1,450 @@
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 } }