@linktr.ee/messaging-react 1.31.0-rc-1776677746 → 1.31.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 (35) hide show
  1. package/dist/{Creator-DGe3CQ_j.js → Card-C5t3dZ5q.js} +177 -150
  2. package/dist/Card-C5t3dZ5q.js.map +1 -0
  3. package/dist/Card-Cn2va-Qr.js +205 -0
  4. package/dist/Card-Cn2va-Qr.js.map +1 -0
  5. package/dist/index.d.ts +35 -30
  6. package/dist/index.js +951 -956
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/ChannelView.tsx +24 -36
  10. package/src/components/CustomMessage/CustomMessage.stories.tsx +1 -14
  11. package/src/components/CustomMessage/context.tsx +20 -0
  12. package/src/components/CustomMessage/index.tsx +39 -28
  13. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +8 -13
  14. package/src/components/LockedAttachment/components/Creator/Card.tsx +159 -0
  15. package/src/components/LockedAttachment/components/Creator/CardAudioPreview.tsx +161 -0
  16. package/src/components/LockedAttachment/components/Creator/CardCollapsedThumbnail.tsx +58 -0
  17. package/src/components/LockedAttachment/components/Creator/CardImagePreview.tsx +56 -0
  18. package/src/components/LockedAttachment/components/Creator/CardVideoPreview.tsx +91 -0
  19. package/src/components/LockedAttachment/components/Creator/index.tsx +2 -0
  20. package/src/components/LockedAttachment/components/Visitor/Card.tsx +186 -0
  21. package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +71 -0
  22. package/src/components/LockedAttachment/components/Visitor/CardImagePreview.tsx +39 -0
  23. package/src/components/LockedAttachment/components/Visitor/CardMediaPreview.tsx +36 -0
  24. package/src/components/LockedAttachment/components/Visitor/CardThumbnailPreview.tsx +45 -0
  25. package/src/components/LockedAttachment/components/Visitor/index.ts +2 -0
  26. package/src/components/LockedAttachment/index.tsx +16 -23
  27. package/src/components/LockedAttachment/types.ts +14 -1
  28. package/src/components/MessagingShell/index.tsx +0 -6
  29. package/src/index.ts +4 -1
  30. package/src/types.ts +0 -21
  31. package/dist/Creator-DGe3CQ_j.js.map +0 -1
  32. package/dist/Visitor-DyJTWB2_.js +0 -204
  33. package/dist/Visitor-DyJTWB2_.js.map +0 -1
  34. package/src/components/LockedAttachment/components/Creator.tsx +0 -470
  35. package/src/components/LockedAttachment/components/Visitor.tsx +0 -356
