@linktr.ee/messaging-react 4.2.1 → 4.3.0-rc-1787410254

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/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-Cd-uS2hf.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageBubble=e.MessageBubble;exports.MessagingLoggerProvider=e.MessagingLoggerProvider;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.RichCard=e.RichCard;exports.TipMessage=e.TipMessage;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isTipMessage=e.isTipMessage;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessaging=e.useMessaging;exports.useMessagingLogger=e.useMessagingLogger;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-C04QXCa0.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MessageAttachment=e.MessageAttachment;exports.MessageBubble=e.MessageBubble;exports.MessagingLoggerProvider=e.MessagingLoggerProvider;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.RichCard=e.RichCard;exports.TipMessage=e.TipMessage;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isTipMessage=e.isTipMessage;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessaging=e.useMessaging;exports.useMessagingLogger=e.useMessagingLogger;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -599,6 +599,11 @@ declare interface ImageAttachmentSharedProps extends MessageAttachmentBaseProps
599
599
  /** Loading hint forwarded to the underlying `<img>` element. */
600
600
  declare type ImageLoadingMode = 'lazy' | 'eager';
601
601
 
602
+ /**
603
+ * True when an attachment is a link preview (OG card) rather than media:
604
+ * either Stream's native `link` type, or a non-empty (trimmed) `og_scrape_url`
605
+ * with no `asset_url`. A whitespace-only `og_scrape_url` is not a link.
606
+ */
602
607
  export declare function isLinkAttachment(a: Attachment): boolean;
603
608
 
604
609
  /** Check if a message is a tip based on metadata */
@@ -943,28 +948,6 @@ declare interface MediaCardSentBaseProps extends MediaCardBaseProps {
943
948
  onFetchSource?: FetchSourceHandler;
944
949
  }
945
950
 
946
- export declare const MediaMessage: default_2.FC<MediaMessageProps> & {
947
- Creator: default_2.FC<{
948
- message: LocalMessage;
949
- }>;
950
- Visitor: default_2.FC<{
951
- message: LocalMessage;
952
- }>;
953
- };
954
-
955
- export declare interface MediaMessageProps {
956
- message: LocalMessage;
957
- isMyMessage?: boolean;
958
- }
959
-
960
- export declare interface MediaMessageResolved {
961
- resolvedUrl: string;
962
- resolvedType: string;
963
- title?: string;
964
- fileSize?: number;
965
- thumbnailUrl?: string;
966
- }
967
-
968
951
  /** Preload hint forwarded to the underlying `<video>` / `<audio>` element. */
969
952
  declare type MediaPreloadMode = 'none' | 'metadata' | 'auto';
970
953
 
@@ -1665,10 +1648,6 @@ export declare type ResolvedOutbound = {
1665
1648
  name?: string;
1666
1649
  };
1667
1650
 
