@linktr.ee/messaging-react 3.12.2-rc-1784020746 → 3.12.3-rc-1784032997
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-Bk9bLpuH.js → Card-Bme-pBSF.js} +123 -119
- package/dist/Card-Bme-pBSF.js.map +1 -0
- package/dist/Card-D-SQP6VG.cjs +2 -0
- package/dist/Card-D-SQP6VG.cjs.map +1 -0
- package/dist/index-B8niA4Dg.cjs +2 -0
- package/dist/index-B8niA4Dg.cjs.map +1 -0
- package/dist/{index-ZjkTaCN6.js → index-D9_M5IWl.js} +1247 -1221
- package/dist/index-D9_M5IWl.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +0 -10
- package/dist/index.js +1 -1
- package/dist/testing.d.ts +0 -10
- package/package.json +2 -2
- package/src/components/AttachmentCard/Thumbnail.tsx +10 -6
- package/src/components/ChannelHeaderRedesign.tsx +13 -4
- package/src/components/ChannelView.stories.tsx +7 -8
- package/src/components/ChannelView.test.tsx +5 -24
- package/src/components/CustomMessage/LockedAttachment/components/_shared/GalleryThumbnail.tsx +5 -1
- package/src/components/CustomMessage/LockedAttachment/components/_shared/SingleThumbnail.tsx +5 -1
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +33 -0
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +6 -3
- package/src/components/LinkAttachment/components/_shared/CardThumbnail.tsx +4 -2
- package/src/components/MediaMessage/index.tsx +2 -1
- package/src/components/MessageAttachment/Image/index.tsx +3 -2
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +20 -0
- package/src/stream-custom-data.ts +0 -11
- package/src/utils/cdnImageUrl.ts +32 -0
- package/dist/Card-Bk9bLpuH.js.map +0 -1
- package/dist/Card-xlNNaZJO.cjs +0 -2
- package/dist/Card-xlNNaZJO.cjs.map +0 -1
- package/dist/index-BMr8tbg4.cjs +0 -2
- package/dist/index-BMr8tbg4.cjs.map +0 -1
- package/dist/index-ZjkTaCN6.js.map +0 -1
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-B8niA4Dg.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.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;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.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;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.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1713,16 +1713,6 @@ declare module 'stream-chat' {
|
|
|
1713
1713
|
* Whether DM agent auto-replies are paused for this channel.
|
|
1714
1714
|
*/
|
|
1715
1715
|
chatbot_paused?: boolean;
|
|
1716
|
-
/**
|
|
1717
|
-
* Customer classification tag for this conversation (e.g. CUSTOMER_PAID).
|
|
1718
|
-
*/
|
|
1719
|
-
customer_tag?: string;
|
|
1720
|
-
}
|
|
1721
|
-
interface CustomMemberData {
|
|
1722
|
-
/**
|
|
1723
|
-
* Whether this member follows the channel's linker account.
|
|
1724
|
-
*/
|
|
1725
|
-
is_follower?: boolean;
|
|
1726
1716
|
}
|
|
1727
1717
|
interface CustomMessageData {
|
|
1728
1718
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as u, i as c, j as L, k as h, l as d, m as p, n as v,
|
|
1
|
+
import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as u, i as c, j as L, k as h, l as d, m as p, n as v, p as C, q as A, s as k, t as F, u as b, v as P, w as f, x, y, z as B, B as S, D as q, E as z, G as D, H as E, I as V } from "./index-D9_M5IWl.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
package/dist/testing.d.ts
CHANGED
|
@@ -133,16 +133,6 @@ declare module 'stream-chat' {
|
|
|
133
133
|
* Whether DM agent auto-replies are paused for this channel.
|
|
134
134
|
*/
|
|
135
135
|
chatbot_paused?: boolean;
|
|
136
|
-
/**
|
|
137
|
-
* Customer classification tag for this conversation (e.g. CUSTOMER_PAID).
|
|
138
|
-
*/
|
|
139
|
-
customer_tag?: string;
|
|
140
|
-
}
|
|
141
|
-
interface CustomMemberData {
|
|
142
|
-
/**
|
|
143
|
-
* Whether this member follows the channel's linker account.
|
|
144
|
-
*/
|
|
145
|
-
is_follower?: boolean;
|
|
146
136
|
}
|
|
147
137
|
interface CustomMessageData {
|
|
148
138
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "3.12.
|
|
3
|
+
"version": "3.12.3-rc-1784032997",
|
|
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
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@linktr.ee/component-library": "11.8.6",
|
|
53
|
-
"@linktr.ee/messaging-core": "2.3.0-rc-
|
|
53
|
+
"@linktr.ee/messaging-core": "2.3.0-rc-1784032997",
|
|
54
54
|
"@phosphor-icons/react": "^2.1.10"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
|
|
3
|
+
import { optimizeMessagingAttachmentUrl } from '../../utils/cdnImageUrl'
|
|
4
|
+
|
|
3
5
|
import MediaPlayer, { type MediaPlayerProps } from './MediaPlayer'
|
|
4
6
|
import { renderTypeIcon } from './utils/icons'
|
|
5
7
|
import { getSourceType } from './utils/mimeType'
|
|
@@ -48,13 +50,15 @@ const AttachmentThumbnail: React.FC<AttachmentThumbnailProps> = ({
|
|
|
48
50
|
}) => {
|
|
49
51
|
const sourceType = getSourceType(mimeType)
|
|
50
52
|
const [sourceReady, setSourceReady] = useState(false)
|
|
53
|
+
const optimizedSourceUrl = optimizeMessagingAttachmentUrl(sourceUrl)
|
|
54
|
+
const optimizedThumbnailUrl = optimizeMessagingAttachmentUrl(thumbnailUrl)
|
|
51
55
|
|
|
52
56
|
if (sourceUrl && (sourceType === 'video' || sourceType === 'audio')) {
|
|
53
57
|
return (
|
|
54
58
|
<MediaPlayer
|
|
55
59
|
source={sourceUrl}
|
|
56
60
|
mimeType={mimeType}
|
|
57
|
-
poster={
|
|
61
|
+
poster={optimizedThumbnailUrl}
|
|
58
62
|
controls
|
|
59
63
|
{...mediaPlayerProps}
|
|
60
64
|
/>
|
|
@@ -66,7 +70,7 @@ const AttachmentThumbnail: React.FC<AttachmentThumbnailProps> = ({
|
|
|
66
70
|
return (
|
|
67
71
|
<div className="relative aspect-video overflow-hidden bg-black/5">
|
|
68
72
|
<img
|
|
69
|
-
src={sourceUrl}
|
|
73
|
+
src={optimizedSourceUrl ?? sourceUrl}
|
|
70
74
|
alt={title ?? ''}
|
|
71
75
|
className={`absolute inset-0 h-full w-full object-contain transition-opacity duration-300 ${sourceReady ? 'opacity-100' : 'opacity-0'}`}
|
|
72
76
|
draggable={false}
|
|
@@ -77,7 +81,7 @@ const AttachmentThumbnail: React.FC<AttachmentThumbnailProps> = ({
|
|
|
77
81
|
}
|
|
78
82
|
return (
|
|
79
83
|
<img
|
|
80
|
-
src={sourceUrl}
|
|
84
|
+
src={optimizedSourceUrl ?? sourceUrl}
|
|
81
85
|
alt={title ?? ''}
|
|
82
86
|
className="block w-full"
|
|
83
87
|
draggable={false}
|
|
@@ -91,7 +95,7 @@ const AttachmentThumbnail: React.FC<AttachmentThumbnailProps> = ({
|
|
|
91
95
|
return (
|
|
92
96
|
<div className="relative aspect-video overflow-hidden bg-black/5">
|
|
93
97
|
<img
|
|
94
|
-
src={thumbnailUrl}
|
|
98
|
+
src={optimizedThumbnailUrl ?? thumbnailUrl}
|
|
95
99
|
alt={title ?? ''}
|
|
96
100
|
className={`absolute inset-0 h-full w-full object-contain transition-opacity duration-300 ${sourceReady ? 'opacity-100' : 'opacity-0'}`}
|
|
97
101
|
draggable={false}
|
|
@@ -102,7 +106,7 @@ const AttachmentThumbnail: React.FC<AttachmentThumbnailProps> = ({
|
|
|
102
106
|
}
|
|
103
107
|
return (
|
|
104
108
|
<img
|
|
105
|
-
src={thumbnailUrl}
|
|
109
|
+
src={optimizedThumbnailUrl ?? thumbnailUrl}
|
|
106
110
|
alt=""
|
|
107
111
|
className="block w-full"
|
|
108
112
|
draggable={false}
|
|
@@ -126,7 +130,7 @@ const AttachmentThumbnail: React.FC<AttachmentThumbnailProps> = ({
|
|
|
126
130
|
return (
|
|
127
131
|
<div className={`relative ${posterShellClass(variant)}`}>
|
|
128
132
|
<img
|
|
129
|
-
src={thumbnailUrl}
|
|
133
|
+
src={optimizedThumbnailUrl ?? thumbnailUrl}
|
|
130
134
|
alt={title ?? ''}
|
|
131
135
|
draggable={false}
|
|
132
136
|
className="absolute inset-0 h-full w-full object-cover"
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from '@phosphor-icons/react'
|
|
8
8
|
import classNames from 'classnames'
|
|
9
9
|
import React from 'react'
|
|
10
|
-
import type { Channel as ChannelType } from 'stream-chat'
|
|
10
|
+
import type { ChannelMemberResponse, Channel as ChannelType } from 'stream-chat'
|
|
11
11
|
import { useChannelStateContext, useChatContext } from 'stream-chat-react'
|
|
12
12
|
|
|
13
13
|
import { useChannelStar } from '../hooks/useChannelStar'
|
|
@@ -30,6 +30,17 @@ const isCustomerPaidTag = (value: unknown) =>
|
|
|
30
30
|
.replace(/[\s-]+/g, '_')
|
|
31
31
|
.toUpperCase() === CUSTOMER_PAID_TAG
|
|
32
32
|
|
|
33
|
+
const hasCustomerPaidTag = (participant?: ChannelMemberResponse) => {
|
|
34
|
+
const member = participant as
|
|
35
|
+
| {
|
|
36
|
+
customerTag?: unknown
|
|
37
|
+
customer_tag?: unknown
|
|
38
|
+
}
|
|
39
|
+
| undefined
|
|
40
|
+
|
|
41
|
+
return [member?.customerTag, member?.customer_tag].some(isCustomerPaidTag)
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
interface ChannelHeaderRedesignProps {
|
|
34
45
|
className?: string
|
|
35
46
|
onBack?: () => void
|
|
@@ -85,9 +96,7 @@ export const ChannelHeaderRedesign: React.FC<ChannelHeaderRedesignProps> = ({
|
|
|
85
96
|
const participantName = resolveParticipantDisplayName(participant?.user)
|
|
86
97
|
const participantImage = participant?.user?.image
|
|
87
98
|
const isStarred = useChannelStar(channel)
|
|
88
|
-
const isPaidCustomer =
|
|
89
|
-
isCustomerPaidTag(channel.data?.customer_tag) &&
|
|
90
|
-
participant?.is_follower === true
|
|
99
|
+
const isPaidCustomer = hasCustomerPaidTag(participant)
|
|
91
100
|
|
|
92
101
|
const handleStarClick = async () => {
|
|
93
102
|
try {
|
|
@@ -44,7 +44,7 @@ const createMockChannel = async (
|
|
|
44
44
|
hasMessages = true,
|
|
45
45
|
followerStatus?: string | boolean,
|
|
46
46
|
isFrozen = false,
|
|
47
|
-
|
|
47
|
+
participantCustomerTag?: string,
|
|
48
48
|
hasScrollableMessages = false
|
|
49
49
|
) => {
|
|
50
50
|
const participant = mockParticipants[0]
|
|
@@ -140,7 +140,6 @@ const createMockChannel = async (
|
|
|
140
140
|
const channelData: Record<string, unknown> = {
|
|
141
141
|
members: [mockUser.id, participant.id],
|
|
142
142
|
frozen: isFrozen,
|
|
143
|
-
...(channelCustomerTag ? { customer_tag: channelCustomerTag } : {}),
|
|
144
143
|
}
|
|
145
144
|
|
|
146
145
|
// Add follower status if provided
|
|
@@ -179,7 +178,7 @@ const createMockChannel = async (
|
|
|
179
178
|
user_id: participant.id,
|
|
180
179
|
role: 'member',
|
|
181
180
|
is_account: true,
|
|
182
|
-
|
|
181
|
+
customerTag: participantCustomerTag,
|
|
183
182
|
},
|
|
184
183
|
}
|
|
185
184
|
return {
|
|
@@ -215,7 +214,7 @@ type TemplateProps = ComponentProps & {
|
|
|
215
214
|
name?: string
|
|
216
215
|
image?: string
|
|
217
216
|
}
|
|
218
|
-
|
|
217
|
+
participantCustomerTag?: string
|
|
219
218
|
hasScrollableMessages?: boolean
|
|
220
219
|
}
|
|
221
220
|
|
|
@@ -224,7 +223,7 @@ const Template: StoryFn<TemplateProps> = (args) => {
|
|
|
224
223
|
hasMessages = true,
|
|
225
224
|
followerStatus,
|
|
226
225
|
isFrozen = false,
|
|
227
|
-
|
|
226
|
+
participantCustomerTag,
|
|
228
227
|
hasScrollableMessages,
|
|
229
228
|
typingUser,
|
|
230
229
|
...channelViewProps
|
|
@@ -239,7 +238,7 @@ const Template: StoryFn<TemplateProps> = (args) => {
|
|
|
239
238
|
hasMessages,
|
|
240
239
|
followerStatus,
|
|
241
240
|
isFrozen,
|
|
242
|
-
|
|
241
|
+
participantCustomerTag,
|
|
243
242
|
hasScrollableMessages
|
|
244
243
|
).then((mockChannel) => {
|
|
245
244
|
setChannel(mockChannel)
|
|
@@ -249,7 +248,7 @@ const Template: StoryFn<TemplateProps> = (args) => {
|
|
|
249
248
|
hasMessages,
|
|
250
249
|
followerStatus,
|
|
251
250
|
isFrozen,
|
|
252
|
-
|
|
251
|
+
participantCustomerTag,
|
|
253
252
|
hasScrollableMessages,
|
|
254
253
|
])
|
|
255
254
|
|
|
@@ -306,7 +305,7 @@ export const ParticipantIdentityTrigger: StoryFn<TemplateProps> = Template.bind(
|
|
|
306
305
|
ParticipantIdentityTrigger.args = {
|
|
307
306
|
showBackButton: false,
|
|
308
307
|
showStarButton: true,
|
|
309
|
-
|
|
308
|
+
participantCustomerTag: 'CUSTOMER_PAID',
|
|
310
309
|
hasScrollableMessages: true,
|
|
311
310
|
onParticipantNameClick: () =>
|
|
312
311
|
// eslint-disable-next-line no-alert
|
|
@@ -111,14 +111,12 @@ const createChannel = ({
|
|
|
111
111
|
username: undefined as string | undefined,
|
|
112
112
|
is_account: true,
|
|
113
113
|
},
|
|
114
|
-
|
|
115
|
-
otherMemberIsFollower,
|
|
114
|
+
otherMemberCustomerTag,
|
|
116
115
|
}: {
|
|
117
116
|
currentUserId?: 'visitor-1' | 'linker-1'
|
|
118
117
|
currentUserRole?: 'owner' | 'member'
|
|
119
118
|
messages?: Channel['state']['messages']
|
|
120
|
-
|
|
121
|
-
otherMemberIsFollower?: boolean
|
|
119
|
+
otherMemberCustomerTag?: string
|
|
122
120
|
otherUser?: {
|
|
123
121
|
id: string
|
|
124
122
|
name?: string
|
|
@@ -129,9 +127,7 @@ const createChannel = ({
|
|
|
129
127
|
({
|
|
130
128
|
id: 'channel-1',
|
|
131
129
|
cid: 'messaging:channel-1',
|
|
132
|
-
data: {
|
|
133
|
-
customer_tag: channelCustomerTag,
|
|
134
|
-
},
|
|
130
|
+
data: {},
|
|
135
131
|
_client: { userID: currentUserId },
|
|
136
132
|
state: {
|
|
137
133
|
members: {
|
|
@@ -151,7 +147,7 @@ const createChannel = ({
|
|
|
151
147
|
(currentUserId === 'visitor-1' ? 'linker-1' : 'visitor-1'),
|
|
152
148
|
is_account: otherUser.is_account ?? currentUserId === 'visitor-1',
|
|
153
149
|
},
|
|
154
|
-
|
|
150
|
+
customerTag: otherMemberCustomerTag,
|
|
155
151
|
role: currentUserRole === 'owner' ? 'member' : 'owner',
|
|
156
152
|
},
|
|
157
153
|
},
|
|
@@ -255,8 +251,7 @@ describe('ChannelView', () => {
|
|
|
255
251
|
renderWithProviders(
|
|
256
252
|
<ChannelView
|
|
257
253
|
channel={createChannel({
|
|
258
|
-
|
|
259
|
-
otherMemberIsFollower: true,
|
|
254
|
+
otherMemberCustomerTag: 'CUSTOMER_PAID',
|
|
260
255
|
})}
|
|
261
256
|
onParticipantNameClick={vi.fn()}
|
|
262
257
|
showStarButton={true}
|
|
@@ -267,20 +262,6 @@ describe('ChannelView', () => {
|
|
|
267
262
|
expect(screen.getByLabelText('Starred conversation')).toBeInTheDocument()
|
|
268
263
|
})
|
|
269
264
|
|
|
270
|
-
it('hides the paid customer badge when the participant is not a follower', () => {
|
|
271
|
-
renderWithProviders(
|
|
272
|
-
<ChannelView
|
|
273
|
-
channel={createChannel({
|
|
274
|
-
channelCustomerTag: 'CUSTOMER_PAID',
|
|
275
|
-
otherMemberIsFollower: false,
|
|
276
|
-
})}
|
|
277
|
-
onParticipantNameClick={vi.fn()}
|
|
278
|
-
/>
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
expect(screen.queryByLabelText('Paid customer')).not.toBeInTheDocument()
|
|
282
|
-
})
|
|
283
|
-
|
|
284
265
|
it('renders the redesigned header outside MessageList head when all messages are deleted', () => {
|
|
285
266
|
renderWithProviders(
|
|
286
267
|
<ChannelView
|
package/src/components/CustomMessage/LockedAttachment/components/_shared/GalleryThumbnail.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { CaretLeftIcon, CaretRightIcon } from '@phosphor-icons/react'
|
|
|
2
2
|
import classNames from 'classnames'
|
|
3
3
|
import React, { useState } from 'react'
|
|
4
4
|
|
|
5
|
+
import { optimizeMessagingAttachmentUrl } from '../../../../../utils/cdnImageUrl'
|
|
5
6
|
import { AttachmentThumbnail, getSourceType } from '../../../../AttachmentCard'
|
|
6
7
|
import type {
|
|
7
8
|
LockedAttachmentGalleryItem,
|
|
@@ -46,6 +47,9 @@ const GalleryThumbnail: React.FC<GalleryThumbnailProps> = ({
|
|
|
46
47
|
const safeIndex = Math.min(index, total - 1)
|
|
47
48
|
const current = gallery[safeIndex]
|
|
48
49
|
const isVideoItem = isPlayableItem(current)
|
|
50
|
+
const optimizedCurrentThumbnailUrl = optimizeMessagingAttachmentUrl(
|
|
51
|
+
current.thumbnailUrl
|
|
52
|
+
)
|
|
49
53
|
|
|
50
54
|
// Stop propagation so arrow taps don't bubble up to the outer preview-toggle
|
|
51
55
|
// wrapper on PreviewMediaCard / OwnMediaCard.
|
|
@@ -90,7 +94,7 @@ const GalleryThumbnail: React.FC<GalleryThumbnailProps> = ({
|
|
|
90
94
|
>
|
|
91
95
|
{current.thumbnailUrl && (
|
|
92
96
|
<img
|
|
93
|
-
src={current.thumbnailUrl}
|
|
97
|
+
src={optimizedCurrentThumbnailUrl ?? current.thumbnailUrl}
|
|
94
98
|
alt=""
|
|
95
99
|
aria-hidden
|
|
96
100
|
draggable={false}
|
package/src/components/CustomMessage/LockedAttachment/components/_shared/SingleThumbnail.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import classNames from 'classnames'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
|
+
import { optimizeMessagingAttachmentUrl } from '../../../../../utils/cdnImageUrl'
|
|
4
5
|
import {
|
|
5
6
|
AttachmentThumbnail,
|
|
6
7
|
renderTypeIcon,
|
|
@@ -55,6 +56,9 @@ const SingleThumbnail: React.FC<SingleThumbnailProps> = ({
|
|
|
55
56
|
const effectiveThumbnailUrl = showLocked
|
|
56
57
|
? thumbnailUrl
|
|
57
58
|
: (source?.thumbnailUrl ?? thumbnailUrl)
|
|
59
|
+
const optimizedThumbnailUrl = optimizeMessagingAttachmentUrl(
|
|
60
|
+
effectiveThumbnailUrl
|
|
61
|
+
)
|
|
58
62
|
|
|
59
63
|
const isDark = variant === 'dark'
|
|
60
64
|
|
|
@@ -80,7 +84,7 @@ const SingleThumbnail: React.FC<SingleThumbnailProps> = ({
|
|
|
80
84
|
>
|
|
81
85
|
{effectiveThumbnailUrl ? (
|
|
82
86
|
<img
|
|
83
|
-
src={effectiveThumbnailUrl}
|
|
87
|
+
src={optimizedThumbnailUrl ?? effectiveThumbnailUrl}
|
|
84
88
|
alt=""
|
|
85
89
|
aria-hidden
|
|
86
90
|
draggable={false}
|
|
@@ -25,6 +25,18 @@ describe('linkCardPropsFromStreamAttachment', () => {
|
|
|
25
25
|
).toBe('featured')
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
+
it('optimizes Linktree-hosted thumbnails for messaging attachment renders', () => {
|
|
29
|
+
expect(
|
|
30
|
+
linkCardPropsFromStreamAttachment({
|
|
31
|
+
type: 'link',
|
|
32
|
+
og_scrape_url: 'https://linktr.ee/someone',
|
|
33
|
+
image_url: 'https://assets.linktr.ee/thumb.jpg',
|
|
34
|
+
}).thumbnailUrl
|
|
35
|
+
).toBe(
|
|
36
|
+
'https://assets.linktr.ee/thumb.jpg?io=true&size=messaging-attachment-v1_0'
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
|
|
28
40
|
it('uses the classic layout for a plain link with no thumbnail', () => {
|
|
29
41
|
expect(
|
|
30
42
|
linkCardPropsFromStreamAttachment({
|
|
@@ -152,6 +164,27 @@ describe('StreamAttachmentMessage', () => {
|
|
|
152
164
|
expect(container).toHaveTextContent('caption')
|
|
153
165
|
})
|
|
154
166
|
|
|
167
|
+
it('optimizes Linktree-hosted image attachments for the channel bubble', () => {
|
|
168
|
+
renderWithProviders(
|
|
169
|
+
<StreamAttachmentMessage
|
|
170
|
+
groupPosition="single"
|
|
171
|
+
isMyMessage={false}
|
|
172
|
+
message={message([
|
|
173
|
+
{
|
|
174
|
+
type: 'image',
|
|
175
|
+
image_url: 'https://ugc.production.linktr.ee/photo.jpg',
|
|
176
|
+
title: 'Vacation',
|
|
177
|
+
},
|
|
178
|
+
])}
|
|
179
|
+
/>
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
expect(screen.getByRole('img', { name: 'Vacation' })).toHaveAttribute(
|
|
183
|
+
'src',
|
|
184
|
+
'https://ugc.production.linktr.ee/photo.jpg?io=true&size=messaging-attachment-v1_0'
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
|
|
155
188
|
it('renders the accompanying message text next to a link attachment', () => {
|
|
156
189
|
const { container } = renderWithProviders(
|
|
157
190
|
<StreamAttachmentMessage
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type MessageContextValue,
|
|
7
7
|
} from 'stream-chat-react'
|
|
8
8
|
|
|
9
|
+
import { optimizeMessagingAttachmentUrl } from '../../utils/cdnImageUrl'
|
|
9
10
|
import LinkAttachment from '../LinkAttachment'
|
|
10
11
|
import { isPlayableMedia } from '../LinkAttachment/components/_shared/CardThumbnail'
|
|
11
12
|
import MessageAttachment from '../MessageAttachment'
|
|
@@ -65,7 +66,7 @@ export function linkCardPropsFromStreamAttachment(a: Attachment) {
|
|
|
65
66
|
title: trimToUndefined((a as { title?: string }).title),
|
|
66
67
|
description: trimToUndefined((a as { text?: string }).text),
|
|
67
68
|
url,
|
|
68
|
-
thumbnailUrl,
|
|
69
|
+
thumbnailUrl: optimizeMessagingAttachmentUrl(thumbnailUrl),
|
|
69
70
|
// Playable clip embedded in a link preview — lets LinkAttachment render its
|
|
70
71
|
// native video/audio player in the hero instead of a static thumbnail.
|
|
71
72
|
sourceUrl,
|
|
@@ -166,8 +167,10 @@ export function bubblePositionInRun(
|
|
|
166
167
|
|
|
167
168
|
function imageSrcFromAttachment(attachment: Attachment): string {
|
|
168
169
|
return (
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
optimizeMessagingAttachmentUrl(
|
|
171
|
+
trimToUndefined((attachment as { image_url?: string }).image_url)
|
|
172
|
+
) ??
|
|
173
|
+
optimizeMessagingAttachmentUrl(trimToUndefined(attachment.asset_url)) ??
|
|
171
174
|
''
|
|
172
175
|
)
|
|
173
176
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import classNames from 'classnames'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
|
+
import { optimizeMessagingAttachmentUrl } from '../../../../utils/cdnImageUrl'
|
|
4
5
|
import { renderTypeIcon } from '../../../AttachmentCard'
|
|
5
6
|
import { getSourceType } from '../../../AttachmentCard/utils/mimeType'
|
|
6
7
|
|
|
@@ -81,6 +82,7 @@ const CardThumbnail: React.FC<CardThumbnailProps> = ({
|
|
|
81
82
|
}) => {
|
|
82
83
|
const sourceType = getSourceType(mimeType)
|
|
83
84
|
const isPlayableVideo = !!sourceUrl && sourceType === 'video'
|
|
85
|
+
const optimizedThumbnailUrl = optimizeMessagingAttachmentUrl(thumbnailUrl)
|
|
84
86
|
|
|
85
87
|
if (isPlayableAudio(mimeType, sourceUrl)) {
|
|
86
88
|
// Audio collapses the hero entirely — the native player sits inside
|
|
@@ -110,7 +112,7 @@ const CardThumbnail: React.FC<CardThumbnailProps> = ({
|
|
|
110
112
|
{isPlayableVideo ? (
|
|
111
113
|
<video
|
|
112
114
|
src={sourceUrl}
|
|
113
|
-
poster={
|
|
115
|
+
poster={optimizedThumbnailUrl}
|
|
114
116
|
controls
|
|
115
117
|
playsInline
|
|
116
118
|
preload="metadata"
|
|
@@ -120,7 +122,7 @@ const CardThumbnail: React.FC<CardThumbnailProps> = ({
|
|
|
120
122
|
</video>
|
|
121
123
|
) : thumbnailUrl ? (
|
|
122
124
|
<img
|
|
123
|
-
src={thumbnailUrl}
|
|
125
|
+
src={optimizedThumbnailUrl ?? thumbnailUrl}
|
|
124
126
|
alt={title ?? ''}
|
|
125
127
|
draggable={false}
|
|
126
128
|
className="absolute inset-0 h-full w-full object-cover"
|
|
@@ -2,6 +2,7 @@ import { CircleNotchIcon, DownloadSimpleIcon, LinkIcon } from '@phosphor-icons/r
|
|
|
2
2
|
import React, { useState } from 'react'
|
|
3
3
|
import type { Attachment, LocalMessage } from 'stream-chat'
|
|
4
4
|
|
|
5
|
+
import { optimizeMessagingAttachmentUrl } from '../../utils/cdnImageUrl'
|
|
5
6
|
import AttachmentCard, {
|
|
6
7
|
AttachmentThumbnail,
|
|
7
8
|
getSourceType,
|
|
@@ -54,7 +55,7 @@ const LinkCard: React.FC<{
|
|
|
54
55
|
<div className="p-2">
|
|
55
56
|
{image_url ? (
|
|
56
57
|
<img
|
|
57
|
-
src={image_url}
|
|
58
|
+
src={optimizeMessagingAttachmentUrl(image_url) ?? image_url}
|
|
58
59
|
alt={title ?? ''}
|
|
59
60
|
className="aspect-video w-full rounded-[20px] object-cover"
|
|
60
61
|
/>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
|
|
3
|
+
import { optimizeMessagingAttachmentUrl } from '../../../utils/cdnImageUrl'
|
|
3
4
|
import Bubble from '../_shared/Bubble'
|
|
4
5
|
import DismissButton from '../_shared/DismissButton'
|
|
5
6
|
import ImageViewer, {
|
|
@@ -61,7 +62,7 @@ const tileFromItem = (
|
|
|
61
62
|
ariaLabel: `Open image ${index + 1} of ${totalCount}`,
|
|
62
63
|
content: (
|
|
63
64
|
<img
|
|
64
|
-
src={item.src}
|
|
65
|
+
src={optimizeMessagingAttachmentUrl(item.src) ?? item.src}
|
|
65
66
|
alt={item.alt ?? ''}
|
|
66
67
|
width={item.width}
|
|
67
68
|
height={item.height}
|
|
@@ -148,7 +149,7 @@ const ImageComposerInner: React.FC<{
|
|
|
148
149
|
className="block size-[280px] cursor-zoom-in overflow-hidden rounded-md outline-none focus-visible:ring-2 focus-visible:ring-black/40"
|
|
149
150
|
>
|
|
150
151
|
<img
|
|
151
|
-
src={src}
|
|
152
|
+
src={optimizeMessagingAttachmentUrl(src) ?? src}
|
|
152
153
|
alt={alt ?? ''}
|
|
153
154
|
draggable={false}
|
|
154
155
|
loading={loading}
|
|
@@ -33,6 +33,26 @@ const expectViewerOpen = (testId: string) => {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
describe('MessageAttachment.Image', () => {
|
|
36
|
+
it('optimizes Linktree-hosted composer previews without changing the viewer source', () => {
|
|
37
|
+
renderWithProviders(
|
|
38
|
+
<MessageAttachment.Image.Composer
|
|
39
|
+
src="https://assets.linktr.ee/photo.jpg"
|
|
40
|
+
alt="Photo"
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
expect(screen.getByRole('img', { name: 'Photo' })).toHaveAttribute(
|
|
45
|
+
'src',
|
|
46
|
+
'https://assets.linktr.ee/photo.jpg?io=true&size=messaging-attachment-v1_0'
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
fireEvent.click(screen.getByLabelText('Open image'))
|
|
50
|
+
const viewer = expectViewerOpen('image-viewer')
|
|
51
|
+
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
52
|
+
'https://assets.linktr.ee/photo.jpg'
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
36
56
|
it('renders an image bubble with caption', () => {
|
|
37
57
|
renderWithProviders(
|
|
38
58
|
<MessageAttachment.Image.Sent
|
|
@@ -97,17 +97,6 @@ declare module 'stream-chat' {
|
|
|
97
97
|
* Whether DM agent auto-replies are paused for this channel.
|
|
98
98
|
*/
|
|
99
99
|
chatbot_paused?: boolean
|
|
100
|
-
/**
|
|
101
|
-
* Customer classification tag for this conversation (e.g. CUSTOMER_PAID).
|
|
102
|
-
*/
|
|
103
|
-
customer_tag?: string
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
interface CustomMemberData {
|
|
107
|
-
/**
|
|
108
|
-
* Whether this member follows the channel's linker account.
|
|
109
|
-
*/
|
|
110
|
-
is_follower?: boolean
|
|
111
100
|
}
|
|
112
101
|
|
|
113
102
|
interface CustomMessageData {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const LINKTREE_CDN_HOSTS = ['linktr.ee', 'cloudfront.net', 'cloudinary']
|
|
2
|
+
|
|
3
|
+
const ATTACHMENT_IMAGE_SIZE_PRESET = 'messaging-attachment-v1_0'
|
|
4
|
+
|
|
5
|
+
function hostnameFromUrl(url: string): string | null {
|
|
6
|
+
try {
|
|
7
|
+
return new URL(url).hostname.toLowerCase()
|
|
8
|
+
} catch {
|
|
9
|
+
return null
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isLinktreeCdnHost(hostname: string): boolean {
|
|
14
|
+
return LINKTREE_CDN_HOSTS.some((host) => hostname.includes(host))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function optimizeMessagingAttachmentUrl(
|
|
18
|
+
url: string | undefined | null
|
|
19
|
+
): string | undefined {
|
|
20
|
+
const trimmed = url?.trim()
|
|
21
|
+
if (!trimmed) return undefined
|
|
22
|
+
|
|
23
|
+
const hostname = hostnameFromUrl(trimmed)
|
|
24
|
+
if (!hostname || !isLinktreeCdnHost(hostname)) return trimmed
|
|
25
|
+
|
|
26
|
+
if (/[?&]io=true(?:&|$)/.test(trimmed) && /[?&]size=/.test(trimmed)) {
|
|
27
|
+
return trimmed
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const separator = trimmed.includes('?') ? '&' : '?'
|
|
31
|
+
return `${trimmed}${separator}io=true&size=${ATTACHMENT_IMAGE_SIZE_PRESET}`
|
|
32
|
+
}
|