@linktr.ee/messaging-react 1.37.0 → 1.38.0-rc-1777617124
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-DwgUtqsA.js +127 -0
- package/dist/Card-DwgUtqsA.js.map +1 -0
- package/dist/Card-RgHsp9x1.js +138 -0
- package/dist/Card-RgHsp9x1.js.map +1 -0
- package/dist/{index-DOsC03ZN.js → index-B_4pciGp.js} +1411 -1358
- package/dist/index-B_4pciGp.js.map +1 -0
- package/dist/index.d.ts +21 -1
- package/dist/index.js +15 -13
- package/package.json +1 -1
- package/src/components/{LockedAttachment/components → AttachmentCard}/MediaPlayer.tsx +4 -3
- package/src/components/AttachmentCard/Thumbnail.tsx +150 -0
- package/src/components/AttachmentCard/index.tsx +114 -0
- package/src/components/LockedAttachment/components/Creator/Card.tsx +123 -113
- package/src/components/LockedAttachment/components/Visitor/Card.tsx +43 -42
- package/src/components/LockedAttachment/components/Visitor/LockBadge.tsx +12 -0
- package/src/components/MediaMessage/MediaMessage.stories.tsx +45 -4
- package/src/components/MediaMessage/MediaMessage.test.tsx +151 -153
- package/src/components/MediaMessage/index.tsx +239 -349
- package/src/index.ts +7 -3
- package/dist/Card-BHrnmHeu.js +0 -167
- package/dist/Card-BHrnmHeu.js.map +0 -1
- package/dist/Card-D4vEgqWt.js +0 -195
- package/dist/Card-D4vEgqWt.js.map +0 -1
- package/dist/index-DOsC03ZN.js.map +0 -1
- package/src/components/LockedAttachment/components/Creator/CardThumbnail.tsx +0 -114
- package/src/components/LockedAttachment/components/Visitor/CardThumbnail.tsx +0 -81
- /package/src/components/{LockedAttachment → AttachmentCard}/utils/icons.ts +0 -0
- /package/src/components/{LockedAttachment → AttachmentCard}/utils/mimeType.test.ts +0 -0
- /package/src/components/{LockedAttachment → AttachmentCard}/utils/mimeType.ts +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { CircleNotchIcon, DownloadSimpleIcon, LinkIcon
|
|
2
|
-
import React, {
|
|
1
|
+
import { CircleNotchIcon, DownloadSimpleIcon, LinkIcon } from '@phosphor-icons/react'
|
|
2
|
+
import React, { useState } from 'react'
|
|
3
3
|
import type { Attachment, LocalMessage } from 'stream-chat'
|
|
4
4
|
|
|
5
|
+
import AttachmentCard, {
|
|
6
|
+
AttachmentThumbnail,
|
|
7
|
+
getSourceType,
|
|
8
|
+
} from '../AttachmentCard'
|
|
5
9
|
import { Avatar } from '../Avatar'
|
|
6
|
-
import MediaPlayer from '../LockedAttachment/components/MediaPlayer'
|
|
7
|
-
import { renderTypeIcon } from '../LockedAttachment/utils/icons'
|
|
8
|
-
import { getSourceType } from '../LockedAttachment/utils/mimeType'
|
|
9
10
|
|
|
10
11
|
function formatBytes(bytes: number): string {
|
|
11
12
|
if (bytes < 1024) return `${bytes} B`
|
|
@@ -13,38 +14,94 @@ function formatBytes(bytes: number): string {
|
|
|
13
14
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
function
|
|
17
|
+
function linkCardShellClass(isMyMessage: boolean) {
|
|
17
18
|
const bg = isMyMessage ? 'bg-[#121110]' : 'bg-[#F3F3F1]'
|
|
18
19
|
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
|
}
|
|
20
21
|
|
|
21
|
-
function
|
|
22
|
+
function linkThumbnailBg(isMyMessage: boolean) {
|
|
23
|
+
return isMyMessage ? 'bg-white/10' : 'bg-black/5'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function linkPrimaryText(isMyMessage: boolean) {
|
|
22
27
|
return isMyMessage ? 'text-white' : 'text-black'
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
function
|
|
30
|
+
function linkSecondaryText(isMyMessage: boolean) {
|
|
26
31
|
return isMyMessage ? 'text-white/55' : 'text-black/55'
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
function
|
|
34
|
+
function linkTertiaryText(isMyMessage: boolean) {
|
|
30
35
|
return isMyMessage ? 'text-white/40' : 'text-black/40'
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
function
|
|
34
|
-
return isMyMessage ? 'bg-white/10' : 'bg-black/5'
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function iconColor(isMyMessage: boolean) {
|
|
38
|
+
function linkIconColor(isMyMessage: boolean) {
|
|
38
39
|
return isMyMessage ? 'text-white/20' : 'text-black/20'
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
/** Link preview (OG) — no download; opens in a new tab. */
|
|
43
|
+
const LinkCard: React.FC<{
|
|
44
|
+
attachment: Attachment
|
|
45
|
+
isMyMessage: boolean
|
|
46
|
+
}> = ({ attachment, isMyMessage }) => {
|
|
47
|
+
const { title, text, image_url, og_scrape_url, title_link } = attachment
|
|
48
|
+
const rawUrl = og_scrape_url ?? title_link
|
|
49
|
+
const url =
|
|
50
|
+
typeof rawUrl === 'string' && rawUrl.trim() !== '' ? rawUrl : undefined
|
|
51
|
+
|
|
52
|
+
const body = (
|
|
53
|
+
<React.Fragment>
|
|
54
|
+
<div className="p-2">
|
|
55
|
+
{image_url ? (
|
|
56
|
+
<img
|
|
57
|
+
src={image_url}
|
|
58
|
+
alt={title ?? ''}
|
|
59
|
+
className="aspect-video w-full rounded-[20px] object-cover"
|
|
60
|
+
/>
|
|
61
|
+
) : (
|
|
62
|
+
<div
|
|
63
|
+
className={`aspect-video w-full rounded-[20px] ${linkThumbnailBg(isMyMessage)} flex items-center justify-center`}
|
|
64
|
+
>
|
|
65
|
+
<LinkIcon className={`size-12 ${linkIconColor(isMyMessage)}`} />
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
<div className="px-3 pb-3">
|
|
70
|
+
{title && (
|
|
71
|
+
<p className={`truncate text-[14px] font-medium leading-5 ${linkPrimaryText(isMyMessage)}`}>
|
|
72
|
+
{title}
|
|
73
|
+
</p>
|
|
74
|
+
)}
|
|
75
|
+
{text && (
|
|
76
|
+
<p className={`truncate text-[12px] leading-4 ${linkSecondaryText(isMyMessage)}`}>{text}</p>
|
|
77
|
+
)}
|
|
78
|
+
{url && (
|
|
79
|
+
<p className={`mt-1 truncate text-[12px] leading-4 ${linkTertiaryText(isMyMessage)}`}>
|
|
80
|
+
{url}
|
|
81
|
+
</p>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</React.Fragment>
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if (url) {
|
|
88
|
+
return (
|
|
89
|
+
<a href={url} target="_blank" rel="noopener noreferrer" className="block no-underline">
|
|
90
|
+
{body}
|
|
91
|
+
</a>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return <div className="block">{body}</div>
|
|
43
96
|
}
|
|
44
97
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
98
|
+
export function resolveLinkAttachment(
|
|
99
|
+
message: LocalMessage
|
|
100
|
+
): Attachment | undefined {
|
|
101
|
+
return message.attachments?.find(
|
|
102
|
+
(a) => a.type === 'link' || (!!a.og_scrape_url && !a.asset_url)
|
|
103
|
+
)
|
|
104
|
+
}
|
|
48
105
|
|
|
49
106
|
async function triggerDownload(url: string, filename?: string): Promise<void> {
|
|
50
107
|
let name: string
|
|
@@ -64,16 +121,14 @@ async function triggerDownload(url: string, filename?: string): Promise<void> {
|
|
|
64
121
|
URL.revokeObjectURL(objectUrl)
|
|
65
122
|
}
|
|
66
123
|
|
|
67
|
-
const
|
|
124
|
+
const DownloadAction: React.FC<{ url: string; filename?: string }> = ({
|
|
68
125
|
url,
|
|
69
126
|
filename,
|
|
70
|
-
isMyMessage,
|
|
71
127
|
}) => {
|
|
72
128
|
const [busy, setBusy] = useState(false)
|
|
73
129
|
|
|
74
130
|
const handleClick = (e: React.MouseEvent) => {
|
|
75
131
|
e.stopPropagation()
|
|
76
|
-
// Open synchronously so the browser treats it as a user gesture (prevents popup blocking)
|
|
77
132
|
const fallback = window.open('', '_blank', 'noopener,noreferrer')
|
|
78
133
|
setBusy(true)
|
|
79
134
|
triggerDownload(url, filename)
|
|
@@ -87,245 +142,31 @@ const DownloadButton: React.FC<{ url: string; filename?: string; isMyMessage: bo
|
|
|
87
142
|
type="button"
|
|
88
143
|
onClick={handleClick}
|
|
89
144
|
disabled={busy}
|
|
90
|
-
className=
|
|
91
|
-
aria-label="Download"
|
|
145
|
+
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 hover:bg-[#2a2928] disabled:opacity-70"
|
|
92
146
|
>
|
|
93
147
|
{busy ? (
|
|
94
|
-
<CircleNotchIcon className=
|
|
148
|
+
<CircleNotchIcon className="size-4 animate-spin text-white" weight="bold" />
|
|
95
149
|
) : (
|
|
96
|
-
<
|
|
150
|
+
<React.Fragment>
|
|
151
|
+
<DownloadSimpleIcon className="size-4 text-white" weight="bold" />
|
|
152
|
+
Download
|
|
153
|
+
</React.Fragment>
|
|
97
154
|
)}
|
|
98
155
|
</button>
|
|
99
156
|
)
|
|
100
157
|
}
|
|
101
158
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const MediaMeta: React.FC<{
|
|
107
|
-
mimeType: string
|
|
159
|
+
export interface MediaMessageResolved {
|
|
160
|
+
resolvedUrl: string
|
|
161
|
+
resolvedType: string
|
|
108
162
|
title?: string
|
|
109
163
|
fileSize?: number
|
|
110
|
-
|
|
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">
|
|
115
|
-
{title && (
|
|
116
|
-
<p className={`truncate text-base font-medium ${primaryText(isMyMessage)}`}>
|
|
117
|
-
{title}
|
|
118
|
-
</p>
|
|
119
|
-
)}
|
|
120
|
-
{fileSize !== undefined && (
|
|
121
|
-
<div className="flex items-center gap-1">
|
|
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>
|
|
129
|
-
</div>
|
|
130
|
-
)}
|
|
131
|
-
</div>
|
|
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
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
const FileCard: React.FC<{
|
|
143
|
-
url: string
|
|
144
|
-
mimeType: string
|
|
145
|
-
title?: string
|
|
146
|
-
fileSize?: number
|
|
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
|
-
})}
|
|
158
|
-
</div>
|
|
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>
|
|
176
|
-
)}
|
|
177
|
-
<MediaMeta
|
|
178
|
-
mimeType={mimeType}
|
|
179
|
-
title={title}
|
|
180
|
-
fileSize={fileSize}
|
|
181
|
-
url={url}
|
|
182
|
-
isMyMessage={isMyMessage}
|
|
183
|
-
/>
|
|
184
|
-
</div>
|
|
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
|
-
)
|
|
164
|
+
thumbnailUrl?: string
|
|
309
165
|
}
|
|
310
166
|
|
|
311
|
-
|
|
312
|
-
// MediaMessage
|
|
313
|
-
// ---------------------------------------------------------------------------
|
|
314
|
-
|
|
315
|
-
export interface MediaMessageProps {
|
|
167
|
+
export function resolveMediaFromMessage(
|
|
316
168
|
message: LocalMessage
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
321
|
-
message,
|
|
322
|
-
isMyMessage = false,
|
|
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
|
-
)
|
|
169
|
+
): MediaMessageResolved | null {
|
|
329
170
|
const videoAttachment = message.attachments?.find(
|
|
330
171
|
(a) => a.type === 'video' && a.asset_url
|
|
331
172
|
)
|
|
@@ -348,6 +189,8 @@ export const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
348
189
|
audioAttachment?.asset_url ??
|
|
349
190
|
fileAttachment?.asset_url
|
|
350
191
|
|
|
192
|
+
if (!resolvedUrl) return null
|
|
193
|
+
|
|
351
194
|
const resolvedType =
|
|
352
195
|
activeAttachment?.mime_type ??
|
|
353
196
|
(activeAttachment?.type === 'image'
|
|
@@ -358,119 +201,166 @@ export const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
358
201
|
? 'audio/mpeg'
|
|
359
202
|
: 'application/octet-stream')
|
|
360
203
|
|
|
361
|
-
if (!linkAttachment && !resolvedUrl) return null
|
|
362
|
-
|
|
363
|
-
const sourceType = resolvedUrl ? getSourceType(resolvedType) : null
|
|
364
204
|
const title = (activeAttachment as { title?: string } | undefined)?.title
|
|
365
205
|
const fileSize = (activeAttachment as { file_size?: number } | undefined)?.file_size
|
|
366
206
|
const thumbnailUrl = (videoAttachment as { thumb_url?: string } | undefined)?.thumb_url
|
|
367
|
-
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
resolvedUrl,
|
|
210
|
+
resolvedType,
|
|
211
|
+
title,
|
|
212
|
+
fileSize,
|
|
213
|
+
thumbnailUrl,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Dark card (sent / own message) — matches LockedAttachment.Creator preview without toggle. */
|
|
218
|
+
const Creator: React.FC<MediaMessageResolved> = ({
|
|
219
|
+
resolvedUrl,
|
|
220
|
+
resolvedType,
|
|
221
|
+
title,
|
|
222
|
+
fileSize,
|
|
223
|
+
thumbnailUrl,
|
|
224
|
+
}) => {
|
|
225
|
+
const detail = fileSize !== undefined ? formatBytes(fileSize) : undefined
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<AttachmentCard
|
|
229
|
+
variant="dark"
|
|
230
|
+
title={title}
|
|
231
|
+
placeholderTitle=""
|
|
232
|
+
mimeType={resolvedType}
|
|
233
|
+
detail={detail}
|
|
234
|
+
thumbnail={
|
|
235
|
+
<AttachmentThumbnail
|
|
236
|
+
mimeType={resolvedType}
|
|
237
|
+
sourceUrl={resolvedUrl}
|
|
238
|
+
thumbnailUrl={thumbnailUrl}
|
|
239
|
+
title={title}
|
|
240
|
+
variant="dark"
|
|
241
|
+
/>
|
|
242
|
+
}
|
|
243
|
+
/>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Light card (received) — matches LockedAttachment.Visitor unlocked without Purchased copy. */
|
|
248
|
+
const Visitor: React.FC<MediaMessageResolved> = ({
|
|
249
|
+
resolvedUrl,
|
|
250
|
+
resolvedType,
|
|
251
|
+
title,
|
|
252
|
+
fileSize,
|
|
253
|
+
thumbnailUrl,
|
|
254
|
+
}) => {
|
|
255
|
+
const sourceType = getSourceType(resolvedType)
|
|
256
|
+
const detail = fileSize !== undefined ? formatBytes(fileSize) : undefined
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<AttachmentCard
|
|
260
|
+
variant="light"
|
|
261
|
+
title={title}
|
|
262
|
+
mimeType={resolvedType}
|
|
263
|
+
detail={detail}
|
|
264
|
+
thumbnail={
|
|
265
|
+
<AttachmentThumbnail
|
|
266
|
+
mimeType={resolvedType}
|
|
267
|
+
sourceUrl={resolvedUrl}
|
|
268
|
+
thumbnailUrl={thumbnailUrl}
|
|
269
|
+
title={title}
|
|
270
|
+
variant="light"
|
|
271
|
+
containedImage={sourceType === 'image' || sourceType === 'document'}
|
|
272
|
+
/>
|
|
273
|
+
}
|
|
274
|
+
action={<DownloadAction url={resolvedUrl} filename={title} />}
|
|
275
|
+
/>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface MediaMessageProps {
|
|
280
|
+
message: LocalMessage
|
|
281
|
+
isMyMessage?: boolean
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const MediaMessageRoot: React.FC<MediaMessageProps> = ({
|
|
285
|
+
message,
|
|
286
|
+
isMyMessage = false,
|
|
287
|
+
}) => {
|
|
288
|
+
const linkAttachment = resolveLinkAttachment(message)
|
|
289
|
+
const resolved = resolveMediaFromMessage(message)
|
|
290
|
+
if (!linkAttachment && !resolved) return null
|
|
368
291
|
|
|
369
292
|
const messageClass = isMyMessage
|
|
370
293
|
? 'str-chat__message str-chat__message-simple str-chat__message--me str-chat__message-simple--me'
|
|
371
294
|
: 'str-chat__message str-chat__message-simple str-chat__message--other'
|
|
372
295
|
|
|
373
|
-
// Only images, video (from poster), and PDFs support the full-screen viewer
|
|
374
|
-
const canView = sourceType === 'image' || sourceType === 'video' || isPdf
|
|
375
|
-
|
|
376
296
|
return (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
>
|
|
391
|
-
<div
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
<div className={
|
|
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!}
|
|
452
|
-
mimeType={resolvedType}
|
|
453
|
-
title={title}
|
|
454
|
-
fileSize={fileSize}
|
|
455
|
-
isMyMessage={isMyMessage}
|
|
456
|
-
onExpand={isPdf ? () => setViewerOpen(true) : undefined}
|
|
457
|
-
/>
|
|
458
|
-
)}
|
|
297
|
+
<div className={messageClass}>
|
|
298
|
+
{!isMyMessage && message.user && (
|
|
299
|
+
<Avatar
|
|
300
|
+
className="str-chat__avatar str-chat__message-sender-avatar"
|
|
301
|
+
id={message.user.id}
|
|
302
|
+
image={message.user.image}
|
|
303
|
+
name={message.user.name ?? message.user.id}
|
|
304
|
+
/>
|
|
305
|
+
)}
|
|
306
|
+
<div
|
|
307
|
+
className="str-chat__message-inner"
|
|
308
|
+
style={{ marginInlineEnd: 0, marginInlineStart: 0 }}
|
|
309
|
+
>
|
|
310
|
+
<div className="str-chat__message-bubble-wrapper">
|
|
311
|
+
<div
|
|
312
|
+
className="str-chat__message-bubble"
|
|
313
|
+
style={{ padding: 0, borderRadius: 0, overflow: 'visible', background: 'transparent' }}
|
|
314
|
+
>
|
|
315
|
+
{linkAttachment ? (
|
|
316
|
+
<div className={linkCardShellClass(isMyMessage)}>
|
|
317
|
+
<LinkCard attachment={linkAttachment} isMyMessage={isMyMessage} />
|
|
459
318
|
</div>
|
|
460
|
-
|
|
319
|
+
) : isMyMessage ? (
|
|
320
|
+
<Creator {...resolved!} />
|
|
321
|
+
) : (
|
|
322
|
+
<Visitor {...resolved!} />
|
|
323
|
+
)}
|
|
461
324
|
</div>
|
|
462
325
|
</div>
|
|
463
326
|
</div>
|
|
464
|
-
|
|
465
|
-
<MediaViewer
|
|
466
|
-
sourceType={sourceType!}
|
|
467
|
-
url={resolvedUrl}
|
|
468
|
-
mimeType={resolvedType}
|
|
469
|
-
title={title}
|
|
470
|
-
poster={thumbnailUrl}
|
|
471
|
-
onClose={() => setViewerOpen(false)}
|
|
472
|
-
/>
|
|
473
|
-
)}
|
|
474
|
-
</>
|
|
327
|
+
</div>
|
|
475
328
|
)
|
|
476
329
|
}
|
|
330
|
+
|
|
331
|
+
const MediaMessageCreatorEntry: React.FC<{ message: LocalMessage }> = ({
|
|
332
|
+
message,
|
|
333
|
+
}) => {
|
|
334
|
+
const linkAttachment = resolveLinkAttachment(message)
|
|
335
|
+
if (linkAttachment) {
|
|
336
|
+
return (
|
|
337
|
+
<div className={linkCardShellClass(true)}>
|
|
338
|
+
<LinkCard attachment={linkAttachment} isMyMessage={true} />
|
|
339
|
+
</div>
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
const resolved = resolveMediaFromMessage(message)
|
|
343
|
+
if (!resolved) return null
|
|
344
|
+
return <Creator {...resolved} />
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const MediaMessageVisitorEntry: React.FC<{ message: LocalMessage }> = ({
|
|
348
|
+
message,
|
|
349
|
+
}) => {
|
|
350
|
+
const linkAttachment = resolveLinkAttachment(message)
|
|
351
|
+
if (linkAttachment) {
|
|
352
|
+
return (
|
|
353
|
+
<div className={linkCardShellClass(false)}>
|
|
354
|
+
<LinkCard attachment={linkAttachment} isMyMessage={false} />
|
|
355
|
+
</div>
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
const resolved = resolveMediaFromMessage(message)
|
|
359
|
+
if (!resolved) return null
|
|
360
|
+
return <Visitor {...resolved} />
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export const MediaMessage = Object.assign(MediaMessageRoot, {
|
|
364
|
+
Creator: MediaMessageCreatorEntry,
|
|
365
|
+
Visitor: MediaMessageVisitorEntry,
|
|
366
|
+
})
|