@@ -1,470 +0,0 @@
1
- import {
2
- CheckCircleIcon,
3
- EyeIcon,
4
- EyeSlashIcon,
5
- LockIcon,
6
- LockOpenIcon,
7
- PauseIcon,
8
- PlayIcon,
9
- XIcon,
10
- } from '@phosphor-icons/react'
11
- import classNames from 'classnames'
12
- import React, { useCallback, useEffect, useRef, useState } from 'react'
13
-
14
- import type { LockedAttachmentBaseProps } from '../types'
15
- import { renderTypeIcon } from '../utils/icons'
16
- import { getSourceType } from '../utils/mimeType'
17
-
18
- import MediaPlayer from './MediaPlayer'
19
-
20
- export interface CreatorCardProps extends LockedAttachmentBaseProps {
21
- isPreview?: boolean
22
- placeholderTitle?: string
23
- placeholderAmountText?: string
24
- sourceUrl?: string
25
- onDismiss?: () => void
26
- }
27
-
28
- interface CloseButtonProps {
29
- onClose: () => void
30
- }
31
-
32
- const CloseButton: React.FC<CloseButtonProps> = (props) => {
33
- const { onClose } = props
34
- return (
35
- <button
36
- type="button"
37
- onClick={onClose}
38
- className="absolute left-3 top-3 z-40 flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
39
- aria-label="Close preview"
40
- >
41
- <EyeIcon className="size-4" weight="fill" />
42
- </button>
43
- )
44
- }
45
-
46
- interface CollapsedThumbnailProps {
47
- thumbnailUrl?: string
48
- mimeType: string
49
- overlayIcon?: React.ElementType
50
- darkOverlay?: boolean
51
- onClick?: () => void
52
- }
53
-
54
- const CollapsedThumbnail: React.FC<CollapsedThumbnailProps> = (props) => {
55
- const { thumbnailUrl, mimeType, overlayIcon: OverlayIcon, darkOverlay, 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
- {thumbnailUrl ? (
68
- <img
69
- src={thumbnailUrl}
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
- {darkOverlay && (
82
- <div className="pointer-events-none absolute inset-0 bg-black/30" />
83
- )}
84
- {OverlayIcon && (
85
- <div className="pointer-events-none absolute left-3 top-3 flex size-8 items-center justify-center rounded-full bg-black/60 text-white">
86
- <OverlayIcon className="size-4" weight="fill" />
87
- </div>
88
- )}
89
- </button>
90
- )
91
- }
92
-
93
-
94
- interface AudioPreviewProps {
95
- sourceUrl?: string
96
- thumbnailUrl?: string
97
- mimeType: string
98
- }
99
-
100
- const AudioPreview: React.FC<AudioPreviewProps> = (props) => {
101
- const { sourceUrl, thumbnailUrl, mimeType } = props
102
- const [playing, setPlaying] = useState(false)
103
- const [played, setPlayed] = useState(0)
104
- const [seeking, setSeeking] = useState(false)
105
- const audioRef = useRef<HTMLAudioElement>(null)
106
- const trackRef = useRef<HTMLDivElement>(null)
107
- const rafRef = useRef<number | null>(null)
108
-
109
- useEffect(() => {
110
- const el = audioRef.current
111
- if (!el) return
112
- if (playing) {
113
- void el.play().catch(() => setPlaying(false))
114
- } else {
115
- el.pause()
116
- }
117
- }, [playing])
118
-
119
- useEffect(() => {
120
- if (!playing) {
121
- if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
122
- return
123
- }
124
- const tick = () => {
125
- const el = audioRef.current
126
- if (el && el.duration && !seeking) setPlayed(el.currentTime / el.duration)
127
- rafRef.current = requestAnimationFrame(tick)
128
- }
129
- rafRef.current = requestAnimationFrame(tick)
130
- return () => {
131
- if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
132
- }
133
- }, [playing, seeking])
134
-
135
- const [audioReady, setAudioReady] = useState(false)
136
-
137
- const getFraction = useCallback(
138
- (e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) => {
139
- const track = trackRef.current
140
- if (!track) return 0
141
- const clientX =
142
- 'touches' in e
143
- ? (e.touches[0]?.clientX ?? e.changedTouches[0]?.clientX ?? 0)
144
- : e.clientX
145
- const rect = track.getBoundingClientRect()
146
- return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))
147
- },
148
- []
149
- )
150
-
151
- const seekTo = useCallback((fraction: number) => {
152
- const el = audioRef.current
153
- if (el && el.duration) el.currentTime = fraction * el.duration
154
- }, [])
155
-
156
- useEffect(() => {
157
- if (!seeking) return
158
- const onMove = (e: MouseEvent | TouchEvent) => {
159
- const f = getFraction(e)
160
- setPlayed(f)
161
- seekTo(f)
162
- }
163
- const onUp = (e: MouseEvent | TouchEvent) => {
164
- setSeeking(false)
165
- seekTo(getFraction(e))
166
- }
167
- window.addEventListener('mousemove', onMove)
168
- window.addEventListener('mouseup', onUp)
169
- window.addEventListener('touchmove', onMove, { passive: true })
170
- window.addEventListener('touchend', onUp)
171
- return () => {
172
- window.removeEventListener('mousemove', onMove)
173
- window.removeEventListener('mouseup', onUp)
174
- window.removeEventListener('touchmove', onMove)
175
- window.removeEventListener('touchend', onUp)
176
- }
177
- }, [seeking, getFraction, seekTo])
178
-
179
- const toggle = useCallback(() => setPlaying((p) => !p), [])
180
-
181
- return (
182
- <div className="relative">
183
- {sourceUrl && (
184
- <audio
185
- ref={audioRef}
186
- src={sourceUrl}
187
- loop
188
- onCanPlay={() => setAudioReady(true)}
189
- onEnded={() => {
190
- setPlaying(false)
191
- setPlayed(0)
192
- }}
193
- >
194
- <track kind="captions" />
195
- </audio>
196
- )}
197
- <CollapsedThumbnail
198
- thumbnailUrl={thumbnailUrl}
199
- mimeType={mimeType}
200
- overlayIcon={sourceUrl && audioReady ? (playing ? PauseIcon : PlayIcon) : undefined}
201
- onClick={sourceUrl && audioReady ? toggle : undefined}
202
- />
203
- {sourceUrl && audioReady && (
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
- sourceUrl?: string
249
- thumbnailUrl?: string
250
- mimeType: string
251
- }
252
-
253
- const VideoPreview: React.FC<VideoPreviewProps> = (props) => {
254
- const { sourceUrl, thumbnailUrl, mimeType } = props
255
- const [expanded, setExpanded] = useState(false)
256
- const collapse = () => setExpanded(false)
257
-
258
- if (!sourceUrl) {
259
- return <CollapsedThumbnail thumbnailUrl={thumbnailUrl} mimeType={mimeType} />
260
- }
261
-
262
- return (
263
- <div
264
- className={classNames('relative overflow-hidden', {
265
- 'aspect-video': !expanded,
266
- })}
267
- >
268
- <MediaPlayer
269
- source={sourceUrl}
270
- mimeType={mimeType}
271
- poster={thumbnailUrl}
272
- playing={expanded}
273
- loop={true}
274
- controls={false}
275
- muted={true}
276
- showProgress={true}
277
- onContainerClick={collapse}
278
- />
279
- {!expanded && (
280
- <button
281
- type="button"
282
- className="absolute inset-0 block cursor-pointer border-0 p-0 text-left appearance-none"
283
- onClick={() => setExpanded(true)}
284
- aria-label="Expand video preview"
285
- >
286
- {thumbnailUrl ? (
287
- <img
288
- src={thumbnailUrl}
289
- alt=""
290
- className="absolute inset-0 h-full w-full object-cover"
291
- />
292
- ) : (
293
- <div className="absolute inset-0 flex items-center justify-center">
294
- {renderTypeIcon(mimeType, {
295
- className: 'size-12 text-black/20',
296
- weight: 'regular',
297
- })}
298
- </div>
299
- )}
300
- <div className="pointer-events-none absolute left-3 top-3 flex size-8 items-center justify-center rounded-full bg-black/60 text-white">
301
- <EyeSlashIcon className="size-4" weight="fill" />
302
- </div>
303
- </button>
304
- )}
305
- {expanded && <CloseButton onClose={collapse} />}
306
- </div>
307
- )
308
- }
309
-
310
- interface ImagePreviewProps {
311
- sourceUrl?: string
312
- thumbnailUrl?: string
313
- mimeType: string
314
- title?: string
315
- }
316
-
317
- const ImagePreview: React.FC<ImagePreviewProps> = (props) => {
318
- const { sourceUrl, thumbnailUrl, mimeType, title } = props
319
- const [expanded, setExpanded] = useState(false)
320
-
321
- if (expanded && sourceUrl) {
322
- return (
323
- <div className="relative">
324
- <button
325
- type="button"
326
- className="block w-full cursor-pointer border-0 p-0 text-left appearance-none"
327
- onClick={() => setExpanded(false)}
328
- aria-label="Close preview"
329
- >
330
- <img src={sourceUrl} alt={title ?? ''} className="block w-full" />
331
- </button>
332
- <CloseButton onClose={() => setExpanded(false)} />
333
- </div>
334
- )
335
- }
336
-
337
- return (
338
- <CollapsedThumbnail
339
- thumbnailUrl={thumbnailUrl}
340
- mimeType={mimeType}
341
- overlayIcon={sourceUrl ? EyeSlashIcon : undefined}
342
- onClick={sourceUrl ? () => setExpanded(true) : undefined}
343
- />
344
- )
345
- }
346
-
347
-
348
- const CreatorCard: React.FC<CreatorCardProps> = (props) => {
349
- const {
350
- title,
351
- mimeType = 'application/octet-stream',
352
- thumbnailUrl,
353
- sourceUrl,
354
- detail,
355
- amountText,
356
- placeholderTitle = 'Attachment title',
357
- placeholderAmountText,
358
- paymentStatus,
359
- onDismiss,
360
- isPreview = false,
361
- } = props
362
- const sourceType = getSourceType(mimeType)
363
- const displayAmountText = amountText ?? placeholderAmountText
364
- const isPlaceholderAmount = !amountText && !!placeholderAmountText
365
-
366
- let mediaPreview: React.ReactNode
367
- if (isPreview && sourceType === 'audio') {
368
- mediaPreview = (
369
- <AudioPreview key={sourceUrl} sourceUrl={sourceUrl} thumbnailUrl={thumbnailUrl} mimeType={mimeType} />
370
- )
371
- } else if (isPreview && sourceType === 'video') {
372
- mediaPreview = (
373
- <VideoPreview key={sourceUrl} sourceUrl={sourceUrl} thumbnailUrl={thumbnailUrl} mimeType={mimeType} />
374
- )
375
- } else if (isPreview && sourceType === 'image') {
376
- mediaPreview = (
377
- <ImagePreview
378
- key={sourceUrl}
379
- sourceUrl={sourceUrl}
380
- thumbnailUrl={thumbnailUrl}
381
- mimeType={mimeType}
382
- title={title}
383
- />
384
- )
385
- } else {
386
- const lockedOverlayIcon = onDismiss
387
- ? undefined
388
- : paymentStatus === 'paid'
389
- ? LockOpenIcon
390
- : LockIcon
391
- mediaPreview = (
392
- <CollapsedThumbnail
393
- thumbnailUrl={thumbnailUrl}
394
- mimeType={mimeType}
395
- overlayIcon={lockedOverlayIcon}
396
- darkOverlay
397
- />
398
- )
399
- }
400
-
401
- return (
402
- <div className="relative w-[280px] select-none overflow-hidden rounded-[24px] bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]">
403
- {onDismiss && (
404
- <button
405
- type="button"
406
- onClick={onDismiss}
407
- className="absolute right-3 top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
408
- aria-label="Dismiss attachment"
409
- >
410
- <XIcon className="size-4" weight="bold" />
411
- </button>
412
- )}
413
- {mediaPreview}
414
- <div className="px-4 pb-3 pt-3">
415
- <p
416
- className={classNames('mb-1.5 truncate text-base font-medium', {
417
- 'text-black/30': !title,
418
- 'text-black': !!title,
419
- })}
420
- >
421
- {title || placeholderTitle}
422
- </p>
423
- <div className="flex items-center gap-1">
424
- {renderTypeIcon(mimeType, {
425
- className: 'size-5 shrink-0 text-black/55',
426
- weight: 'regular',
427
- })}
428
- {detail && (
429
- <span className="text-xs font-medium text-black/55">{detail}</span>
430
- )}
431
- {paymentStatus === 'paid' ? (
432
- <>
433
- <span className="text-xs font-medium text-black/55">•</span>
434
- <span className="text-xs font-medium text-[#008236]">
435
- Purchased
436
- </span>
437
- <CheckCircleIcon
438
- className="size-4 text-[#008236]"
439
- weight="bold"
440
- />
441
- </>
442
- ) : (
443
- displayAmountText && (
444
- <>
445
- <span
446
- className={classNames('text-xs font-medium', {
447
- 'text-black/30': isPlaceholderAmount,
448
- 'text-black/55': !isPlaceholderAmount,
449
- })}
450
- >
451
-
452
- </span>
453
- <span
454
- className={classNames('text-xs font-medium', {
455
- 'text-black/30': isPlaceholderAmount,
456
- 'text-black/55': !isPlaceholderAmount,
457
- })}
458
- >
459
- {displayAmountText}
460
- </span>
461
- </>
462
- )
463
- )}
464
- </div>
465
- </div>
466
- </div>
467
- )
468
- }
469
-
470
- export default CreatorCard