@linktr.ee/messaging-react 1.37.0 → 1.38.0-rc-1777583423
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-BlXnKGaR.js +127 -0
- package/dist/Card-BlXnKGaR.js.map +1 -0
- package/dist/Card-DoNJA-jg.js +138 -0
- package/dist/Card-DoNJA-jg.js.map +1 -0
- package/dist/{index-DOsC03ZN.js → index-jnKl3mQ0.js} +1404 -1352
- package/dist/index-jnKl3mQ0.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 +112 -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 +125 -160
- package/src/components/MediaMessage/index.tsx +226 -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,82 @@ 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 url = og_scrape_url ?? title_link
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<a href={url} target="_blank" rel="noopener noreferrer" className="block no-underline">
|
|
52
|
+
<div className="p-2">
|
|
53
|
+
{image_url ? (
|
|
54
|
+
<img
|
|
55
|
+
src={image_url}
|
|
56
|
+
alt={title ?? ''}
|
|
57
|
+
className="aspect-video w-full rounded-[20px] object-cover"
|
|
58
|
+
/>
|
|
59
|
+
) : (
|
|
60
|
+
<div
|
|
61
|
+
className={`aspect-video w-full rounded-[20px] ${linkThumbnailBg(isMyMessage)} flex items-center justify-center`}
|
|
62
|
+
>
|
|
63
|
+
<LinkIcon className={`size-12 ${linkIconColor(isMyMessage)}`} />
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
<div className="px-3 pb-3">
|
|
68
|
+
{title && (
|
|
69
|
+
<p className={`truncate text-[14px] font-medium leading-5 ${linkPrimaryText(isMyMessage)}`}>
|
|
70
|
+
{title}
|
|
71
|
+
</p>
|
|
72
|
+
)}
|
|
73
|
+
{text && (
|
|
74
|
+
<p className={`truncate text-[12px] leading-4 ${linkSecondaryText(isMyMessage)}`}>{text}</p>
|
|
75
|
+
)}
|
|
76
|
+
{url && (
|
|
77
|
+
<p className={`mt-1 truncate text-[12px] leading-4 ${linkTertiaryText(isMyMessage)}`}>
|
|
78
|
+
{url}
|
|
79
|
+
</p>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
</a>
|
|
83
|
+
)
|
|
43
84
|
}
|
|
44
85
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
86
|
+
export function resolveLinkAttachment(
|
|
87
|
+
message: LocalMessage
|
|
88
|
+
): Attachment | undefined {
|
|
89
|
+
return message.attachments?.find(
|
|
90
|
+
(a) => a.type === 'link' || (a.og_scrape_url != null && !a.asset_url)
|
|
91
|
+
)
|
|
92
|
+
}
|
|
48
93
|
|
|
49
94
|
async function triggerDownload(url: string, filename?: string): Promise<void> {
|
|
50
95
|
let name: string
|
|
@@ -64,16 +109,14 @@ async function triggerDownload(url: string, filename?: string): Promise<void> {
|
|
|
64
109
|
URL.revokeObjectURL(objectUrl)
|
|
65
110
|
}
|
|
66
111
|
|
|
67
|
-
const
|
|
112
|
+
const DownloadAction: React.FC<{ url: string; filename?: string }> = ({
|
|
68
113
|
url,
|
|
69
114
|
filename,
|
|
70
|
-
isMyMessage,
|
|
71
115
|
}) => {
|
|
72
116
|
const [busy, setBusy] = useState(false)
|
|
73
117
|
|
|
74
118
|
const handleClick = (e: React.MouseEvent) => {
|
|
75
119
|
e.stopPropagation()
|
|
76
|
-
// Open synchronously so the browser treats it as a user gesture (prevents popup blocking)
|
|
77
120
|
const fallback = window.open('', '_blank', 'noopener,noreferrer')
|
|
78
121
|
setBusy(true)
|
|
79
122
|
triggerDownload(url, filename)
|
|
@@ -87,245 +130,31 @@ const DownloadButton: React.FC<{ url: string; filename?: string; isMyMessage: bo
|
|
|
87
130
|
type="button"
|
|
88
131
|
onClick={handleClick}
|
|
89
132
|
disabled={busy}
|
|
90
|
-
className=
|
|
91
|
-
aria-label="Download"
|
|
133
|
+
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
134
|
>
|
|
93
135
|
{busy ? (
|
|
94
|
-
<CircleNotchIcon className=
|
|
136
|
+
<CircleNotchIcon className="size-4 animate-spin text-white" weight="bold" />
|
|
95
137
|
) : (
|
|
96
|
-
<
|
|
138
|
+
<React.Fragment>
|
|
139
|
+
<DownloadSimpleIcon className="size-4 text-white" weight="bold" />
|
|
140
|
+
Download
|
|
141
|
+
</React.Fragment>
|
|
97
142
|
)}
|
|
98
143
|
</button>
|
|
99
144
|
)
|
|
100
145
|
}
|
|
101
146
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const MediaMeta: React.FC<{
|
|
107
|
-
mimeType: string
|
|
147
|
+
export interface MediaMessageResolved {
|
|
148
|
+
resolvedUrl: string
|
|
149
|
+
resolvedType: string
|
|
108
150
|
title?: string
|
|
109
151
|
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
|
-
)
|
|
152
|
+
thumbnailUrl?: string
|
|
233
153
|
}
|
|
234
154
|
|
|
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
|
-
// ---------------------------------------------------------------------------
|
|
314
|
-
|
|
315
|
-
export interface MediaMessageProps {
|
|
155
|
+
export function resolveMediaFromMessage(
|
|
316
156
|
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
|
-
)
|
|
157
|
+
): MediaMessageResolved | null {
|
|
329
158
|
const videoAttachment = message.attachments?.find(
|
|
330
159
|
(a) => a.type === 'video' && a.asset_url
|
|
331
160
|
)
|
|
@@ -348,6 +177,8 @@ export const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
348
177
|
audioAttachment?.asset_url ??
|
|
349
178
|
fileAttachment?.asset_url
|
|
350
179
|
|
|
180
|
+
if (!resolvedUrl) return null
|
|
181
|
+
|
|
351
182
|
const resolvedType =
|
|
352
183
|
activeAttachment?.mime_type ??
|
|
353
184
|
(activeAttachment?.type === 'image'
|
|
@@ -358,119 +189,165 @@ export const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
358
189
|
? 'audio/mpeg'
|
|
359
190
|
: 'application/octet-stream')
|
|
360
191
|
|
|
361
|
-
if (!linkAttachment && !resolvedUrl) return null
|
|
362
|
-
|
|
363
|
-
const sourceType = resolvedUrl ? getSourceType(resolvedType) : null
|
|
364
192
|
const title = (activeAttachment as { title?: string } | undefined)?.title
|
|
365
193
|
const fileSize = (activeAttachment as { file_size?: number } | undefined)?.file_size
|
|
366
194
|
const thumbnailUrl = (videoAttachment as { thumb_url?: string } | undefined)?.thumb_url
|
|
367
|
-
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
resolvedUrl,
|
|
198
|
+
resolvedType,
|
|
199
|
+
title,
|
|
200
|
+
fileSize,
|
|
201
|
+
thumbnailUrl,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Dark card (sent / own message) — matches LockedAttachment.Creator preview without toggle. */
|
|
206
|
+
const Creator: React.FC<MediaMessageResolved> = ({
|
|
207
|
+
resolvedUrl,
|
|
208
|
+
resolvedType,
|
|
209
|
+
title,
|
|
210
|
+
fileSize,
|
|
211
|
+
thumbnailUrl,
|
|
212
|
+
}) => {
|
|
213
|
+
const detail = fileSize !== undefined ? formatBytes(fileSize) : undefined
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<AttachmentCard
|
|
217
|
+
variant="dark"
|
|
218
|
+
title={title}
|
|
219
|
+
mimeType={resolvedType}
|
|
220
|
+
detail={detail}
|
|
221
|
+
thumbnail={
|
|
222
|
+
<AttachmentThumbnail
|
|
223
|
+
mimeType={resolvedType}
|
|
224
|
+
sourceUrl={resolvedUrl}
|
|
225
|
+
thumbnailUrl={thumbnailUrl}
|
|
226
|
+
title={title}
|
|
227
|
+
variant="dark"
|
|
228
|
+
/>
|
|
229
|
+
}
|
|
230
|
+
/>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Light card (received) — matches LockedAttachment.Visitor unlocked without Purchased copy. */
|
|
235
|
+
const Visitor: React.FC<MediaMessageResolved> = ({
|
|
236
|
+
resolvedUrl,
|
|
237
|
+
resolvedType,
|
|
238
|
+
title,
|
|
239
|
+
fileSize,
|
|
240
|
+
thumbnailUrl,
|
|
241
|
+
}) => {
|
|
242
|
+
const sourceType = getSourceType(resolvedType)
|
|
243
|
+
const detail = fileSize !== undefined ? formatBytes(fileSize) : undefined
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<AttachmentCard
|
|
247
|
+
variant="light"
|
|
248
|
+
title={title}
|
|
249
|
+
mimeType={resolvedType}
|
|
250
|
+
detail={detail}
|
|
251
|
+
thumbnail={
|
|
252
|
+
<AttachmentThumbnail
|
|
253
|
+
mimeType={resolvedType}
|
|
254
|
+
sourceUrl={resolvedUrl}
|
|
255
|
+
thumbnailUrl={thumbnailUrl}
|
|
256
|
+
title={title}
|
|
257
|
+
variant="light"
|
|
258
|
+
containedImage={sourceType === 'image' || sourceType === 'document'}
|
|
259
|
+
/>
|
|
260
|
+
}
|
|
261
|
+
action={<DownloadAction url={resolvedUrl} filename={title} />}
|
|
262
|
+
/>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface MediaMessageProps {
|
|
267
|
+
message: LocalMessage
|
|
268
|
+
isMyMessage?: boolean
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const MediaMessageRoot: React.FC<MediaMessageProps> = ({
|
|
272
|
+
message,
|
|
273
|
+
isMyMessage = false,
|
|
274
|
+
}) => {
|
|
275
|
+
const linkAttachment = resolveLinkAttachment(message)
|
|
276
|
+
const resolved = resolveMediaFromMessage(message)
|
|
277
|
+
if (!linkAttachment && !resolved) return null
|
|
368
278
|
|
|
369
279
|
const messageClass = isMyMessage
|
|
370
280
|
? 'str-chat__message str-chat__message-simple str-chat__message--me str-chat__message-simple--me'
|
|
371
281
|
: 'str-chat__message str-chat__message-simple str-chat__message--other'
|
|
372
282
|
|
|
373
|
-
// Only images, video (from poster), and PDFs support the full-screen viewer
|
|
374
|
-
const canView = sourceType === 'image' || sourceType === 'video' || isPdf
|
|
375
|
-
|
|
376
283
|
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
|
-
)}
|
|
284
|
+
<div className={messageClass}>
|
|
285
|
+
{!isMyMessage && message.user && (
|
|
286
|
+
<Avatar
|
|
287
|
+
className="str-chat__avatar str-chat__message-sender-avatar"
|
|
288
|
+
id={message.user.id}
|
|
289
|
+
image={message.user.image}
|
|
290
|
+
name={message.user.name ?? message.user.id}
|
|
291
|
+
/>
|
|
292
|
+
)}
|
|
293
|
+
<div
|
|
294
|
+
className="str-chat__message-inner"
|
|
295
|
+
style={{ marginInlineEnd: 0, marginInlineStart: 0 }}
|
|
296
|
+
>
|
|
297
|
+
<div className="str-chat__message-bubble-wrapper">
|
|
298
|
+
<div
|
|
299
|
+
className="str-chat__message-bubble"
|
|
300
|
+
style={{ padding: 0, borderRadius: 0, overflow: 'visible', background: 'transparent' }}
|
|
301
|
+
>
|
|
302
|
+
{linkAttachment ? (
|
|
303
|
+
<div className={linkCardShellClass(isMyMessage)}>
|
|
304
|
+
<LinkCard attachment={linkAttachment} isMyMessage={isMyMessage} />
|
|
459
305
|
</div>
|
|
460
|
-
|
|
306
|
+
) : isMyMessage ? (
|
|
307
|
+
<Creator {...resolved!} />
|
|
308
|
+
) : (
|
|
309
|
+
<Visitor {...resolved!} />
|
|
310
|
+
)}
|
|
461
311
|
</div>
|
|
462
312
|
</div>
|
|
463
313
|
</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
|
-
</>
|
|
314
|
+
</div>
|
|
475
315
|
)
|
|
476
316
|
}
|
|
317
|
+
|
|
318
|
+
const MediaMessageCreatorEntry: React.FC<{ message: LocalMessage }> = ({
|
|
319
|
+
message,
|
|
320
|
+
}) => {
|
|
321
|
+
const linkAttachment = resolveLinkAttachment(message)
|
|
322
|
+
if (linkAttachment) {
|
|
323
|
+
return (
|
|
324
|
+
<div className={linkCardShellClass(true)}>
|
|
325
|
+
<LinkCard attachment={linkAttachment} isMyMessage={true} />
|
|
326
|
+
</div>
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
const resolved = resolveMediaFromMessage(message)
|
|
330
|
+
if (!resolved) return null
|
|
331
|
+
return <Creator {...resolved} />
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const MediaMessageVisitorEntry: React.FC<{ message: LocalMessage }> = ({
|
|
335
|
+
message,
|
|
336
|
+
}) => {
|
|
337
|
+
const linkAttachment = resolveLinkAttachment(message)
|
|
338
|
+
if (linkAttachment) {
|
|
339
|
+
return (
|
|
340
|
+
<div className={linkCardShellClass(false)}>
|
|
341
|
+
<LinkCard attachment={linkAttachment} isMyMessage={false} />
|
|
342
|
+
</div>
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
const resolved = resolveMediaFromMessage(message)
|
|
346
|
+
if (!resolved) return null
|
|
347
|
+
return <Visitor {...resolved} />
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export const MediaMessage = Object.assign(MediaMessageRoot, {
|
|
351
|
+
Creator: MediaMessageCreatorEntry,
|
|
352
|
+
Visitor: MediaMessageVisitorEntry,
|
|
353
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -13,8 +13,12 @@ export { FaqList } from './components/FaqList'
|
|
|
13
13
|
export { FaqListItem } from './components/FaqList/FaqListItem'
|
|
14
14
|
export { ChannelEmptyState } from './components/MessagingShell/ChannelEmptyState'
|
|
15
15
|
export { MessageVoteButtons } from './components/CustomMessage/MessageVoteButtons'
|
|
16
|
-
export {
|
|
17
|
-
|
|
16
|
+
export {
|
|
17
|
+
MediaMessage,
|
|
18
|
+
resolveLinkAttachment,
|
|
19
|
+
resolveMediaFromMessage,
|
|
20
|
+
} from './components/MediaMessage'
|
|
21
|
+
export type { MediaMessageProps, MediaMessageResolved } from './components/MediaMessage'
|
|
18
22
|
|
|
19
23
|
// Providers
|
|
20
24
|
export { MessagingProvider } from './providers/MessagingProvider'
|
|
@@ -46,7 +50,7 @@ export type { AvatarProps } from './components/Avatar'
|
|
|
46
50
|
export type { ActionButtonProps } from './components/ActionButton'
|
|
47
51
|
export type { CreatorCardProps, VisitorCardProps, LockedAttachmentContextValue } from './components/LockedAttachment'
|
|
48
52
|
export type { CustomMessageRegistry } from './components/CustomMessage/context'
|
|
49
|
-
export type { AttachmentSourceType } from './components/
|
|
53
|
+
export type { AttachmentSourceType } from './components/AttachmentCard/utils/mimeType'
|
|
50
54
|
export type { Faq, FaqListProps } from './components/FaqList'
|
|
51
55
|
export type { FaqListItemProps } from './components/FaqList/FaqListItem'
|
|
52
56
|
export type { VoteSelection } from './hooks/useMessageVote'
|