@linktr.ee/messaging-react 1.26.0 → 1.26.1-rc-1776055454
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/Preview-DqAv16NS.js +87 -0
- package/dist/Preview-DqAv16NS.js.map +1 -0
- package/dist/dash.all.min-Duv4lvGS.js +18858 -0
- package/dist/dash.all.min-Duv4lvGS.js.map +1 -0
- package/dist/hls-Bogc7CBn.js +21710 -0
- package/dist/hls-Bogc7CBn.js.map +1 -0
- package/dist/index-Da-xN4Yq.js +16142 -0
- package/dist/index-Da-xN4Yq.js.map +1 -0
- package/dist/index-Dj9rqWcU.js +69 -0
- package/dist/index-Dj9rqWcU.js.map +1 -0
- package/dist/index.d.ts +60 -9
- package/dist/index.js +1766 -1181
- package/dist/index.js.map +1 -1
- package/dist/mixin-B6jYfIcp.js +808 -0
- package/dist/mixin-B6jYfIcp.js.map +1 -0
- package/dist/react-BxlQMOfz.js +419 -0
- package/dist/react-BxlQMOfz.js.map +1 -0
- package/dist/react-COAP-MIW.js +377 -0
- package/dist/react-COAP-MIW.js.map +1 -0
- package/dist/react-Cn4WlMcl.js +3108 -0
- package/dist/react-Cn4WlMcl.js.map +1 -0
- package/dist/react-CwTJArKY.js +459 -0
- package/dist/react-CwTJArKY.js.map +1 -0
- package/dist/react-DkfS_atT.js +373 -0
- package/dist/react-DkfS_atT.js.map +1 -0
- package/dist/react-Pea5fum1.js +286 -0
- package/dist/react-Pea5fum1.js.map +1 -0
- package/dist/react-RiBbsUDd.js +534 -0
- package/dist/react-RiBbsUDd.js.map +1 -0
- package/dist/react-dS1WBxxz.js +238 -0
- package/dist/react-dS1WBxxz.js.map +1 -0
- package/package.json +2 -1
- package/src/components/ChannelView.test.tsx +50 -1
- package/src/components/ChannelView.tsx +13 -3
- package/src/components/CustomMessage/CustomMessage.stories.tsx +61 -2
- package/src/components/CustomMessage/MessageTag.tsx +5 -0
- package/src/components/CustomMessage/index.tsx +46 -4
- package/src/components/LockedAttachmentCard/LockedAttachmentCard.stories.tsx +351 -0
- package/src/components/LockedAttachmentCard/index.tsx +378 -0
- package/src/components/LockedAttachmentCard/mimeType.test.ts +97 -0
- package/src/components/LockedAttachmentCard/mimeType.ts +35 -0
- package/src/index.ts +4 -0
- package/src/stream-custom-data.ts +10 -3
- package/src/types.ts +15 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DownloadSimpleIcon,
|
|
3
|
+
CircleNotchIcon,
|
|
4
|
+
CheckCircleIcon,
|
|
5
|
+
FileIcon,
|
|
6
|
+
FileCsvIcon,
|
|
7
|
+
FileDocIcon,
|
|
8
|
+
FileMdIcon,
|
|
9
|
+
FilePdfIcon,
|
|
10
|
+
FilePptIcon,
|
|
11
|
+
FileTextIcon,
|
|
12
|
+
FileXlsIcon,
|
|
13
|
+
FileZipIcon,
|
|
14
|
+
ImageIcon,
|
|
15
|
+
LockSimpleIcon,
|
|
16
|
+
LockSimpleOpenIcon,
|
|
17
|
+
PauseIcon,
|
|
18
|
+
PlayIcon,
|
|
19
|
+
SpeakerHighIcon,
|
|
20
|
+
VideoCameraIcon,
|
|
21
|
+
} from '@phosphor-icons/react'
|
|
22
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
23
|
+
import ReactPlayer from 'react-player'
|
|
24
|
+
|
|
25
|
+
import { getDocumentIconType, getSourceType } from './mimeType'
|
|
26
|
+
import type { AttachmentSourceType } from './mimeType'
|
|
27
|
+
|
|
28
|
+
export interface LockedAttachmentCardProps {
|
|
29
|
+
title: string
|
|
30
|
+
amountText?: string
|
|
31
|
+
thumbnail?: string
|
|
32
|
+
/**
|
|
33
|
+
* The unlocked media URL. When undefined the card is in locked state.
|
|
34
|
+
* Typically undefined while the source is being fetched post-purchase.
|
|
35
|
+
*/
|
|
36
|
+
source?: string
|
|
37
|
+
/** MIME type of the attachment (e.g. 'video/mp4', 'audio/mpeg', 'image/jpeg', 'application/pdf'). Determines the icon and player behaviour. */
|
|
38
|
+
mimeType: string
|
|
39
|
+
detail?: string
|
|
40
|
+
/**
|
|
41
|
+
* Called when the visitor clicks Unlock.
|
|
42
|
+
* Omit to hide the Unlock button (e.g. creator-side views).
|
|
43
|
+
*/
|
|
44
|
+
onUnlock?: () => void
|
|
45
|
+
/** Called when the visitor clicks Download on an unlocked card. */
|
|
46
|
+
onDownload?: () => void
|
|
47
|
+
/** Shows a loading spinner on the unlock button while the source is being fetched. */
|
|
48
|
+
loading?: boolean
|
|
49
|
+
/**
|
|
50
|
+
* Payment has been completed but the source URL hasn't been resolved yet.
|
|
51
|
+
* Shows a "Preparing…" state instead of the lock overlay.
|
|
52
|
+
*/
|
|
53
|
+
isPurchased?: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const MEDIA_TYPE_ICON: Record<AttachmentSourceType, React.ElementType> = {
|
|
57
|
+
video: VideoCameraIcon,
|
|
58
|
+
audio: SpeakerHighIcon,
|
|
59
|
+
image: ImageIcon,
|
|
60
|
+
document: FileIcon,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const DOCUMENT_ICON_COMPONENT = {
|
|
64
|
+
pdf: FilePdfIcon,
|
|
65
|
+
doc: FileDocIcon,
|
|
66
|
+
xls: FileXlsIcon,
|
|
67
|
+
csv: FileCsvIcon,
|
|
68
|
+
ppt: FilePptIcon,
|
|
69
|
+
zip: FileZipIcon,
|
|
70
|
+
text: FileTextIcon,
|
|
71
|
+
markdown: FileMdIcon,
|
|
72
|
+
generic: FileIcon,
|
|
73
|
+
} as const
|
|
74
|
+
|
|
75
|
+
function getTypeIcon(mimeType: string): React.ElementType {
|
|
76
|
+
const sourceType = getSourceType(mimeType)
|
|
77
|
+
if (sourceType !== 'document') return MEDIA_TYPE_ICON[sourceType]
|
|
78
|
+
return DOCUMENT_ICON_COMPONENT[getDocumentIconType(mimeType)]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const LockedAttachmentCard: React.FC<LockedAttachmentCardProps> = ({
|
|
82
|
+
title,
|
|
83
|
+
amountText,
|
|
84
|
+
thumbnail,
|
|
85
|
+
source,
|
|
86
|
+
mimeType,
|
|
87
|
+
detail,
|
|
88
|
+
onUnlock,
|
|
89
|
+
onDownload,
|
|
90
|
+
loading = false,
|
|
91
|
+
isPurchased = false,
|
|
92
|
+
}) => {
|
|
93
|
+
const isLocked = source === undefined
|
|
94
|
+
const LockIcon = isPurchased ? LockSimpleOpenIcon : LockSimpleIcon
|
|
95
|
+
const sourceType = getSourceType(mimeType)
|
|
96
|
+
const TypeIcon = getTypeIcon(mimeType)
|
|
97
|
+
const [playing, setPlaying] = useState(false)
|
|
98
|
+
const [played, setPlayed] = useState(0)
|
|
99
|
+
const [seeking, setSeeking] = useState(false)
|
|
100
|
+
const [scrubberHovered, setScrubberHovered] = useState(false)
|
|
101
|
+
const [sourceReady, setSourceReady] = useState(false)
|
|
102
|
+
const [videoAspect, setVideoAspect] = useState<number | null>(null)
|
|
103
|
+
const [buffering, setBuffering] = useState(false)
|
|
104
|
+
const playerRef = useRef<HTMLVideoElement>(null)
|
|
105
|
+
const trackRef = useRef<HTMLDivElement>(null)
|
|
106
|
+
const rafRef = useRef<number | null>(null)
|
|
107
|
+
|
|
108
|
+
const tickProgress = useCallback(() => {
|
|
109
|
+
const el = playerRef.current
|
|
110
|
+
if (el && el.duration && !seeking) setPlayed(el.currentTime / el.duration)
|
|
111
|
+
rafRef.current = requestAnimationFrame(tickProgress)
|
|
112
|
+
}, [seeking])
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (playing) {
|
|
116
|
+
rafRef.current = requestAnimationFrame(tickProgress)
|
|
117
|
+
} else {
|
|
118
|
+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
|
|
119
|
+
}
|
|
120
|
+
return () => { if (rafRef.current !== null) cancelAnimationFrame(rafRef.current) }
|
|
121
|
+
}, [playing, tickProgress])
|
|
122
|
+
|
|
123
|
+
// ReactPlayer v3 uses native HTML media elements and does not support a
|
|
124
|
+
// declarative `playing` prop — playback must be driven imperatively.
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
const el = playerRef.current
|
|
127
|
+
if (!el) return
|
|
128
|
+
if (playing) {
|
|
129
|
+
el.play().catch(() => {})
|
|
130
|
+
} else {
|
|
131
|
+
el.pause()
|
|
132
|
+
}
|
|
133
|
+
}, [playing])
|
|
134
|
+
|
|
135
|
+
const getFraction = (e: MouseEvent | React.MouseEvent) => {
|
|
136
|
+
const track = trackRef.current
|
|
137
|
+
if (!track) return 0
|
|
138
|
+
const rect = track.getBoundingClientRect()
|
|
139
|
+
return Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const seekTo = (fraction: number) => {
|
|
143
|
+
const el = playerRef.current
|
|
144
|
+
if (el && el.duration) el.currentTime = fraction * el.duration
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const handleTrackMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
148
|
+
e.stopPropagation()
|
|
149
|
+
setSeeking(true)
|
|
150
|
+
const fraction = getFraction(e)
|
|
151
|
+
setPlayed(fraction)
|
|
152
|
+
seekTo(fraction)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (!seeking) return
|
|
157
|
+
const onMove = (e: MouseEvent) => setPlayed(getFraction(e))
|
|
158
|
+
const onUp = (e: MouseEvent) => {
|
|
159
|
+
setSeeking(false)
|
|
160
|
+
seekTo(getFraction(e))
|
|
161
|
+
}
|
|
162
|
+
window.addEventListener('mousemove', onMove)
|
|
163
|
+
window.addEventListener('mouseup', onUp)
|
|
164
|
+
return () => {
|
|
165
|
+
window.removeEventListener('mousemove', onMove)
|
|
166
|
+
window.removeEventListener('mouseup', onUp)
|
|
167
|
+
}
|
|
168
|
+
}, [seeking])
|
|
169
|
+
|
|
170
|
+
const mediaPlayer = (
|
|
171
|
+
<div
|
|
172
|
+
role="button"
|
|
173
|
+
tabIndex={0}
|
|
174
|
+
className={`relative cursor-pointer overflow-hidden ${sourceType === 'audio' && !thumbnail ? 'bg-black/5' : 'bg-black'}${!thumbnail ? ' aspect-video' : ''}`}
|
|
175
|
+
style={!thumbnail && videoAspect ? { aspectRatio: String(videoAspect) } : undefined}
|
|
176
|
+
onClick={() => setPlaying((p) => !p)}
|
|
177
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setPlaying((p) => !p) }}
|
|
178
|
+
>
|
|
179
|
+
{thumbnail && (
|
|
180
|
+
<img src={thumbnail} alt="" className="block w-full" />
|
|
181
|
+
)}
|
|
182
|
+
{!thumbnail && (
|
|
183
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
184
|
+
<TypeIcon className="size-12 text-black/20" weight="regular" />
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
<div className="absolute inset-0">
|
|
188
|
+
<ReactPlayer
|
|
189
|
+
ref={playerRef}
|
|
190
|
+
src={source}
|
|
191
|
+
poster={thumbnail}
|
|
192
|
+
width="100%"
|
|
193
|
+
height="100%"
|
|
194
|
+
onLoadStart={() => setBuffering(true)}
|
|
195
|
+
onCanPlay={() => setBuffering(false)}
|
|
196
|
+
onWaiting={() => setBuffering(true)}
|
|
197
|
+
onLoadedMetadata={() => {
|
|
198
|
+
const el = playerRef.current
|
|
199
|
+
if (el && el.videoWidth && el.videoHeight) {
|
|
200
|
+
setVideoAspect(el.videoWidth / el.videoHeight)
|
|
201
|
+
}
|
|
202
|
+
}}
|
|
203
|
+
onEnded={() => {
|
|
204
|
+
setPlaying(false)
|
|
205
|
+
setPlayed(0)
|
|
206
|
+
}}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{buffering && (
|
|
211
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
212
|
+
<CircleNotchIcon className="size-8 animate-spin text-white/80" weight="bold" />
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{/* Custom controls */}
|
|
217
|
+
<div className="absolute inset-x-0 bottom-0 flex items-center gap-2 bg-gradient-to-t from-black/60 to-transparent px-3 pb-2.5 pt-6">
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
onClick={(e) => { e.stopPropagation(); setPlaying((p) => !p) }}
|
|
221
|
+
className="shrink-0 text-white"
|
|
222
|
+
aria-label={playing ? 'Pause' : 'Play'}
|
|
223
|
+
>
|
|
224
|
+
{playing
|
|
225
|
+
? <PauseIcon className="size-5" weight="fill" />
|
|
226
|
+
: <PlayIcon className="size-5 translate-x-px" weight="fill" />
|
|
227
|
+
}
|
|
228
|
+
</button>
|
|
229
|
+
|
|
230
|
+
{/* Scrubber */}
|
|
231
|
+
<div
|
|
232
|
+
role="slider"
|
|
233
|
+
aria-label="Playback position"
|
|
234
|
+
aria-valuenow={Math.round(played * 100)}
|
|
235
|
+
aria-valuemin={0}
|
|
236
|
+
aria-valuemax={100}
|
|
237
|
+
tabIndex={0}
|
|
238
|
+
ref={trackRef}
|
|
239
|
+
className="relative flex h-4 w-full cursor-pointer items-center"
|
|
240
|
+
onMouseDown={handleTrackMouseDown}
|
|
241
|
+
onClick={(e) => e.stopPropagation()}
|
|
242
|
+
onMouseEnter={() => setScrubberHovered(true)}
|
|
243
|
+
onMouseLeave={() => setScrubberHovered(false)}
|
|
244
|
+
onKeyDown={(e) => {
|
|
245
|
+
if (e.key === 'ArrowRight') seekTo(Math.min(1, played + 0.05))
|
|
246
|
+
if (e.key === 'ArrowLeft') seekTo(Math.max(0, played - 0.05))
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
<div className={`w-full overflow-hidden rounded-full bg-white/30 transition-all duration-200 ${scrubberHovered || seeking ? 'h-1.5' : 'h-1'}`}>
|
|
250
|
+
<div className="h-full rounded-full bg-white" style={{ width: `${played * 100}%` }} />
|
|
251
|
+
</div>
|
|
252
|
+
<div
|
|
253
|
+
className={`absolute size-3 -translate-x-1/2 rounded-full bg-white shadow transition-[opacity,transform] duration-200 ${scrubberHovered || seeking ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
|
|
254
|
+
style={{ left: `${played * 100}%` }}
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<div className="w-[280px] overflow-hidden rounded-3xl bg-white shadow-[0px_0px_0px_1px_rgba(0,0,0,0.04),0px_1px_2px_0px_rgba(0,0,0,0.04),0px_8px_32px_0px_rgba(0,0,0,0.1)]">
|
|
263
|
+
|
|
264
|
+
{/* Media area */}
|
|
265
|
+
{sourceType === 'image' || sourceType === 'document' ? (
|
|
266
|
+
<div className={`relative overflow-hidden bg-black/5${!thumbnail && (isLocked || sourceType === 'document') ? ' aspect-video' : ''}`}>
|
|
267
|
+
{thumbnail && (
|
|
268
|
+
<img
|
|
269
|
+
src={thumbnail}
|
|
270
|
+
alt=""
|
|
271
|
+
className="block w-full"
|
|
272
|
+
/>
|
|
273
|
+
)}
|
|
274
|
+
{!thumbnail && !isLocked && (
|
|
275
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
276
|
+
<TypeIcon className="size-12 text-black/20" weight="regular" />
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
{!thumbnail && !isLocked && sourceType === 'image' && (
|
|
280
|
+
<img
|
|
281
|
+
src={source}
|
|
282
|
+
alt={title}
|
|
283
|
+
className="relative block w-full"
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
286
|
+
{isLocked ? (
|
|
287
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
|
288
|
+
<div className="flex size-12 items-center justify-center rounded-full bg-black/60">
|
|
289
|
+
<LockIcon className="size-6 text-white" weight="regular" />
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
) : thumbnail && sourceType === 'image' && (
|
|
293
|
+
<img
|
|
294
|
+
src={source}
|
|
295
|
+
alt={title}
|
|
296
|
+
className={`absolute inset-0 h-full w-full object-cover transition-opacity duration-300 ${sourceReady ? 'opacity-100' : 'opacity-0'}`}
|
|
297
|
+
onLoad={() => setSourceReady(true)}
|
|
298
|
+
/>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
) : isLocked ? (
|
|
302
|
+
<div className={`relative overflow-hidden bg-black/5${!thumbnail ? ' aspect-video' : ''}`}>
|
|
303
|
+
{thumbnail && (
|
|
304
|
+
<img src={thumbnail} alt="" className="block w-full" />
|
|
305
|
+
)}
|
|
306
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
|
307
|
+
<div className="flex size-12 items-center justify-center rounded-full bg-black/60">
|
|
308
|
+
<LockIcon className="size-6 text-white" weight="regular" />
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
) : (
|
|
313
|
+
mediaPlayer
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{/* Footer */}
|
|
317
|
+
<div className="px-4 pb-3 pt-3">
|
|
318
|
+
<p className="mb-1.5 truncate text-base font-medium text-black">{title}</p>
|
|
319
|
+
|
|
320
|
+
<div className="flex items-center gap-1">
|
|
321
|
+
<TypeIcon className="size-5 shrink-0 text-black/55" weight="regular" />
|
|
322
|
+
{detail && (
|
|
323
|
+
<span className="text-xs font-medium text-black/55">{detail}</span>
|
|
324
|
+
)}
|
|
325
|
+
|
|
326
|
+
{isPurchased || source ? (
|
|
327
|
+
<>
|
|
328
|
+
<span className="text-xs font-medium text-black/55">•</span>
|
|
329
|
+
<span className="text-xs font-medium text-[#008236]">Purchased</span>
|
|
330
|
+
<CheckCircleIcon className="size-4 text-[#008236]" weight="bold" />
|
|
331
|
+
</>
|
|
332
|
+
) : amountText && (
|
|
333
|
+
<>
|
|
334
|
+
<span className="text-xs font-medium text-black/55">•</span>
|
|
335
|
+
<span className="text-xs font-medium text-black/55">{amountText}</span>
|
|
336
|
+
</>
|
|
337
|
+
)}
|
|
338
|
+
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
{isLocked ? (onUnlock || loading) && (
|
|
342
|
+
<button
|
|
343
|
+
type="button"
|
|
344
|
+
onClick={onUnlock}
|
|
345
|
+
disabled={loading}
|
|
346
|
+
className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-[#121110] px-4 text-sm font-medium leading-none text-white disabled:opacity-70"
|
|
347
|
+
>
|
|
348
|
+
{loading ? (
|
|
349
|
+
<span className="flex items-center gap-1">
|
|
350
|
+
<span className="size-1 rounded-full bg-white animate-bounce [animation-delay:-0.3s]" />
|
|
351
|
+
<span className="size-1 rounded-full bg-white animate-bounce [animation-delay:-0.15s]" />
|
|
352
|
+
<span className="size-1 rounded-full bg-white animate-bounce" />
|
|
353
|
+
</span>
|
|
354
|
+
) : (
|
|
355
|
+
<>
|
|
356
|
+
<LockIcon className="size-4" weight="fill" />
|
|
357
|
+
{isPurchased ? 'Open' : 'Unlock'}
|
|
358
|
+
</>
|
|
359
|
+
)}
|
|
360
|
+
</button>
|
|
361
|
+
) : onDownload && source && (
|
|
362
|
+
<a
|
|
363
|
+
href={`${source}${source.includes('?') ? '&' : '?'}d=true`}
|
|
364
|
+
target="_blank"
|
|
365
|
+
rel="noopener noreferrer"
|
|
366
|
+
onClick={onDownload}
|
|
367
|
+
className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-[#121110] px-4 text-sm font-medium leading-none !text-white"
|
|
368
|
+
>
|
|
369
|
+
<DownloadSimpleIcon className="size-4" weight="bold" />
|
|
370
|
+
Download
|
|
371
|
+
</a>
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export default LockedAttachmentCard
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { getDocumentIconType, getSourceType } from './mimeType'
|
|
2
|
+
|
|
3
|
+
describe('getSourceType', () => {
|
|
4
|
+
it('returns video for video/* types', () => {
|
|
5
|
+
expect(getSourceType('video/mp4')).toBe('video')
|
|
6
|
+
expect(getSourceType('video/webm')).toBe('video')
|
|
7
|
+
expect(getSourceType('video/quicktime')).toBe('video')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('returns audio for audio/* types', () => {
|
|
11
|
+
expect(getSourceType('audio/mpeg')).toBe('audio')
|
|
12
|
+
expect(getSourceType('audio/mp4')).toBe('audio')
|
|
13
|
+
expect(getSourceType('audio/ogg')).toBe('audio')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('returns image for image/* types', () => {
|
|
17
|
+
expect(getSourceType('image/jpeg')).toBe('image')
|
|
18
|
+
expect(getSourceType('image/png')).toBe('image')
|
|
19
|
+
expect(getSourceType('image/webp')).toBe('image')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('returns document for application/* types', () => {
|
|
23
|
+
expect(getSourceType('application/pdf')).toBe('document')
|
|
24
|
+
expect(getSourceType('application/msword')).toBe('document')
|
|
25
|
+
expect(getSourceType('application/zip')).toBe('document')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns document for text/* types', () => {
|
|
29
|
+
expect(getSourceType('text/plain')).toBe('document')
|
|
30
|
+
expect(getSourceType('text/csv')).toBe('document')
|
|
31
|
+
expect(getSourceType('text/markdown')).toBe('document')
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('getDocumentIconType', () => {
|
|
36
|
+
it('returns pdf for application/pdf', () => {
|
|
37
|
+
expect(getDocumentIconType('application/pdf')).toBe('pdf')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('returns doc for Word types', () => {
|
|
41
|
+
expect(getDocumentIconType('application/msword')).toBe('doc')
|
|
42
|
+
expect(getDocumentIconType('application/vnd.openxmlformats-officedocument.wordprocessingml.document')).toBe('doc')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('returns xls for Excel types', () => {
|
|
46
|
+
expect(getDocumentIconType('application/vnd.ms-excel')).toBe('xls')
|
|
47
|
+
expect(getDocumentIconType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')).toBe('xls')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('returns csv for text/csv', () => {
|
|
51
|
+
expect(getDocumentIconType('text/csv')).toBe('csv')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('returns ppt for PowerPoint types', () => {
|
|
55
|
+
expect(getDocumentIconType('application/vnd.ms-powerpoint')).toBe('ppt')
|
|
56
|
+
expect(getDocumentIconType('application/vnd.openxmlformats-officedocument.presentationml.presentation')).toBe('ppt')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('returns zip for archive types', () => {
|
|
60
|
+
expect(getDocumentIconType('application/zip')).toBe('zip')
|
|
61
|
+
expect(getDocumentIconType('application/x-zip-compressed')).toBe('zip')
|
|
62
|
+
expect(getDocumentIconType('application/gzip')).toBe('zip')
|
|
63
|
+
expect(getDocumentIconType('application/x-gzip')).toBe('zip')
|
|
64
|
+
expect(getDocumentIconType('application/x-rar-compressed')).toBe('zip')
|
|
65
|
+
expect(getDocumentIconType('application/x-7z-compressed')).toBe('zip')
|
|
66
|
+
expect(getDocumentIconType('application/x-tar')).toBe('zip')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('returns text for plain text and rtf', () => {
|
|
70
|
+
expect(getDocumentIconType('text/plain')).toBe('text')
|
|
71
|
+
expect(getDocumentIconType('text/rtf')).toBe('text')
|
|
72
|
+
expect(getDocumentIconType('application/rtf')).toBe('text')
|
|
73
|
+
expect(getDocumentIconType('application/x-rtf')).toBe('text')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('returns markdown for markdown types', () => {
|
|
77
|
+
expect(getDocumentIconType('text/markdown')).toBe('markdown')
|
|
78
|
+
expect(getDocumentIconType('text/x-markdown')).toBe('markdown')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('returns xls for macro-enabled Excel', () => {
|
|
82
|
+
expect(getDocumentIconType('application/vnd.ms-excel.sheet.macroEnabled.12')).toBe('xls')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('returns ppt for macro-enabled PowerPoint', () => {
|
|
86
|
+
expect(getDocumentIconType('application/vnd.ms-powerpoint.presentation.macroEnabled.12')).toBe('ppt')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('returns generic for unknown types', () => {
|
|
90
|
+
expect(getDocumentIconType('application/octet-stream')).toBe('generic')
|
|
91
|
+
expect(getDocumentIconType('application/json')).toBe('generic')
|
|
92
|
+
expect(getDocumentIconType('text/html')).toBe('generic')
|
|
93
|
+
expect(getDocumentIconType('application/vnd.rar')).toBe('generic')
|
|
94
|
+
expect(getDocumentIconType('application/vnd.oasis.opendocument.text')).toBe('generic')
|
|
95
|
+
expect(getDocumentIconType('application/vnd.oasis.opendocument.spreadsheet')).toBe('generic')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type AttachmentSourceType = 'image' | 'audio' | 'video' | 'document'
|
|
2
|
+
|
|
3
|
+
export type DocumentIconType =
|
|
4
|
+
| 'pdf'
|
|
5
|
+
| 'doc'
|
|
6
|
+
| 'xls'
|
|
7
|
+
| 'csv'
|
|
8
|
+
| 'ppt'
|
|
9
|
+
| 'zip'
|
|
10
|
+
| 'text'
|
|
11
|
+
| 'markdown'
|
|
12
|
+
| 'generic'
|
|
13
|
+
|
|
14
|
+
const DOCUMENT_ICON_PATTERNS: Array<[RegExp, DocumentIconType]> = [
|
|
15
|
+
[/pdf/, 'pdf'],
|
|
16
|
+
[/wordprocessingml|msword|\.doc/, 'doc'],
|
|
17
|
+
[/spreadsheetml|ms-excel|\.xls/, 'xls'],
|
|
18
|
+
[/csv/, 'csv'],
|
|
19
|
+
[/presentationml|ms-powerpoint|\.ppt/, 'ppt'],
|
|
20
|
+
[/zip|x-rar|x-7z|x-tar|x-gzip/, 'zip'],
|
|
21
|
+
[/plain|rtf/, 'text'],
|
|
22
|
+
[/markdown/, 'markdown'],
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
export function getSourceType(mimeType: string): AttachmentSourceType {
|
|
26
|
+
if (mimeType.startsWith('video/')) return 'video'
|
|
27
|
+
if (mimeType.startsWith('audio/')) return 'audio'
|
|
28
|
+
if (mimeType.startsWith('image/')) return 'image'
|
|
29
|
+
return 'document'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getDocumentIconType(mimeType: string): DocumentIconType {
|
|
33
|
+
const match = DOCUMENT_ICON_PATTERNS.find(([pattern]) => pattern.test(mimeType))
|
|
34
|
+
return match ? match[1] : 'generic'
|
|
35
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,12 +6,14 @@ export { MessagingShell } from './components/MessagingShell'
|
|
|
6
6
|
export { ChannelList } from './components/ChannelList'
|
|
7
7
|
export { ChannelView } from './components/ChannelView'
|
|
8
8
|
export { default as ActionButton } from './components/ActionButton'
|
|
9
|
+
export { default as LockedAttachmentCard } from './components/LockedAttachmentCard'
|
|
9
10
|
export { ParticipantPicker } from './components/ParticipantPicker'
|
|
10
11
|
export { Avatar } from './components/Avatar'
|
|
11
12
|
export { FaqList } from './components/FaqList'
|
|
12
13
|
export { FaqListItem } from './components/FaqList/FaqListItem'
|
|
13
14
|
export { ChannelEmptyState } from './components/MessagingShell/ChannelEmptyState'
|
|
14
15
|
export { MessageVoteButtons } from './components/CustomMessage/MessageVoteButtons'
|
|
16
|
+
export { isAttachmentMessage } from './components/CustomMessage/MessageTag'
|
|
15
17
|
|
|
16
18
|
// Providers
|
|
17
19
|
export { MessagingProvider } from './providers/MessagingProvider'
|
|
@@ -38,6 +40,8 @@ export type {
|
|
|
38
40
|
export type { MessageMetadata } from './stream-custom-data'
|
|
39
41
|
export type { AvatarProps } from './components/Avatar'
|
|
40
42
|
export type { ActionButtonProps } from './components/ActionButton'
|
|
43
|
+
export type { LockedAttachmentCardProps } from './components/LockedAttachmentCard'
|
|
44
|
+
export type { AttachmentSourceType } from './components/LockedAttachmentCard/mimeType'
|
|
41
45
|
export type { Faq, FaqListProps } from './components/FaqList'
|
|
42
46
|
export type { FaqListItemProps } from './components/FaqList/FaqListItem'
|
|
43
47
|
export type { VoteSelection } from './hooks/useMessageVote'
|
|
@@ -24,6 +24,7 @@ export type MessageCustomType =
|
|
|
24
24
|
| 'MESSAGE_TIP'
|
|
25
25
|
| 'MESSAGE_PAID'
|
|
26
26
|
| 'MESSAGE_CHATBOT'
|
|
27
|
+
| 'MESSAGE_ATTACHMENT'
|
|
27
28
|
| AgeSafetySystemType
|
|
28
29
|
| DmAgentSystemType
|
|
29
30
|
|
|
@@ -31,12 +32,18 @@ export type MessageCustomType =
|
|
|
31
32
|
* Message metadata for paid messaging and chatbot flows.
|
|
32
33
|
* Used to identify message types and payment status.
|
|
33
34
|
*/
|
|
35
|
+
export type PaymentStatus = 'pending' | 'paid' | 'failed' | 'refunded'
|
|
36
|
+
|
|
34
37
|
export interface MessageMetadata {
|
|
35
38
|
custom_type?: MessageCustomType
|
|
36
|
-
amount_text?: string
|
|
37
|
-
payment_status?: string
|
|
38
|
-
payment_intent_id?: string
|
|
39
39
|
listing_id?: string
|
|
40
|
+
amount_text?: string
|
|
41
|
+
payment_status?: PaymentStatus
|
|
42
|
+
// MESSAGE_ATTACHMENT
|
|
43
|
+
attachment_title?: string
|
|
44
|
+
attachment_mime_type?: string
|
|
45
|
+
attachment_thumbnail?: string
|
|
46
|
+
attachment_detail?: string
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
declare module 'stream-chat' {
|
package/src/types.ts
CHANGED
|
@@ -231,6 +231,19 @@ export interface ChannelViewProps {
|
|
|
231
231
|
messageNode: React.ReactElement,
|
|
232
232
|
message: LocalMessage
|
|
233
233
|
) => React.ReactNode
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Called when the visitor clicks Unlock on a locked attachment message.
|
|
237
|
+
* Receives the message and channel. Show checkout, confirm payment, fetch
|
|
238
|
+
* the unlocked URL. `attachment_source` must NOT be stored on the Stream message metadata.
|
|
239
|
+
* The card shows a loading state for the full duration of the promise.
|
|
240
|
+
*/
|
|
241
|
+
onAttachmentUnlock?: (message: LocalMessage, channel: Channel) => Promise<string>
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Called when the visitor clicks Download on an unlocked attachment message.
|
|
245
|
+
*/
|
|
246
|
+
onAttachmentDownload?: (message: LocalMessage, channel: Channel) => void
|
|
234
247
|
}
|
|
235
248
|
|
|
236
249
|
/**
|
|
@@ -254,6 +267,8 @@ export type ChannelViewPassthroughProps = Pick<
|
|
|
254
267
|
| 'customProfileContent'
|
|
255
268
|
| 'customChannelActions'
|
|
256
269
|
| 'renderMessage'
|
|
270
|
+
| 'onAttachmentUnlock'
|
|
271
|
+
| 'onAttachmentDownload'
|
|
257
272
|
>
|
|
258
273
|
|
|
259
274
|
/**
|