1668
- export declare function resolveLinkAttachment(message: LocalMessage): Attachment | undefined;
1669
-
1670
- export declare function resolveMediaFromMessage(message: LocalMessage): MediaMessageResolved | null;
1671
-
1672
1651
  /**
1673
1652
  * Resolves a human-readable participant label from Stream user fields.
1674
1653
  * Never falls back to user.id. UUID-like names are treated as missing.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, b as t, C as i, c as n, d as o, e as g, F as r, f as m, L as l, h as M, M as c, i as L, j as h, k as p, l as u, m as d, R as v, T as C, n as A, p as k, q as F, s as b, t as P, u as T, v as f, w as x, x as y, y as z, o as S, z as q, B, D, E, G as R, H as w, I as G, J as I } from "./index-wG3iH9e6.js";
1
+ import { A as e, a as t, C as i, b as n, c as o, d as g, F as r, e as m, L as l, f as M, M as c, h as L, i as p, j as u, k as h, R as C, T as d, l as v, m as A, n as b, p as k, q as F, s as P, t as T, u as f, v as x, w as y, o as z, x as S, y as q, z as B, B as D, D as E, E as R } from "./index-Cc8BzIhS.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
@@ -10,32 +10,29 @@ export {
10
10
  m as FaqListItem,
11
11
  l as LinkAttachment,
12
12
  M as LockedAttachment,
13
- c as MediaMessage,
14
- L as MessageAttachment,
15
- h as MessageBubble,
13
+ c as MessageAttachment,
14
+ L as MessageBubble,
16
15
  p as MessagingLoggerProvider,
17
16
  u as MessagingProvider,
18
- d as MessagingShell,
19
- v as RichCard,
20
- C as TipMessage,
21
- A as buildCompactMetaLabel,
22
- k as formatFileSize,
23
- F as formatRelativeTime,
24
- b as getFileExtensionLabel,
25
- P as getMessageDisplayText,
26
- T as isLinkAttachment,
27
- f as isTipMessage,
28
- x as isUuidLike,
29
- y as messageAttachmentGroupPositionFromStream,
30
- z as normalizeLanguageCode,
31
- S as optimizeMessagingAttachmentUrl,
32
- q as resolveConversationParticipant,
33
- B as resolveLinkAttachment,
34
- D as resolveMediaFromMessage,
35
- E as resolveParticipantDisplayName,
36
- R as useComposerLocked,
37
- w as useCustomMessage,
38
- G as useMessaging,
39
- I as useMessagingLogger
17
+ h as MessagingShell,
18
+ C as RichCard,
19
+ d as TipMessage,
20
+ v as buildCompactMetaLabel,
21
+ A as formatFileSize,
22
+ b as formatRelativeTime,
23
+ k as getFileExtensionLabel,
24
+ F as getMessageDisplayText,
25
+ P as isLinkAttachment,
26
+ T as isTipMessage,
27
+ f as isUuidLike,
28
+ x as messageAttachmentGroupPositionFromStream,
29
+ y as normalizeLanguageCode,
30
+ z as optimizeMessagingAttachmentUrl,
31
+ S as resolveConversationParticipant,
32
+ q as resolveParticipantDisplayName,
33
+ B as useComposerLocked,
34
+ D as useCustomMessage,
35
+ E as useMessaging,
36
+ R as useMessagingLogger
40
37
  };
41
38
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "4.2.1",
3
+ "version": "4.3.0-rc-1787410254",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -50,7 +50,7 @@
50
50
  "chromatic:local": "yarn storybook:build && chromatic"
51
51
  },
52
52
  "dependencies": {
53
- "@linktr.ee/messaging-core": "^2.5.0",
53
+ "@linktr.ee/messaging-core": "2.5.0-rc-1787410254",
54
54
  "@linktr.ee/messaging-taxonomy": "^0.10.0",
55
55
  "@phosphor-icons/react": "^2.1.10"
56
56
  },
@@ -1,427 +1,10 @@
1
- import {
2
- CircleNotchIcon,
3
- DownloadSimpleIcon,
4
- LinkIcon,
5
- } from '@phosphor-icons/react'
6
- import React, { useState } from 'react'
7
- import type { Attachment, LocalMessage } from 'stream-chat'
8
-
9
- import { optimizeMessagingAttachmentUrl } from '../../utils/cdnImageUrl'
10
- import AttachmentCard, {
11
- AttachmentThumbnail,
12
- getSourceType,
13
- } from '../AttachmentCard'
14
- import { Avatar } from '../Avatar'
15
-
16
- function formatBytes(bytes: number): string {
17
- if (bytes < 1024) return `${bytes} B`
18
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
19
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
20
- }
21
-
22
- function linkCardShellClass(isMyMessage: boolean) {
23
- const bg = isMyMessage ? 'bg-[#1e2330]' : 'bg-[#F3F3F1]'
24
- 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)]`
25
- }
26
-
27
- function linkThumbnailBg(isMyMessage: boolean) {
28
- return isMyMessage ? 'bg-white/10' : 'bg-black/5'
29
- }
30
-
31
- function linkPrimaryText(isMyMessage: boolean) {
32
- return isMyMessage ? 'text-white' : 'text-black'
33
- }
34
-
35
- function linkSecondaryText(isMyMessage: boolean) {
36
- return isMyMessage ? 'text-white/55' : 'text-black/55'
37
- }
38
-
39
- function linkTertiaryText(isMyMessage: boolean) {
40
- return isMyMessage ? 'text-white/40' : 'text-black/40'
41
- }
42
-
43
- function linkIconColor(isMyMessage: boolean) {
44
- return isMyMessage ? 'text-white/20' : 'text-black/20'
45
- }
46
-
47
- /** Link preview (OG) — no download; opens in a new tab. */
48
- const LinkCard: React.FC<{
49
- attachment: Attachment
50
- isMyMessage: boolean
51
- }> = ({ attachment, isMyMessage }) => {
52
- const { title, text, image_url, og_scrape_url, title_link } = attachment
53
- const rawUrl = og_scrape_url ?? title_link
54
- const url =
55
- typeof rawUrl === 'string' && rawUrl.trim() !== '' ? rawUrl : undefined
56
-
57
- const body = (
58
- <React.Fragment>
59
- <div className="p-2">
60
- {image_url ? (
61
- <img
62
- src={optimizeMessagingAttachmentUrl(image_url) ?? image_url}
63
- alt={title ?? ''}
64
- className="aspect-video w-full rounded-[20px] object-cover"
65
- />
66
- ) : (
67
- <div
68
- className={`aspect-video w-full rounded-[20px] ${linkThumbnailBg(isMyMessage)} flex items-center justify-center`}
69
- >
70
- <LinkIcon className={`size-12 ${linkIconColor(isMyMessage)}`} />
71
- </div>
72
- )}
73
- </div>
74
- <div className="px-3 pb-3">
75
- {title && (
76
- <p
77
- className={`truncate text-[14px] font-medium leading-5 ${linkPrimaryText(isMyMessage)}`}
78
- >
79
- {title}
80
- </p>
81
- )}
82
- {text && (
83
- <p
84
- className={`truncate text-[12px] leading-4 ${linkSecondaryText(isMyMessage)}`}
85
- >
86
- {text}
87
- </p>
88
- )}
89
- {url && (
90
- <p
91
- className={`mt-1 truncate text-[12px] leading-4 ${linkTertiaryText(isMyMessage)}`}
92
- >
93
- {url}
94
- </p>
95
- )}
96
- </div>
97
- </React.Fragment>
98
- )
99
-
100
- if (url) {
101
- return (
102
- <a
103
- href={url}
104
- target="_blank"
105
- rel="noopener noreferrer"
106
- className="block no-underline"
107
- >
108
- {body}
109
- </a>
110
- )
111
- }
112
-
113
- return <div className="block">{body}</div>
114
- }
115
-
116
- export function isLinkAttachment(a: Attachment): boolean {
117
- return a.type === 'link' || (!!a.og_scrape_url?.trim() && !a.asset_url)
118
- }
119
-
120
- export function resolveLinkAttachment(
121
- message: LocalMessage
122
- ): Attachment | undefined {
123
- return message.attachments?.find(isLinkAttachment)
124
- }
125
-
126
- async function triggerDownload(url: string, filename?: string): Promise<void> {
127
- let name: string
128
- try {
129
- name = filename ?? new URL(url).pathname.split('/').pop() ?? 'download'
130
- } catch {
131
- name = filename ?? 'download'
132
- }
133
- const res = await fetch(url, { mode: 'cors' })
134
- if (!res.ok) throw new Error(`HTTP ${res.status}`)
135
- const blob = await res.blob()
136
- const objectUrl = URL.createObjectURL(blob)
137
- const a = document.createElement('a')
138
- a.href = objectUrl
139
- a.download = name
140
- a.style.display = 'none'
141
- document.body.appendChild(a)
142
- a.click()
143
- document.body.removeChild(a)
144
- URL.revokeObjectURL(objectUrl)
145
- }
146
-
147
- const DownloadAction: React.FC<{ url: string; filename?: string }> = ({
148
- url,
149
- filename,
150
- }) => {
151
- const [busy, setBusy] = useState(false)
152
-
153
- const handleClick = (e: React.MouseEvent) => {
154
- e.stopPropagation()
155
- const fallback = window.open('', '_blank', 'noopener,noreferrer')
156
- setBusy(true)
157
- triggerDownload(url, filename)
158
- .then(() => {
159
- fallback?.close()
160
- })
161
- .catch(() => {
162
- if (fallback) fallback.location.href = url
163
- })
164
- .finally(() => setBusy(false))
165
- }
166
-
167
- return (
168
- <button
169
- type="button"
170
- onClick={handleClick}
171
- disabled={busy}
172
- 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"
173
- >
174
- {busy ? (
175
- <CircleNotchIcon
176
- className="size-4 animate-spin text-white"
177
- weight="bold"
178
- />
179
- ) : (
180
- <React.Fragment>
181
- <DownloadSimpleIcon className="size-4 text-white" weight="bold" />
182
- Download
183
- </React.Fragment>
184
- )}
185
- </button>
186
- )
187
- }
188
-
189
- export interface MediaMessageResolved {
190
- resolvedUrl: string
191
- resolvedType: string
192
- title?: string
193
- fileSize?: number
194
- thumbnailUrl?: string
195
- }
196
-
197
- export function resolveMediaFromMessage(
198
- message: LocalMessage
199
- ): MediaMessageResolved | null {
200
- const videoAttachment = message.attachments?.find(
201
- (a) => a.type === 'video' && a.asset_url
202
- )
203
- const imageAttachment = message.attachments?.find(
204
- (a) => a.type === 'image' && (a as { image_url?: string }).image_url
205
- )
206
- const audioAttachment = message.attachments?.find(
207
- (a) => a.type === 'audio' && a.asset_url
208
- )
209
- const fileAttachment = message.attachments?.find(
210
- (a) => a.type === 'file' && a.asset_url
211
- )
212
-
213
- const activeAttachment =
214
- videoAttachment ?? imageAttachment ?? audioAttachment ?? fileAttachment
215
-
216
- const resolvedUrl =
217
- videoAttachment?.asset_url ??
218
- (imageAttachment as { image_url?: string } | undefined)?.image_url ??
219
- audioAttachment?.asset_url ??
220
- fileAttachment?.asset_url
221
-
222
- if (!resolvedUrl) return null
223
-
224
- const resolvedType =
225
- activeAttachment?.mime_type ??
226
- (activeAttachment?.type === 'image'
227
- ? 'image/jpeg'
228
- : activeAttachment?.type === 'video'
229
- ? 'video/mp4'
230
- : activeAttachment?.type === 'audio'
231
- ? 'audio/mpeg'
232
- : 'application/octet-stream')
233
-
234
- const title = (activeAttachment as { title?: string } | undefined)?.title
235
- const fileSize = (activeAttachment as { file_size?: number } | undefined)
236
- ?.file_size
237
- const thumbnailUrl = (videoAttachment as { thumb_url?: string } | undefined)
238
- ?.thumb_url
239
-
240
- return {
241
- resolvedUrl,
242
- resolvedType,
243
- title,
244
- fileSize,
245
- thumbnailUrl,
246
- }
247
- }
248
-
249
- /** Dark card (sent / own message) — matches LockedAttachment.Sent preview without toggle. */
250
- const Creator: React.FC<MediaMessageResolved> = ({
251
- resolvedUrl,
252
- resolvedType,
253
- title,
254
- fileSize,
255
- thumbnailUrl,
256
- }) => {
257
- const detail = fileSize !== undefined ? formatBytes(fileSize) : undefined
258
-
259
- return (
260
- <AttachmentCard
261
- variant="dark"
262
- title={title}
263
- placeholderTitle=""
264
- mimeType={resolvedType}
265
- detail={detail}
266
- thumbnail={
267
- <AttachmentThumbnail
268
- mimeType={resolvedType}
269
- sourceUrl={resolvedUrl}
270
- thumbnailUrl={thumbnailUrl}
271
- title={title}
272
- variant="dark"
273
- />
274
- }
275
- />
276
- )
277
- }
278
-
279
- /** Light card (received) — matches LockedAttachment.Received unlocked without Purchased copy. */
280
- const Visitor: React.FC<MediaMessageResolved> = ({
281
- resolvedUrl,
282
- resolvedType,
283
- title,
284
- fileSize,
285
- thumbnailUrl,
286
- }) => {
287
- const sourceType = getSourceType(resolvedType)
288
- const detail = fileSize !== undefined ? formatBytes(fileSize) : undefined
289
-
290
- return (
291
- <AttachmentCard
292
- variant="light"
293
- title={title}
294
- mimeType={resolvedType}
295
- detail={detail}
296
- thumbnail={
297
- <AttachmentThumbnail
298
- mimeType={resolvedType}
299
- sourceUrl={resolvedUrl}
300
- thumbnailUrl={thumbnailUrl}
301
- title={title}
302
- variant="light"
303
- containedImage={sourceType === 'image' || sourceType === 'document'}
304
- />
305
- }
306
- action={<DownloadAction url={resolvedUrl} filename={title} />}
307
- />
308
- )
309
- }
310
-
311
- export interface MediaMessageProps {
312
- message: LocalMessage
313
- isMyMessage?: boolean
314
- }
1
+ import type { Attachment } from 'stream-chat'
315
2
 
