@linktr.ee/messaging-react 1.33.1 → 1.33.2-rc-1777444067
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-Ddi8bg90.js → Card-CQYlmwhm.js} +2 -2
- package/dist/{Card-Ddi8bg90.js.map → Card-CQYlmwhm.js.map} +1 -1
- package/dist/{Card-DEe10CiS.js → Card-V-Et3tc4.js} +2 -2
- package/dist/{Card-DEe10CiS.js.map → Card-V-Et3tc4.js.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/{index-BePLvyvi.js → index-CahzrNJz.js} +1363 -1127
- package/dist/{index-BePLvyvi.js.map → index-CahzrNJz.js.map} +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/LockedAttachment/components/MediaPlayer.tsx +10 -1
- package/src/components/MediaMessage/MediaMessage.stories.tsx +82 -19
- package/src/components/MediaMessage/MediaMessage.test.tsx +277 -18
- package/src/components/MediaMessage/index.tsx +388 -77
- package/src/providers/MessagingProvider.stories.tsx +214 -0
- package/src/providers/MessagingProvider.test.tsx +126 -0
- package/src/styles.css +53 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { CircleNotchIcon, DownloadSimpleIcon, LinkIcon, XIcon } from '@phosphor-icons/react'
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
3
|
+
import type { Attachment, LocalMessage } from 'stream-chat'
|
|
3
4
|
|
|
4
5
|
import { Avatar } from '../Avatar'
|
|
5
6
|
import MediaPlayer from '../LockedAttachment/components/MediaPlayer'
|
|
@@ -12,53 +13,304 @@ function formatBytes(bytes: number): string {
|
|
|
12
13
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
function cardClass(isMyMessage: boolean) {
|
|
17
|
+
const bg = isMyMessage ? 'bg-[#121110]' : 'bg-[#F3F3F1]'
|
|
18
|
+
return `w-[280px] select-none overflow-hidden rounded-[24px] ${bg} shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function primaryText(isMyMessage: boolean) {
|
|
22
|
+
return isMyMessage ? 'text-white' : 'text-black'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function secondaryText(isMyMessage: boolean) {
|
|
26
|
+
return isMyMessage ? 'text-white/55' : 'text-black/55'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tertiaryText(isMyMessage: boolean) {
|
|
30
|
+
return isMyMessage ? 'text-white/40' : 'text-black/40'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function thumbnailBg(isMyMessage: boolean) {
|
|
34
|
+
return isMyMessage ? 'bg-white/10' : 'bg-black/5'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function iconColor(isMyMessage: boolean) {
|
|
38
|
+
return isMyMessage ? 'text-white/20' : 'text-black/20'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buttonBg(isMyMessage: boolean) {
|
|
42
|
+
return isMyMessage ? 'bg-white/10 hover:bg-white/15' : 'bg-black/[0.06] hover:bg-black/10'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Download button (shared across all media types)
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
async function triggerDownload(url: string, filename?: string): Promise<void> {
|
|
50
|
+
let name: string
|
|
51
|
+
try { name = filename ?? new URL(url).pathname.split('/').pop() ?? 'download' }
|
|
52
|
+
catch { name = filename ?? 'download' }
|
|
53
|
+
const res = await fetch(url, { mode: 'cors' })
|
|
54
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
55
|
+
const blob = await res.blob()
|
|
56
|
+
const objectUrl = URL.createObjectURL(blob)
|
|
57
|
+
const a = document.createElement('a')
|
|
58
|
+
a.href = objectUrl
|
|
59
|
+
a.download = name
|
|
60
|
+
a.style.display = 'none'
|
|
61
|
+
document.body.appendChild(a)
|
|
62
|
+
a.click()
|
|
63
|
+
document.body.removeChild(a)
|
|
64
|
+
URL.revokeObjectURL(objectUrl)
|
|
65
|
+
}
|
|
17
66
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
67
|
+
const DownloadButton: React.FC<{ url: string; filename?: string; isMyMessage: boolean }> = ({
|
|
68
|
+
url,
|
|
69
|
+
filename,
|
|
70
|
+
isMyMessage,
|
|
22
71
|
}) => {
|
|
23
|
-
|
|
72
|
+
const [busy, setBusy] = useState(false)
|
|
73
|
+
|
|
74
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
75
|
+
e.stopPropagation()
|
|
76
|
+
// Open synchronously so the browser treats it as a user gesture (prevents popup blocking)
|
|
77
|
+
const fallback = window.open('', '_blank', 'noopener,noreferrer')
|
|
78
|
+
setBusy(true)
|
|
79
|
+
triggerDownload(url, filename)
|
|
80
|
+
.then(() => { fallback?.close() })
|
|
81
|
+
.catch(() => { if (fallback) fallback.location.href = url })
|
|
82
|
+
.finally(() => setBusy(false))
|
|
83
|
+
}
|
|
84
|
+
|
|
24
85
|
return (
|
|
25
|
-
<
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={handleClick}
|
|
89
|
+
disabled={busy}
|
|
90
|
+
className={`flex size-8 items-center justify-center rounded-full ${buttonBg(isMyMessage)} disabled:opacity-50`}
|
|
91
|
+
aria-label="Download"
|
|
92
|
+
>
|
|
93
|
+
{busy ? (
|
|
94
|
+
<CircleNotchIcon className={`size-4 animate-spin ${secondaryText(isMyMessage)}`} weight="bold" />
|
|
95
|
+
) : (
|
|
96
|
+
<DownloadSimpleIcon className={`size-4 ${secondaryText(isMyMessage)}`} weight="bold" />
|
|
97
|
+
)}
|
|
98
|
+
</button>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Chin: title + file size + download
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
const MediaMeta: React.FC<{
|
|
107
|
+
mimeType: string
|
|
108
|
+
title?: string
|
|
109
|
+
fileSize?: number
|
|
110
|
+
url: string
|
|
111
|
+
isMyMessage: boolean
|
|
112
|
+
}> = ({ mimeType, title, fileSize, url, isMyMessage }) => (
|
|
113
|
+
<div className="flex items-start gap-2 px-4 pb-3 pt-3">
|
|
114
|
+
<div className="min-w-0 flex-1">
|
|
26
115
|
{title && (
|
|
27
|
-
<p className=
|
|
116
|
+
<p className={`mb-1.5 truncate text-base font-medium ${primaryText(isMyMessage)}`}>
|
|
117
|
+
{title}
|
|
118
|
+
</p>
|
|
28
119
|
)}
|
|
29
120
|
{fileSize !== undefined && (
|
|
30
121
|
<div className="flex items-center gap-1">
|
|
31
|
-
{renderTypeIcon(mimeType, {
|
|
32
|
-
|
|
122
|
+
{renderTypeIcon(mimeType, {
|
|
123
|
+
className: `size-5 shrink-0 ${secondaryText(isMyMessage)}`,
|
|
124
|
+
weight: 'regular',
|
|
125
|
+
})}
|
|
126
|
+
<span className={`text-xs font-medium ${secondaryText(isMyMessage)}`}>
|
|
127
|
+
{formatBytes(fileSize)}
|
|
128
|
+
</span>
|
|
33
129
|
</div>
|
|
34
130
|
)}
|
|
35
131
|
</div>
|
|
36
|
-
|
|
37
|
-
}
|
|
132
|
+
<div className="flex shrink-0 items-center gap-1 pt-0.5">
|
|
133
|
+
<DownloadButton url={url} filename={title} isMyMessage={isMyMessage} />
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Document card: PDF (click → viewer) or unknown (link only)
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
38
141
|
|
|
39
142
|
const FileCard: React.FC<{
|
|
40
143
|
url: string
|
|
41
144
|
mimeType: string
|
|
42
145
|
title?: string
|
|
43
146
|
fileSize?: number
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
147
|
+
isMyMessage: boolean
|
|
148
|
+
onExpand?: () => void
|
|
149
|
+
}> = ({ url, mimeType, title, fileSize, isMyMessage, onExpand }) => {
|
|
150
|
+
const thumbnailContent = (
|
|
151
|
+
<div
|
|
152
|
+
className={`aspect-video w-full ${thumbnailBg(isMyMessage)} flex items-center justify-center`}
|
|
153
|
+
>
|
|
154
|
+
{renderTypeIcon(mimeType, {
|
|
155
|
+
className: `size-12 ${iconColor(isMyMessage)}`,
|
|
156
|
+
weight: 'regular',
|
|
157
|
+
})}
|
|
48
158
|
</div>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div>
|
|
163
|
+
{onExpand ? (
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onClick={onExpand}
|
|
167
|
+
className="block w-full"
|
|
168
|
+
aria-label="Open PDF viewer"
|
|
169
|
+
>
|
|
170
|
+
{thumbnailContent}
|
|
171
|
+
</button>
|
|
172
|
+
) : (
|
|
173
|
+
<a href={url} target="_blank" rel="noopener noreferrer" className="block no-underline">
|
|
174
|
+
{thumbnailContent}
|
|
175
|
+
</a>
|
|
58
176
|
)}
|
|
177
|
+
<MediaMeta
|
|
178
|
+
mimeType={mimeType}
|
|
179
|
+
title={title}
|
|
180
|
+
fileSize={fileSize}
|
|
181
|
+
url={url}
|
|
182
|
+
isMyMessage={isMyMessage}
|
|
183
|
+
/>
|
|
59
184
|
</div>
|
|
60
|
-
|
|
61
|
-
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Link preview card (no download / no viewer)
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
const LinkCard: React.FC<{
|
|
193
|
+
attachment: Attachment
|
|
194
|
+
isMyMessage: boolean
|
|
195
|
+
}> = ({ attachment, isMyMessage }) => {
|
|
196
|
+
const { title, text, image_url, og_scrape_url, title_link } = attachment
|
|
197
|
+
const url = og_scrape_url ?? title_link
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<a href={url} target="_blank" rel="noopener noreferrer" className="block no-underline">
|
|
201
|
+
<div className="p-2">
|
|
202
|
+
{image_url ? (
|
|
203
|
+
<img
|
|
204
|
+
src={image_url}
|
|
205
|
+
alt={title ?? ''}
|
|
206
|
+
className="aspect-video w-full rounded-[20px] object-cover"
|
|
207
|
+
/>
|
|
208
|
+
) : (
|
|
209
|
+
<div
|
|
210
|
+
className={`aspect-video w-full rounded-[20px] ${thumbnailBg(isMyMessage)} flex items-center justify-center`}
|
|
211
|
+
>
|
|
212
|
+
<LinkIcon className={`size-12 ${iconColor(isMyMessage)}`} />
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
<div className="px-3 pb-3">
|
|
217
|
+
{title && (
|
|
218
|
+
<p className={`truncate text-[14px] font-medium leading-5 ${primaryText(isMyMessage)}`}>
|
|
219
|
+
{title}
|
|
220
|
+
</p>
|
|
221
|
+
)}
|
|
222
|
+
{text && (
|
|
223
|
+
<p className={`truncate text-[12px] leading-4 ${secondaryText(isMyMessage)}`}>{text}</p>
|
|
224
|
+
)}
|
|
225
|
+
{url && (
|
|
226
|
+
<p className={`mt-1 truncate text-[12px] leading-4 ${tertiaryText(isMyMessage)}`}>
|
|
227
|
+
{url}
|
|
228
|
+
</p>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</a>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Full-screen media viewer (image, video, PDF)
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
const MediaViewer: React.FC<{
|
|
240
|
+
sourceType: string
|
|
241
|
+
url: string
|
|
242
|
+
mimeType: string
|
|
243
|
+
title?: string
|
|
244
|
+
poster?: string
|
|
245
|
+
onClose: () => void
|
|
246
|
+
}> = ({ sourceType, url, mimeType, title, poster, onClose }) => {
|
|
247
|
+
const dialogRef = useRef<HTMLDialogElement>(null)
|
|
248
|
+
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
const el = dialogRef.current
|
|
251
|
+
el?.showModal()
|
|
252
|
+
return () => {
|
|
253
|
+
if (el?.open) el.close()
|
|
254
|
+
}
|
|
255
|
+
}, [])
|
|
256
|
+
|
|
257
|
+
const handleClick = (e: React.MouseEvent<HTMLDialogElement>) => {
|
|
258
|
+
if (e.target === dialogRef.current) onClose()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const isPdf = mimeType === 'application/pdf'
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
|
|
265
|
+
<dialog
|
|
266
|
+
ref={dialogRef}
|
|
267
|
+
className="mes-media-viewer"
|
|
268
|
+
onClose={onClose}
|
|
269
|
+
onClick={handleClick}
|
|
270
|
+
>
|
|
271
|
+
<div className="relative flex h-full w-full items-center justify-center p-6">
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
onClick={onClose}
|
|
275
|
+
className="absolute right-4 top-4 z-10 flex size-10 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20"
|
|
276
|
+
aria-label="Close"
|
|
277
|
+
>
|
|
278
|
+
<XIcon className="size-5" weight="bold" />
|
|
279
|
+
</button>
|
|
280
|
+
|
|
281
|
+
{sourceType === 'image' ? (
|
|
282
|
+
<img
|
|
283
|
+
src={url}
|
|
284
|
+
alt={title ?? ''}
|
|
285
|
+
className="max-h-[90vh] max-w-[90vw] rounded-2xl object-contain"
|
|
286
|
+
/>
|
|
287
|
+
) : isPdf ? (
|
|
288
|
+
<div className="flex h-full w-full max-w-4xl flex-col pt-14">
|
|
289
|
+
{title && (
|
|
290
|
+
<p className="mb-2 shrink-0 truncate text-sm font-medium text-white/70">{title}</p>
|
|
291
|
+
)}
|
|
292
|
+
<iframe
|
|
293
|
+
src={url}
|
|
294
|
+
title={title ?? 'Document'}
|
|
295
|
+
className="w-full flex-1 rounded-xl"
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
298
|
+
) : (
|
|
299
|
+
<div className="w-full max-w-2xl">
|
|
300
|
+
{title && (
|
|
301
|
+
<p className="mb-4 text-center text-sm font-medium text-white/70">{title}</p>
|
|
302
|
+
)}
|
|
303
|
+
<MediaPlayer source={url} mimeType={mimeType} poster={poster} controls />
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
307
|
+
</dialog>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// MediaMessage
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
62
314
|
|
|
63
315
|
export interface MediaMessageProps {
|
|
64
316
|
message: LocalMessage
|
|
@@ -69,6 +321,11 @@ export const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
69
321
|
message,
|
|
70
322
|
isMyMessage = false,
|
|
71
323
|
}) => {
|
|
324
|
+
const [viewerOpen, setViewerOpen] = useState(false)
|
|
325
|
+
|
|
326
|
+
const linkAttachment = message.attachments?.find(
|
|
327
|
+
(a) => a.type === 'link' || (a.og_scrape_url && !a.asset_url)
|
|
328
|
+
)
|
|
72
329
|
const videoAttachment = message.attachments?.find(
|
|
73
330
|
(a) => a.type === 'video' && a.asset_url
|
|
74
331
|
)
|
|
@@ -93,73 +350,127 @@ export const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
93
350
|
|
|
94
351
|
const resolvedType =
|
|
95
352
|
activeAttachment?.mime_type ??
|
|
96
|
-
(
|
|
353
|
+
(activeAttachment?.type === 'image'
|
|
97
354
|
? 'image/jpeg'
|
|
98
|
-
:
|
|
355
|
+
: activeAttachment?.type === 'video'
|
|
99
356
|
? 'video/mp4'
|
|
100
|
-
:
|
|
357
|
+
: activeAttachment?.type === 'audio'
|
|
101
358
|
? 'audio/mpeg'
|
|
102
359
|
: 'application/octet-stream')
|
|
103
360
|
|
|
104
|
-
if (!resolvedUrl) return null
|
|
361
|
+
if (!linkAttachment && !resolvedUrl) return null
|
|
105
362
|
|
|
106
|
-
const sourceType = getSourceType(resolvedType)
|
|
363
|
+
const sourceType = resolvedUrl ? getSourceType(resolvedType) : null
|
|
107
364
|
const title = (activeAttachment as { title?: string } | undefined)?.title
|
|
108
365
|
const fileSize = (activeAttachment as { file_size?: number } | undefined)?.file_size
|
|
109
366
|
const thumbnailUrl = (videoAttachment as { thumb_url?: string } | undefined)?.thumb_url
|
|
367
|
+
const isPdf = resolvedType === 'application/pdf'
|
|
110
368
|
|
|
111
369
|
const messageClass = isMyMessage
|
|
112
370
|
? 'str-chat__message str-chat__message-simple str-chat__message--me str-chat__message-simple--me'
|
|
113
371
|
: 'str-chat__message str-chat__message-simple str-chat__message--other'
|
|
114
372
|
|
|
373
|
+
// Only images, video (from poster), and PDFs support the full-screen viewer
|
|
374
|
+
const canView = sourceType === 'image' || sourceType === 'video' || isPdf
|
|
375
|
+
|
|
115
376
|
return (
|
|
116
|
-
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
<div className="str-chat__message-bubble"
|
|
131
|
-
<div
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
377
|
+
<>
|
|
378
|
+
<div className={messageClass}>
|
|
379
|
+
{!isMyMessage && message.user && (
|
|
380
|
+
<Avatar
|
|
381
|
+
className="str-chat__avatar str-chat__message-sender-avatar"
|
|
382
|
+
id={message.user.id}
|
|
383
|
+
image={message.user.image}
|
|
384
|
+
name={message.user.name ?? message.user.id}
|
|
385
|
+
/>
|
|
386
|
+
)}
|
|
387
|
+
<div
|
|
388
|
+
className="str-chat__message-inner"
|
|
389
|
+
style={{ marginInlineEnd: 0, marginInlineStart: 0 }}
|
|
390
|
+
>
|
|
391
|
+
<div className="str-chat__message-bubble-wrapper">
|
|
392
|
+
<div
|
|
393
|
+
className="str-chat__message-bubble"
|
|
394
|
+
style={{ padding: 0, borderRadius: 0, overflow: 'visible', background: 'transparent' }}
|
|
395
|
+
>
|
|
396
|
+
<div className={cardClass(isMyMessage)}>
|
|
397
|
+
{linkAttachment ? (
|
|
398
|
+
<LinkCard attachment={linkAttachment} isMyMessage={isMyMessage} />
|
|
399
|
+
) : sourceType === 'image' ? (
|
|
400
|
+
<>
|
|
401
|
+
<button
|
|
402
|
+
type="button"
|
|
403
|
+
onClick={() => setViewerOpen(true)}
|
|
404
|
+
className="block w-full cursor-zoom-in"
|
|
405
|
+
>
|
|
406
|
+
<img src={resolvedUrl!} alt={title ?? ''} className="block w-full" />
|
|
407
|
+
</button>
|
|
408
|
+
<MediaMeta
|
|
409
|
+
mimeType={resolvedType}
|
|
410
|
+
title={title}
|
|
411
|
+
fileSize={fileSize}
|
|
412
|
+
url={resolvedUrl!}
|
|
413
|
+
isMyMessage={isMyMessage}
|
|
414
|
+
/>
|
|
415
|
+
</>
|
|
416
|
+
) : sourceType === 'video' ? (
|
|
417
|
+
<>
|
|
418
|
+
<MediaPlayer
|
|
419
|
+
source={resolvedUrl!}
|
|
420
|
+
mimeType={resolvedType}
|
|
421
|
+
poster={thumbnailUrl}
|
|
422
|
+
controls
|
|
423
|
+
onContainerClick={() => setViewerOpen(true)}
|
|
424
|
+
/>
|
|
425
|
+
<MediaMeta
|
|
426
|
+
mimeType={resolvedType}
|
|
427
|
+
title={title}
|
|
428
|
+
fileSize={fileSize}
|
|
429
|
+
url={resolvedUrl!}
|
|
430
|
+
isMyMessage={isMyMessage}
|
|
431
|
+
/>
|
|
432
|
+
</>
|
|
433
|
+
) : sourceType === 'audio' ? (
|
|
434
|
+
<>
|
|
435
|
+
<MediaPlayer
|
|
436
|
+
source={resolvedUrl!}
|
|
437
|
+
mimeType={resolvedType}
|
|
438
|
+
controls
|
|
439
|
+
/>
|
|
440
|
+
<MediaMeta
|
|
441
|
+
mimeType={resolvedType}
|
|
442
|
+
title={title}
|
|
443
|
+
fileSize={fileSize}
|
|
444
|
+
url={resolvedUrl!}
|
|
445
|
+
isMyMessage={isMyMessage}
|
|
446
|
+
/>
|
|
447
|
+
</>
|
|
448
|
+
) : (
|
|
449
|
+
// document: PDF gets viewer, unknown gets link-only thumbnail
|
|
450
|
+
<FileCard
|
|
451
|
+
url={resolvedUrl!}
|
|
152
452
|
mimeType={resolvedType}
|
|
153
|
-
|
|
154
|
-
|
|
453
|
+
title={title}
|
|
454
|
+
fileSize={fileSize}
|
|
455
|
+
isMyMessage={isMyMessage}
|
|
456
|
+
onExpand={isPdf ? () => setViewerOpen(true) : undefined}
|
|
155
457
|
/>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)}
|
|
458
|
+
)}
|
|
459
|
+
</div>
|
|
159
460
|
</div>
|
|
160
461
|
</div>
|
|
161
462
|
</div>
|
|
162
463
|
</div>
|
|
163
|
-
|
|
464
|
+
{viewerOpen && canView && resolvedUrl && (
|
|
465
|
+
<MediaViewer
|
|
466
|
+
sourceType={sourceType!}
|
|
467
|
+
url={resolvedUrl}
|
|
468
|
+
mimeType={resolvedType}
|
|
469
|
+
title={title}
|
|
470
|
+
poster={thumbnailUrl}
|
|
471
|
+
onClose={() => setViewerOpen(false)}
|
|
472
|
+
/>
|
|
473
|
+
)}
|
|
474
|
+
</>
|
|
164
475
|
)
|
|
165
476
|
}
|