@linktr.ee/messaging-react 1.36.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.
Files changed (30) hide show
  1. package/dist/Card-BlXnKGaR.js +127 -0
  2. package/dist/Card-BlXnKGaR.js.map +1 -0
  3. package/dist/Card-DoNJA-jg.js +138 -0
  4. package/dist/Card-DoNJA-jg.js.map +1 -0
  5. package/dist/{index-DOsC03ZN.js → index-jnKl3mQ0.js} +1404 -1352
  6. package/dist/index-jnKl3mQ0.js.map +1 -0
  7. package/dist/index.d.ts +21 -1
  8. package/dist/index.js +15 -13
  9. package/package.json +2 -2
  10. package/src/components/{LockedAttachment/components → AttachmentCard}/MediaPlayer.tsx +4 -3
  11. package/src/components/AttachmentCard/Thumbnail.tsx +150 -0
  12. package/src/components/AttachmentCard/index.tsx +112 -0
  13. package/src/components/LockedAttachment/components/Creator/Card.tsx +123 -113
  14. package/src/components/LockedAttachment/components/Visitor/Card.tsx +43 -42
  15. package/src/components/LockedAttachment/components/Visitor/LockBadge.tsx +12 -0
  16. package/src/components/MediaMessage/MediaMessage.stories.tsx +45 -4
  17. package/src/components/MediaMessage/MediaMessage.test.tsx +125 -160
  18. package/src/components/MediaMessage/index.tsx +226 -349
  19. package/src/index.ts +7 -3
  20. package/src/providers/MessagingProvider.test.tsx +126 -0
  21. package/dist/Card-BHrnmHeu.js +0 -167
  22. package/dist/Card-BHrnmHeu.js.map +0 -1
  23. package/dist/Card-D4vEgqWt.js +0 -195
  24. package/dist/Card-D4vEgqWt.js.map +0 -1
  25. package/dist/index-DOsC03ZN.js.map +0 -1
  26. package/src/components/LockedAttachment/components/Creator/CardThumbnail.tsx +0 -114
  27. package/src/components/LockedAttachment/components/Visitor/CardThumbnail.tsx +0 -81
  28. /package/src/components/{LockedAttachment → AttachmentCard}/utils/icons.ts +0 -0
  29. /package/src/components/{LockedAttachment → AttachmentCard}/utils/mimeType.test.ts +0 -0
  30. /package/src/components/{LockedAttachment → AttachmentCard}/utils/mimeType.ts +0 -0
@@ -1,11 +1,12 @@
1
- import { CircleNotchIcon, DownloadSimpleIcon, LinkIcon, XIcon } from '@phosphor-icons/react'
2
- import React, { useEffect, useRef, useState } from '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 cardClass(isMyMessage: boolean) {
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 primaryText(isMyMessage: boolean) {
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 secondaryText(isMyMessage: boolean) {
30
+ function linkSecondaryText(isMyMessage: boolean) {
26
31
  return isMyMessage ? 'text-white/55' : 'text-black/55'
27
32
  }
28
33
 
29
- function tertiaryText(isMyMessage: boolean) {
34
+ function linkTertiaryText(isMyMessage: boolean) {
30
35
  return isMyMessage ? 'text-white/40' : 'text-black/40'
31
36
  }
32
37
 
33
- function thumbnailBg(isMyMessage: boolean) {
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
- function buttonBg(isMyMessage: boolean) {
42
- return isMyMessage ? 'bg-white/10 hover:bg-white/15' : 'bg-black/[0.06] hover:bg-black/10'
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
- // Download button (shared across all media types)
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 DownloadButton: React.FC<{ url: string; filename?: string; isMyMessage: boolean }> = ({
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={`flex size-8 items-center justify-center rounded-full ${buttonBg(isMyMessage)} disabled:opacity-50`}
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={`size-4 animate-spin ${secondaryText(isMyMessage)}`} weight="bold" />
136
+ <CircleNotchIcon className="size-4 animate-spin text-white" weight="bold" />
95
137
  ) : (
96
- <DownloadSimpleIcon className={`size-4 ${secondaryText(isMyMessage)}`} weight="bold" />
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
- // Chin: title + file size + download
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
- 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">
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
- isMyMessage?: boolean
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
- const isPdf = resolvedType === 'application/pdf'
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
- <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!}
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
- </div>
306
+ ) : isMyMessage ? (
307
+ <Creator {...resolved!} />
308
+ ) : (
309
+ <Visitor {...resolved!} />
310
+ )}
461
311
  </div>
462
312
  </div>
463
313
  </div>
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
- </>
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 { MediaMessage } from './components/MediaMessage'
17
- export type { MediaMessageProps } from './components/MediaMessage'
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/LockedAttachment/utils/mimeType'
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'