316
3
  /**
317
- * @deprecated Slated for removal once the profile repo is migrated to
318
- * `LinkAttachment` (and its forthcoming media-only Attachment variants).
319
- * New consumers should reach for `LinkAttachment.Composer`,
320
- * `LinkAttachment.Sent`, or `LinkAttachment.Received` instead.
4
+ * True when an attachment is a link preview (OG card) rather than media:
5
+ * either Stream's native `link` type, or a non-empty (trimmed) `og_scrape_url`
6
+ * with no `asset_url`. A whitespace-only `og_scrape_url` is not a link.
321
7
  */
322
- const MediaMessageRoot: React.FC<MediaMessageProps> = ({
323
- message,
324
- isMyMessage = false,
325
- }) => {
326
- const linkAttachment = resolveLinkAttachment(message)
327
- const resolved = resolveMediaFromMessage(message)
328
- if (!linkAttachment && !resolved) return null
329
-
330
- const messageClass = isMyMessage
331
- ? 'str-chat__message str-chat__message-simple str-chat__message--me str-chat__message-simple--me'
332
- : 'str-chat__message str-chat__message-simple str-chat__message--other'
333
-
334
- return (
335
- <div className={messageClass} data-dd-privacy="mask">
336
- {!isMyMessage && message.user && (
337
- <Avatar
338
- className="str-chat__avatar str-chat__message-sender-avatar"
339
- id={message.user.id}
340
- image={message.user.image}
341
- name={message.user.name ?? message.user.id}
342
- />
343
- )}
344
- <div
345
- className="str-chat__message-inner"
346
- style={{ marginInlineEnd: 0, marginInlineStart: 0 }}
347
- >
348
- <div className="str-chat__message-bubble-wrapper">
349
- <div
350
- className="str-chat__message-bubble"
351
- // The visible child is a self-contained AttachmentCard (white /
352
- // #1e2330 card with its own shadow + radius), not a speech
353
- // bubble, and this wrapper is transparent. Mark it so the
354
- // received/sent tail in `styles.css` skips it — otherwise a
355
- // tail tinted from the text-bubble surface (#f2f1ef) hangs off
356
- // a white card and reads as a detached grey nub.
357
- data-attachment-bubble="true"
358
- style={{
359
- padding: 0,
360
- borderRadius: 0,
361
- overflow: 'visible',
362
- background: 'transparent',
363
- }}
364
- >
365
- {linkAttachment ? (
366
- <div className={linkCardShellClass(isMyMessage)}>
367
- <LinkCard
368
- attachment={linkAttachment}
369
- isMyMessage={isMyMessage}
370
- />
371
- </div>
372
- ) : isMyMessage ? (
373
- <Creator {...resolved!} />
374
- ) : (
375
- <Visitor {...resolved!} />
376
- )}
377
- </div>
378
- </div>
379
- </div>
380
- </div>
381
- )
382
- }
383
-
384
- const MediaMessageCreatorEntry: React.FC<{ message: LocalMessage }> = ({
385
- message,
386
- }) => {
387
- const linkAttachment = resolveLinkAttachment(message)
388
- if (linkAttachment) {
389
- return (
390
- <div className={linkCardShellClass(true)} data-dd-privacy="mask">
391
- <LinkCard attachment={linkAttachment} isMyMessage={true} />
392
- </div>
393
- )
394
- }
395
- const resolved = resolveMediaFromMessage(message)
396
- if (!resolved) return null
397
- return (
398
- <div style={{ display: 'contents' }} data-dd-privacy="mask">
399
- <Creator {...resolved} />
400
- </div>
401
- )
402
- }
403
-
404
- const MediaMessageVisitorEntry: React.FC<{ message: LocalMessage }> = ({
405
- message,
406
- }) => {
407
- const linkAttachment = resolveLinkAttachment(message)
408
- if (linkAttachment) {
409
- return (
410
- <div className={linkCardShellClass(false)} data-dd-privacy="mask">
411
- <LinkCard attachment={linkAttachment} isMyMessage={false} />
412
- </div>
413
- )
414
- }
415
- const resolved = resolveMediaFromMessage(message)
416
- if (!resolved) return null
417
- return (
418
- <div style={{ display: 'contents' }} data-dd-privacy="mask">
419
- <Visitor {...resolved} />
420
- </div>
421
- )
8
+ export function isLinkAttachment(a: Attachment): boolean {
9
+ return a.type === 'link' || (!!a.og_scrape_url?.trim() && !a.asset_url)
422
10
  }
423
-
424
- export const MediaMessage = Object.assign(MediaMessageRoot, {
425
- Creator: MediaMessageCreatorEntry,
426
- Visitor: MediaMessageVisitorEntry,
427
- })
@@ -0,0 +1,33 @@
1
+ import type { Attachment } from 'stream-chat'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { isLinkAttachment } from '.'
5
+
6
+ describe('isLinkAttachment', () => {
7
+ it('is true for a Stream link-type attachment', () => {
8
+ expect(isLinkAttachment({ type: 'link' } as Attachment)).toBe(true)
9
+ })
10
+
11
+ it('is true for an og_scrape_url with no asset_url', () => {
12
+ expect(
13
+ isLinkAttachment({ og_scrape_url: 'https://example.com' } as Attachment)
14
+ ).toBe(true)
15
+ })
16
+
17
+ it('ignores a whitespace-only og_scrape_url', () => {
18
+ expect(isLinkAttachment({ og_scrape_url: ' ' } as Attachment)).toBe(false)
19
+ })
20
+
21
+ it('is false when og_scrape_url accompanies an asset_url (media, not a link)', () => {
22
+ expect(
23
+ isLinkAttachment({
24
+ og_scrape_url: 'https://example.com',
25
+ asset_url: 'https://cdn.example.com/file.mp4',
26
+ } as Attachment)
27
+ ).toBe(false)
28
+ })
29
+
30
+ it('is false for a plain attachment with neither', () => {
31
+ expect(isLinkAttachment({ type: 'image' } as Attachment)).toBe(false)
32
+ })
33
+ })
package/src/index.ts CHANGED
@@ -16,16 +16,7 @@ export { default as MessageBubble } from './components/MessageBubble'
16
16
  export { FaqList } from './components/FaqList'
17
17
  export { FaqListItem } from './components/FaqList/FaqListItem'
18
18
  export { ChannelEmptyState } from './components/MessagingShell/ChannelEmptyState'
19
- export {
20
- MediaMessage,
21
- isLinkAttachment,
22
- resolveLinkAttachment,
23
- resolveMediaFromMessage,
24
- } from './components/MediaMessage'
25
- export type {
26
- MediaMessageProps,
27
- MediaMessageResolved,
28
- } from './components/MediaMessage'
19
+ export { isLinkAttachment } from './components/MediaMessage'
29
20
 
30
21
  // Providers
31
22
  export { MessagingProvider } from './providers/MessagingProvider'