@linktr.ee/messaging-react 4.2.1 → 4.3.0-rc-1787409421
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-DA-EJpQI.cjs +2 -0
- package/dist/Card-DA-EJpQI.cjs.map +1 -0
- package/dist/Card-DyoGzhhM.js +1033 -0
- package/dist/Card-DyoGzhhM.js.map +1 -0
- package/dist/index-C04QXCa0.cjs +3 -0
- package/dist/index-C04QXCa0.cjs.map +1 -0
- package/dist/index-Cc8BzIhS.js +5253 -0
- package/dist/index-Cc8BzIhS.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +5 -26
- package/dist/index.js +23 -26
- package/package.json +2 -2
- package/src/components/MediaMessage/index.tsx +6 -423
- package/src/components/MediaMessage/isLinkAttachment.test.ts +33 -0
- package/src/index.ts +1 -10
- package/dist/Card-3VtiPkQH.js +0 -701
- package/dist/Card-3VtiPkQH.js.map +0 -1
- package/dist/Card-BxYYijOd.cjs +0 -2
- package/dist/Card-BxYYijOd.cjs.map +0 -1
- package/dist/index-Cd-uS2hf.cjs +0 -3
- package/dist/index-Cd-uS2hf.cjs.map +0 -1
- package/dist/index-wG3iH9e6.js +0 -5916
- package/dist/index-wG3iH9e6.js.map +0 -1
- package/src/components/MediaMessage/MediaMessage.stories.tsx +0 -270
- package/src/components/MediaMessage/MediaMessage.test.tsx +0 -494
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-
|
|
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 {
|
|
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
|
|
14
|
-
L as
|
|
15
|
-
h as MessageBubble,
|
|
13
|
+
c as MessageAttachment,
|
|
14
|
+
L as MessageBubble,
|
|
16
15
|
p as MessagingLoggerProvider,
|
|
17
16
|
u as MessagingProvider,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
|
3
|
+
"version": "4.3.0-rc-1787409421",
|
|
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": "
|
|
53
|
+
"@linktr.ee/messaging-core": "2.5.0-rc-1787409421",
|
|
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
|
-
*
|
|
318
|
-
* `
|
|
319
|
-
*
|
|
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
|
-
|
|
323
|
-
|
|
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'
|