@linktr.ee/messaging-react 1.26.1 → 1.28.0-rc-1776225927

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/Creator-D38dWn2X.js +318 -0
  2. package/dist/Creator-D38dWn2X.js.map +1 -0
  3. package/dist/MediaPlayer-DE9MC6k6.js +599 -0
  4. package/dist/MediaPlayer-DE9MC6k6.js.map +1 -0
  5. package/dist/Preview-DqAv16NS.js +87 -0
  6. package/dist/Preview-DqAv16NS.js.map +1 -0
  7. package/dist/Visitor-BG-9-3HU.js +199 -0
  8. package/dist/Visitor-BG-9-3HU.js.map +1 -0
  9. package/dist/dash.all.min-Duv4lvGS.js +18858 -0
  10. package/dist/dash.all.min-Duv4lvGS.js.map +1 -0
  11. package/dist/hls-Bogc7CBn.js +21710 -0
  12. package/dist/hls-Bogc7CBn.js.map +1 -0
  13. package/dist/index-Da-xN4Yq.js +16142 -0
  14. package/dist/index-Da-xN4Yq.js.map +1 -0
  15. package/dist/index-Dj9rqWcU.js +69 -0
  16. package/dist/index-Dj9rqWcU.js.map +1 -0
  17. package/dist/index.d.ts +74 -10
  18. package/dist/index.js +979 -934
  19. package/dist/index.js.map +1 -1
  20. package/dist/mixin-B6jYfIcp.js +808 -0
  21. package/dist/mixin-B6jYfIcp.js.map +1 -0
  22. package/dist/react-BxlQMOfz.js +419 -0
  23. package/dist/react-BxlQMOfz.js.map +1 -0
  24. package/dist/react-COAP-MIW.js +377 -0
  25. package/dist/react-COAP-MIW.js.map +1 -0
  26. package/dist/react-Cn4WlMcl.js +3108 -0
  27. package/dist/react-Cn4WlMcl.js.map +1 -0
  28. package/dist/react-CwTJArKY.js +459 -0
  29. package/dist/react-CwTJArKY.js.map +1 -0
  30. package/dist/react-DkfS_atT.js +373 -0
  31. package/dist/react-DkfS_atT.js.map +1 -0
  32. package/dist/react-Pea5fum1.js +286 -0
  33. package/dist/react-Pea5fum1.js.map +1 -0
  34. package/dist/react-RiBbsUDd.js +534 -0
  35. package/dist/react-RiBbsUDd.js.map +1 -0
  36. package/dist/react-dS1WBxxz.js +238 -0
  37. package/dist/react-dS1WBxxz.js.map +1 -0
  38. package/package.json +2 -1
  39. package/src/components/ChannelView.tsx +12 -2
  40. package/src/components/CustomMessage/CustomMessage.stories.tsx +173 -41
  41. package/src/components/CustomMessage/MessageTag.tsx +5 -0
  42. package/src/components/CustomMessage/index.tsx +43 -4
  43. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +343 -0
  44. package/src/components/LockedAttachment/components/Creator.tsx +469 -0
  45. package/src/components/LockedAttachment/components/MediaPlayer.tsx +359 -0
  46. package/src/components/LockedAttachment/components/Visitor.tsx +356 -0
  47. package/src/components/LockedAttachment/index.tsx +39 -0
  48. package/src/components/LockedAttachment/types.ts +17 -0
  49. package/src/components/LockedAttachment/utils/icons.ts +53 -0
  50. package/src/components/LockedAttachment/utils/mimeType.test.ts +119 -0
  51. package/src/components/LockedAttachment/utils/mimeType.ts +37 -0
  52. package/src/components/ParticipantPicker/index.tsx +8 -1
  53. package/src/hooks/useParticipants.ts +3 -2
  54. package/src/index.ts +4 -0
  55. package/src/stories/decorators/storyUser.tsx +37 -0
  56. package/src/stream-custom-data.ts +9 -3
  57. package/src/types.ts +20 -1
  58. package/src/utils/isDevBuild.ts +10 -0
