@linktr.ee/messaging-react 1.40.2 → 2.0.1-rc-1778656305
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.
- package/dist/Card-CAC3fPjy.js +107 -0
- package/dist/Card-CAC3fPjy.js.map +1 -0
- package/dist/Card-DLUBUg_w.js +132 -0
- package/dist/Card-DLUBUg_w.js.map +1 -0
- package/dist/Card-_StSlnYh.js +163 -0
- package/dist/Card-_StSlnYh.js.map +1 -0
- package/dist/LockedThumbnail-p5RsFOug.js +220 -0
- package/dist/LockedThumbnail-p5RsFOug.js.map +1 -0
- package/dist/assets/index.css +1 -1
- package/dist/index-B1h46F9x.js +3092 -0
- package/dist/index-B1h46F9x.js.map +1 -0
- package/dist/index.d.ts +109 -30
- package/dist/index.js +14 -12
- package/package.json +2 -2
- package/src/components/ChannelInfoDialog/ChannelInfoDialog.test.tsx +2 -14
- package/src/components/ChannelInfoDialog/index.tsx +4 -8
- package/src/components/ChannelList/ChannelListContext.tsx +2 -0
- package/src/components/ChannelList/CustomChannelPreview.tsx +14 -3
- package/src/components/ChannelList/index.tsx +9 -1
- package/src/components/ChannelView.test.tsx +11 -0
- package/src/components/ChannelView.tsx +44 -33
- package/src/components/CustomMessage/index.tsx +24 -7
- package/src/components/CustomTypingIndicator/CustomTypingIndicator.stories.tsx +57 -17
- package/src/components/CustomTypingIndicator/CustomTypingIndicator.test.tsx +187 -0
- package/src/components/CustomTypingIndicator/DmAgentContext.ts +3 -0
- package/src/components/CustomTypingIndicator/index.tsx +101 -37
- package/src/components/LockedAttachment/LockedAttachment.stories.tsx +230 -89
- package/src/components/LockedAttachment/components/Composer/Card.tsx +221 -0
- package/src/components/LockedAttachment/components/Composer/index.ts +2 -0
- package/src/components/LockedAttachment/components/Received/Card.tsx +191 -0
- package/src/components/LockedAttachment/components/Received/CardActions.tsx +91 -0
- package/src/components/LockedAttachment/components/Received/index.ts +2 -0
- package/src/components/LockedAttachment/components/Sent/Card.tsx +177 -0
- package/src/components/LockedAttachment/components/Sent/index.ts +2 -0
- package/src/components/LockedAttachment/components/_shared/CardBody.tsx +94 -0
- package/src/components/LockedAttachment/components/_shared/GalleryThumbnail.tsx +178 -0
- package/src/components/LockedAttachment/components/_shared/LockBadge.tsx +39 -0
- package/src/components/LockedAttachment/components/_shared/LockedCardShell.tsx +36 -0
- package/src/components/LockedAttachment/components/_shared/LockedThumbnail.tsx +128 -0
- package/src/components/LockedAttachment/index.tsx +43 -12
- package/src/components/LockedAttachment/types.ts +17 -0
- package/src/components/MediaMessage/index.tsx +2 -2
- package/src/components/MessagingShell/index.tsx +4 -4
- package/src/index.ts +18 -2
- package/src/stories/mocks.tsx +2 -9
- package/src/styles.css +7 -0
- package/src/types.ts +11 -1
- package/src/utils/getMessageDisplayText.test.ts +44 -0
- package/src/utils/getMessageDisplayText.ts +27 -0
- package/dist/Card-A0lkei-S.js +0 -138
- package/dist/Card-A0lkei-S.js.map +0 -1
- package/dist/Card-DXoAKkv0.js +0 -127
- package/dist/Card-DXoAKkv0.js.map +0 -1
- package/dist/index-B_PLgcDi.js +0 -2994
- package/dist/index-B_PLgcDi.js.map +0 -1
- package/src/components/LockedAttachment/components/Creator/Card.tsx +0 -210
- package/src/components/LockedAttachment/components/Creator/index.tsx +0 -2
- package/src/components/LockedAttachment/components/Visitor/Card.tsx +0 -155
- package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +0 -62
- package/src/components/LockedAttachment/components/Visitor/LockBadge.tsx +0 -12
- package/src/components/LockedAttachment/components/Visitor/index.ts +0 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
2
|
import React, { useState } from 'react'
|
|
3
3
|
|
|
4
|
+
import type { LockedAttachmentGalleryItem } from './types'
|
|
5
|
+
|
|
4
6
|
import LockedAttachment from '.'
|
|
5
7
|
|
|
6
8
|
const VIDEO_THUMBNAIL_BLURRED = '/video-thumbnail-blurred.jpg'
|
|
@@ -73,6 +75,37 @@ const VARIANTS = [
|
|
|
73
75
|
},
|
|
74
76
|
]
|
|
75
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
|
+
|
|
76
109
|
const Table = ({ children }: { children: React.ReactNode }) => (
|
|
77
110
|
<div className="min-h-screen w-full p-12 bg-[#F9F7F4]">
|
|
78
111
|
<table className="border-separate border-spacing-4">{children}</table>
|
|
@@ -99,91 +132,7 @@ const TableHead = ({
|
|
|
99
132
|
</thead>
|
|
100
133
|
)
|
|
101
134
|
|
|
102
|
-
export const
|
|
103
|
-
const [isPaid, setPaid] = useState<string | undefined>()
|
|
104
|
-
const [isUnlocking, setUnlocking] = useState<string | undefined>()
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<Table>
|
|
108
|
-
<TableHead variants={VARIANTS} />
|
|
109
|
-
<tbody>
|
|
110
|
-
<tr>
|
|
111
|
-
<td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
|
|
112
|
-
Locked
|
|
113
|
-
</td>
|
|
114
|
-
{VARIANTS.map(
|
|
115
|
-
({
|
|
116
|
-
title,
|
|
117
|
-
mimeType,
|
|
118
|
-
detail,
|
|
119
|
-
thumbnailUrl,
|
|
120
|
-
thumbnailUnlockedUrl,
|
|
121
|
-
sourceUrl,
|
|
122
|
-
}) => (
|
|
123
|
-
<td key={mimeType} className="align-top">
|
|
124
|
-
<LockedAttachment.Visitor
|
|
125
|
-
title={title}
|
|
126
|
-
thumbnailUrl={thumbnailUrl}
|
|
127
|
-
mimeType={mimeType}
|
|
128
|
-
detail={detail}
|
|
129
|
-
paymentStatus={isPaid === mimeType ? 'paid' : undefined}
|
|
130
|
-
amountText="AU$9.99"
|
|
131
|
-
isUnlocking={isUnlocking === mimeType}
|
|
132
|
-
onUnlockClick={() => {
|
|
133
|
-
setUnlocking(mimeType)
|
|
134
|
-
setTimeout(() => {
|
|
135
|
-
setUnlocking(undefined)
|
|
136
|
-
setPaid(mimeType)
|
|
137
|
-
}, 1500)
|
|
138
|
-
}}
|
|
139
|
-
onDownloadClick={() => alert('Download clicked')}
|
|
140
|
-
onFetchSource={async () => ({
|
|
141
|
-
sourceUrl: sourceUrl,
|
|
142
|
-
thumbnailUrl: thumbnailUnlockedUrl,
|
|
143
|
-
})}
|
|
144
|
-
/>
|
|
145
|
-
</td>
|
|
146
|
-
)
|
|
147
|
-
)}
|
|
148
|
-
</tr>
|
|
149
|
-
<tr>
|
|
150
|
-
<td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
|
|
151
|
-
Unlocked
|
|
152
|
-
</td>
|
|
153
|
-
{VARIANTS.map(
|
|
154
|
-
({
|
|
155
|
-
title,
|
|
156
|
-
mimeType,
|
|
157
|
-
detail,
|
|
158
|
-
thumbnailUrl,
|
|
159
|
-
thumbnailUnlockedUrl,
|
|
160
|
-
sourceUrl,
|
|
161
|
-
}) => (
|
|
162
|
-
<td key={mimeType} className="align-top">
|
|
163
|
-
<LockedAttachment.Visitor
|
|
164
|
-
title={title}
|
|
165
|
-
thumbnailUrl={thumbnailUrl}
|
|
166
|
-
mimeType={mimeType}
|
|
167
|
-
detail={detail}
|
|
168
|
-
amountText="AU$9.99"
|
|
169
|
-
paymentStatus="paid"
|
|
170
|
-
onUnlockClick={() => alert('Unlock clicked')}
|
|
171
|
-
onDownloadClick={() => alert('Download clicked')}
|
|
172
|
-
onFetchSource={async () => ({
|
|
173
|
-
sourceUrl: sourceUrl,
|
|
174
|
-
thumbnailUrl: thumbnailUnlockedUrl,
|
|
175
|
-
})}
|
|
176
|
-
/>
|
|
177
|
-
</td>
|
|
178
|
-
)
|
|
179
|
-
)}
|
|
180
|
-
</tr>
|
|
181
|
-
</tbody>
|
|
182
|
-
</Table>
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export const Sent: StoryFn = () => (
|
|
135
|
+
export const Composer: StoryFn = () => (
|
|
187
136
|
<Table>
|
|
188
137
|
<TableHead variants={VARIANTS} />
|
|
189
138
|
<tbody>
|
|
@@ -200,7 +149,7 @@ export const Sent: StoryFn = () => (
|
|
|
200
149
|
sourceUrl,
|
|
201
150
|
}) => (
|
|
202
151
|
<td key={mimeType} className="align-top">
|
|
203
|
-
<LockedAttachment.
|
|
152
|
+
<LockedAttachment.Composer
|
|
204
153
|
placeholderTitle="Attachment title"
|
|
205
154
|
placeholderAmountText="AU$0.00"
|
|
206
155
|
thumbnailUrl={thumbnailUrl}
|
|
@@ -224,7 +173,7 @@ export const Sent: StoryFn = () => (
|
|
|
224
173
|
</td>
|
|
225
174
|
{VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
|
|
226
175
|
<td key={mimeType} className="align-top">
|
|
227
|
-
<LockedAttachment.
|
|
176
|
+
<LockedAttachment.Composer
|
|
228
177
|
title={title}
|
|
229
178
|
thumbnailUrl={thumbnailUrl}
|
|
230
179
|
mimeType={mimeType}
|
|
@@ -235,6 +184,14 @@ export const Sent: StoryFn = () => (
|
|
|
235
184
|
</td>
|
|
236
185
|
))}
|
|
237
186
|
</tr>
|
|
187
|
+
</tbody>
|
|
188
|
+
</Table>
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
export const Sent: StoryFn = () => (
|
|
192
|
+
<Table>
|
|
193
|
+
<TableHead variants={VARIANTS} />
|
|
194
|
+
<tbody>
|
|
238
195
|
<tr>
|
|
239
196
|
<td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
|
|
240
197
|
Sent
|
|
@@ -249,7 +206,7 @@ export const Sent: StoryFn = () => (
|
|
|
249
206
|
sourceUrl,
|
|
250
207
|
}) => (
|
|
251
208
|
<td key={mimeType} className="align-top">
|
|
252
|
-
<LockedAttachment.
|
|
209
|
+
<LockedAttachment.Sent
|
|
253
210
|
title={title}
|
|
254
211
|
thumbnailUrl={thumbnailUrl}
|
|
255
212
|
mimeType={mimeType}
|
|
@@ -281,7 +238,7 @@ export const Sent: StoryFn = () => (
|
|
|
281
238
|
sourceUrl,
|
|
282
239
|
}) => (
|
|
283
240
|
<td key={mimeType} className="align-top">
|
|
284
|
-
<LockedAttachment.
|
|
241
|
+
<LockedAttachment.Sent
|
|
285
242
|
title={title}
|
|
286
243
|
thumbnailUrl={thumbnailUrl}
|
|
287
244
|
mimeType={mimeType}
|
|
@@ -303,3 +260,187 @@ export const Sent: StoryFn = () => (
|
|
|
303
260
|
</tbody>
|
|
304
261
|
</Table>
|
|
305
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
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
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">•</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
|