@@ -0,0 +1,469 @@
1
+ import {
2
+ CheckCircleIcon,
3
+ EyeIcon,
4
+ EyeSlashIcon,
5
+ PauseIcon,
6
+ PlayIcon,
7
+ XIcon,
8
+ } from '@phosphor-icons/react'
9
+ import classNames from 'classnames'
10
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
11
+
12
+ import type { LockedAttachmentBaseProps } from '../types'
13
+ import { renderTypeIcon } from '../utils/icons'
14
+ import { getSourceType } from '../utils/mimeType'
15
+
16
+ import MediaPlayer from './MediaPlayer'
17
+
18
+ export interface CreatorCardProps extends LockedAttachmentBaseProps {
19
+ title?: string
20
+ /** When true, shows interactive media preview (composing state). When false/omitted, shows static thumbnail (sent/sold state). */
21
+ isPreview?: boolean
22
+ placeholderTitle?: string
23
+ placeholderAmountText?: string
24
+ onDismiss?: () => void
25
+ }
26
+
27
+ // ─── Shared primitives ────────────────────────────────────────────────────────
28
+
29
+ interface CloseButtonProps {
30
+ onClose: () => void
31
+ }
32
+
33
+ const CloseButton: React.FC<CloseButtonProps> = (props) => {
34
+ const { onClose } = props
35
+ return (
36
+ <button
37
+ type="button"
38
+ onClick={onClose}
39
+ className="absolute left-3 top-3 z-40 flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
40
+ aria-label="Close preview"
41
+ >
42
+ <EyeIcon className="size-4" weight="fill" />
43
+ </button>
44
+ )
45
+ }
46
+
47
+ interface CollapsedThumbnailProps {
48
+ thumbnail?: string
49
+ mimeType: string
50
+ overlayIcon?: React.ElementType
51
+ onClick?: () => void
52
+ }
53
+
54
+ const CollapsedThumbnail: React.FC<CollapsedThumbnailProps> = (props) => {
55
+ const { thumbnail, mimeType, overlayIcon: OverlayIcon, onClick } = props
56
+ return (
57
+ <button
58
+ type="button"
59
+ disabled={!onClick}
60
+ className={classNames(
61
+ 'relative aspect-video block w-full overflow-hidden border-0 bg-black/5 p-0 text-left appearance-none',
62
+ { 'cursor-pointer': !!onClick, 'cursor-default': !onClick }
63
+ )}
64
+ onClick={onClick}
65
+ aria-label={OverlayIcon ? 'Toggle preview' : undefined}
66
+ >
67
+ {thumbnail ? (
68
+ <img
69
+ src={thumbnail}
70
+ alt=""
71
+ className="absolute inset-0 h-full w-full object-cover"
72
+ />
73
+ ) : (
74
+ <div className="absolute inset-0 flex items-center justify-center">
75
+ {renderTypeIcon(mimeType, {
76
+ className: 'size-12 text-black/20',
77
+ weight: 'regular',
78
+ })}
79
+ </div>
80
+ )}
81
+ {OverlayIcon && (
82
+ <div className="pointer-events-none absolute left-3 top-3 flex size-8 items-center justify-center rounded-full bg-black/60 text-white">
83
+ <OverlayIcon className="size-4" weight="fill" />
84
+ </div>
85
+ )}
86
+ </button>
87
+ )
88
+ }
89
+
90
+ // ─── Per-type preview components ─────────────────────────────────────────────
91
+
92
+ interface AudioPreviewProps {
93
+ source?: string
94
+ thumbnail?: string
95
+ mimeType: string
96
+ }
97
+
98
+ const AudioPreview: React.FC<AudioPreviewProps> = (props) => {
99
+ const { source, thumbnail, mimeType } = props
100
+ const [playing, setPlaying] = useState(false)
101
+ const [played, setPlayed] = useState(0)
102
+ const [seeking, setSeeking] = useState(false)
103
+ const audioRef = useRef<HTMLAudioElement>(null)
104
+ const trackRef = useRef<HTMLDivElement>(null)
105
+ const rafRef = useRef<number | null>(null)
106
+
107
+ useEffect(() => {
108
+ const el = audioRef.current
109
+ if (!el) return
110
+ if (playing) {
111
+ void el.play().catch(() => setPlaying(false))
112
+ } else {
113
+ el.pause()
114
+ }
115
+ }, [playing])
116
+
117
+ useEffect(() => {
118
+ if (!playing) {
119
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
120
+ return
121
+ }
122
+ const tick = () => {
123
+ const el = audioRef.current
124
+ if (el && el.duration && !seeking) setPlayed(el.currentTime / el.duration)
125
+ rafRef.current = requestAnimationFrame(tick)
126
+ }
127
+ rafRef.current = requestAnimationFrame(tick)
128
+ return () => {
129
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
130
+ }
131
+ }, [playing, seeking])
132
+
133
+ useEffect(() => {
134
+ setPlaying(false)
135
+ setPlayed(0)
136
+ }, [source])
137
+
138
+ const getFraction = useCallback(
139
+ (e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) => {
140
+ const track = trackRef.current
141
+ if (!track) return 0
142
+ const clientX =
143
+ 'touches' in e
144
+ ? (e.touches[0]?.clientX ?? e.changedTouches[0]?.clientX ?? 0)
145
+ : e.clientX
146
+ const rect = track.getBoundingClientRect()
147
+ return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))
148
+ },
149
+ []
150
+ )
151
+
152
+ const seekTo = useCallback((fraction: number) => {
153
+ const el = audioRef.current
154
+ if (el && el.duration) el.currentTime = fraction * el.duration
155
+ }, [])
156
+
157
+ useEffect(() => {
158
+ if (!seeking) return
159
+ const onMove = (e: MouseEvent | TouchEvent) => {
160
+ const f = getFraction(e)
161
+ setPlayed(f)
162
+ seekTo(f)
163
+ }
164
+ const onUp = (e: MouseEvent | TouchEvent) => {
165
+ setSeeking(false)
166
+ seekTo(getFraction(e))
167
+ }
168
+ window.addEventListener('mousemove', onMove)
169
+ window.addEventListener('mouseup', onUp)
170
+ window.addEventListener('touchmove', onMove, { passive: true })
171
+ window.addEventListener('touchend', onUp)
172
+ return () => {
173
+ window.removeEventListener('mousemove', onMove)
174
+ window.removeEventListener('mouseup', onUp)
175
+ window.removeEventListener('touchmove', onMove)
176
+ window.removeEventListener('touchend', onUp)
177
+ }
178
+ }, [seeking, getFraction, seekTo])
179
+
180
+ const toggle = useCallback(() => setPlaying((p) => !p), [])
181
+
182
+ return (
183
+ <div className="relative">
184
+ {source && (
185
+ <audio
186
+ ref={audioRef}
187
+ src={source}
188
+ loop
189
+ onEnded={() => {
190
+ setPlaying(false)
191
+ setPlayed(0)
192
+ }}
193
+ >
194
+ <track kind="captions" />
195
+ </audio>
196
+ )}
197
+ <CollapsedThumbnail
198
+ thumbnail={thumbnail}
199
+ mimeType={mimeType}
200
+ overlayIcon={source ? (playing ? PauseIcon : PlayIcon) : undefined}
201
+ onClick={source ? toggle : undefined}
202
+ />
203
+ {source && (
204
+ <div className="absolute inset-x-0 bottom-0 px-3 pb-2.5 pt-6 bg-gradient-to-t from-black/40 to-transparent">
205
+ <div
206
+ ref={trackRef}
207
+ role="slider"
208
+ aria-label="Playback position"
209
+ aria-valuenow={Math.round(played * 100)}
210
+ aria-valuemin={0}
211
+ aria-valuemax={100}
212
+ tabIndex={0}
213
+ className="relative flex h-4 w-full cursor-pointer items-center"
214
+ onMouseDown={(e) => {
215
+ e.stopPropagation()
216
+ setSeeking(true)
217
+ const f = getFraction(e)
218
+ setPlayed(f)
219
+ seekTo(f)
220
+ }}
221
+ onTouchStart={(e) => {
222
+ e.stopPropagation()
223
+ setSeeking(true)
224
+ const f = getFraction(e)
225
+ setPlayed(f)
226
+ seekTo(f)
227
+ }}
228
+ onClick={(e) => e.stopPropagation()}
229
+ onKeyDown={(e) => {
230
+ if (e.key === 'ArrowRight') seekTo(Math.min(1, played + 0.05))
231
+ if (e.key === 'ArrowLeft') seekTo(Math.max(0, played - 0.05))
232
+ }}
233
+ >
234
+ <div className="w-full overflow-hidden rounded-full bg-white/30 h-1">
235
+ <div
236
+ className="h-full rounded-full bg-white"
237
+ style={{ width: `${Math.round(played * 100)}%` }}
238
+ />
239
+ </div>
240
+ </div>
241
+ </div>
242
+ )}
243
+ </div>
244
+ )
245
+ }
246
+
247
+ interface VideoPreviewProps {
248
+ source?: string
249
+ thumbnail?: string
250
+ mimeType: string
251
+ }
252
+
253
+ const VideoPreview: React.FC<VideoPreviewProps> = (props) => {
254
+ const { source, thumbnail, mimeType } = props
255
+ const [expanded, setExpanded] = useState(false)
256
+ const collapse = () => setExpanded(false)
257
+
258
+ useEffect(() => {
259
+ setExpanded(false)
260
+ }, [source])
261
+
262
+ if (!source) {
263
+ return <CollapsedThumbnail thumbnail={thumbnail} mimeType={mimeType} />
264
+ }
265
+
266
+ return (
267
+ <div
268
+ className={classNames('relative overflow-hidden', {
269
+ 'aspect-video': !expanded,
270
+ })}
271
+ >
272
+ <MediaPlayer
273
+ source={source}
274
+ mimeType={mimeType}
275
+ poster={thumbnail}
276
+ playing={expanded}
277
+ loop
278
+ controls={false}
279
+ showProgress
280
+ onContainerClick={collapse}
281
+ muted
282
+ />
283
+ {!expanded && (
284
+ <button
285
+ type="button"
286
+ className="absolute inset-0 block cursor-pointer border-0 p-0 text-left appearance-none"
287
+ onClick={() => setExpanded(true)}
288
+ aria-label="Expand video preview"
289
+ >
290
+ {thumbnail ? (
291
+ <img
292
+ src={thumbnail}
293
+ alt=""
294
+ className="absolute inset-0 h-full w-full object-cover"
295
+ />
296
+ ) : (
297
+ <div className="absolute inset-0 flex items-center justify-center">
298
+ {renderTypeIcon(mimeType, {
299
+ className: 'size-12 text-black/20',
300
+ weight: 'regular',
301
+ })}
302
+ </div>
303
+ )}
304
+ <div className="pointer-events-none absolute left-3 top-3 flex size-8 items-center justify-center rounded-full bg-black/60 text-white">
305
+ <EyeSlashIcon className="size-4" weight="fill" />
306
+ </div>
307
+ </button>
308
+ )}
309
+ {expanded && <CloseButton onClose={collapse} />}
310
+ </div>
311
+ )
312
+ }
313
+
314
+ interface ImagePreviewProps {
315
+ source?: string
316
+ thumbnail?: string
317
+ mimeType: string
318
+ title?: string
319
+ }
320
+
321
+ const ImagePreview: React.FC<ImagePreviewProps> = (props) => {
322
+ const { source, thumbnail, mimeType, title } = props
323
+ const [expanded, setExpanded] = useState(false)
324
+ const collapse = () => setExpanded(false)
325
+
326
+ useEffect(() => {
327
+ setExpanded(false)
328
+ }, [source])
329
+
330
+ if (expanded && source) {
331
+ return (
332
+ <div className="relative">
333
+ <button
334
+ type="button"
335
+ className="block w-full cursor-pointer border-0 p-0 text-left appearance-none"
336
+ onClick={collapse}
337
+ aria-label="Close preview"
338
+ >
339
+ <img src={source} alt={title ?? ''} className="block w-full" />
340
+ </button>
341
+ <CloseButton onClose={collapse} />
342
+ </div>
343
+ )
344
+ }
345
+
346
+ return (
347
+ <CollapsedThumbnail
348
+ thumbnail={thumbnail}
349
+ mimeType={mimeType}
350
+ overlayIcon={source ? EyeSlashIcon : undefined}
351
+ onClick={source ? () => setExpanded(true) : undefined}
352
+ />
353
+ )
354
+ }
355
+
356
+ // ─── Card shell ───────────────────────────────────────────────────────────────
357
+
358
+ const CreatorCard: React.FC<CreatorCardProps> = (props) => {
359
+ const {
360
+ title,
361
+ mimeType = 'application/octet-stream',
362
+ thumbnail,
363
+ source,
364
+ detail,
365
+ amountText,
366
+ placeholderTitle = 'Attachment title',
367
+ placeholderAmountText,
368
+ paymentStatus,
369
+ onDismiss,
370
+ isPreview = false,
371
+ } = props
372
+ const sourceType = getSourceType(mimeType)
373
+ const displayAmountText = placeholderAmountText ?? amountText
374
+ const isPlaceholderAmount = !!placeholderAmountText
375
+
376
+ let mediaPreview: React.ReactNode
377
+ if (isPreview && sourceType === 'audio') {
378
+ mediaPreview = (
379
+ <AudioPreview source={source} thumbnail={thumbnail} mimeType={mimeType} />
380
+ )
381
+ } else if (isPreview && sourceType === 'video') {
382
+ mediaPreview = (
383
+ <VideoPreview source={source} thumbnail={thumbnail} mimeType={mimeType} />
384
+ )
385
+ } else if (isPreview && sourceType === 'image') {
386
+ mediaPreview = (
387
+ <ImagePreview
388
+ source={source}
389
+ thumbnail={thumbnail}
390
+ mimeType={mimeType}
391
+ title={title}
392
+ />
393
+ )
394
+ } else {
395
+ mediaPreview = (
396
+ <CollapsedThumbnail thumbnail={thumbnail} mimeType={mimeType} />
397
+ )
398
+ }
399
+
400
+ return (
401
+ <div className="relative w-[280px] select-none overflow-hidden rounded-3xl bg-white shadow-card">
402
+ {onDismiss && (
403
+ <button
404
+ type="button"
405
+ onClick={onDismiss}
406
+ className="absolute right-3 top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
407
+ aria-label="Dismiss attachment"
408
+ >
409
+ <XIcon className="size-4" weight="bold" />
410
+ </button>
411
+ )}
412
+ {mediaPreview}
413
+ <div className="px-4 pb-3 pt-3">
414
+ <p
415
+ className={classNames('mb-1.5 truncate text-base font-medium', {
416
+ 'text-black/30': !title,
417
+ 'text-black': !!title,
418
+ })}
419
+ >
420
+ {title || placeholderTitle}
421
+ </p>
422
+ <div className="flex items-center gap-1">
423
+ {renderTypeIcon(mimeType, {
424
+ className: 'size-5 shrink-0 text-black/55',
425
+ weight: 'regular',
426
+ })}
427
+ {detail && (
428
+ <span className="text-xs font-medium text-black/55">{detail}</span>
429
+ )}
430
+ {paymentStatus === 'paid' ? (
431
+ <>
432
+ <span className="text-xs font-medium text-black/55">•</span>
433
+ <span className="text-xs font-medium text-[#008236]">
434
+ Purchased
435
+ </span>
436
+ <CheckCircleIcon
437
+ className="size-4 text-[#008236]"
438
+ weight="bold"
439
+ />
440
+ </>
441
+ ) : (
442
+ displayAmountText && (
443
+ <>
444
+ <span
445
+ className={classNames('text-xs font-medium', {
446
+ 'text-black/30': isPlaceholderAmount,
447
+ 'text-black/55': !isPlaceholderAmount,
448
+ })}
449
+ >
450
+
451
+ </span>
452
+ <span
453
+ className={classNames('text-xs font-medium', {
454
+ 'text-black/30': isPlaceholderAmount,
455
+ 'text-black/55': !isPlaceholderAmount,
456
+ })}
457
+ >
458
+ {displayAmountText}
459
+ </span>
460
+ </>
461
+ )
462
+ )}
463
+ </div>
464
+ </div>
465
+ </div>
466
+ )
467
+ }
468
+
469
+ export default CreatorCard