@linktr.ee/messaging-react 3.12.2 → 3.12.3-rc-1784034329
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-Dd-lw8HD.cjs +2 -0
- package/dist/Card-Dd-lw8HD.cjs.map +1 -0
- package/dist/{Card-Za6WfwFP.js → Card-dFpcKsyN.js} +123 -119
- package/dist/Card-dFpcKsyN.js.map +1 -0
- package/dist/{index-KfZ2Wx3D.js → index-BvuCjzqJ.js} +958 -936
- package/dist/index-BvuCjzqJ.js.map +1 -0
- package/dist/index-C1eZGd_f.cjs +2 -0
- package/dist/index-C1eZGd_f.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/components/AttachmentCard/Thumbnail.tsx +10 -6
- package/src/components/ChannelView.test.tsx +3 -23
- package/src/components/ChannelView.tsx +0 -1
- 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 +58 -1
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +2 -1
- 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/utils/cdnImageUrl.test.ts +37 -0
- package/src/utils/cdnImageUrl.ts +34 -0
- package/dist/Card-Bs3dV_7R.cjs +0 -2
- package/dist/Card-Bs3dV_7R.cjs.map +0 -1
- package/dist/Card-Za6WfwFP.js.map +0 -1
- package/dist/index-KfZ2Wx3D.js.map +0 -1
- package/dist/index-gl5pV2zJ.cjs +0 -2
- package/dist/index-gl5pV2zJ.cjs.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-C1eZGd_f.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.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-BvuCjzqJ.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
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-1784034329",
|
|
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": "
|
|
53
|
+
"@linktr.ee/messaging-core": "2.3.0-rc-1784034329",
|
|
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"
|
|
@@ -8,7 +8,6 @@ import { ChannelView } from './ChannelView'
|
|
|
8
8
|
|
|
9
9
|
let activeChannel: unknown
|
|
10
10
|
let activeChannelProps: Record<string, unknown> = {}
|
|
11
|
-
let activeComponentOverrides: Record<string, unknown> = {}
|
|
12
11
|
|
|
13
12
|
vi.mock('stream-chat-react', () => ({
|
|
14
13
|
Channel: (props: {
|
|
@@ -26,16 +25,9 @@ vi.mock('stream-chat-react', () => ({
|
|
|
26
25
|
MessageList: ({ head }: { head?: React.ReactElement }) => (
|
|
27
26
|
<div data-testid="message-list">{head}</div>
|
|
28
27
|
),
|
|
29
|
-
WithComponents: ({
|
|
30
|
-
children
|
|
31
|
-
|
|
32
|
-
}: {
|
|
33
|
-
children: React.ReactNode
|
|
34
|
-
overrides?: Record<string, unknown>
|
|
35
|
-
}) => {
|
|
36
|
-
activeComponentOverrides = overrides ?? {}
|
|
37
|
-
return <>{children}</>
|
|
38
|
-
},
|
|
28
|
+
WithComponents: ({ children }: { children: React.ReactNode }) => (
|
|
29
|
+
<>{children}</>
|
|
30
|
+
),
|
|
39
31
|
useMessageContext: () => ({ message: { id: 'message-1', text: 'hello' } }),
|
|
40
32
|
useChannelStateContext: () => ({ channel: activeChannel }),
|
|
41
33
|
useChatContext: () => ({
|
|
@@ -166,7 +158,6 @@ describe('ChannelView', () => {
|
|
|
166
158
|
beforeEach(() => {
|
|
167
159
|
activeChannel = undefined
|
|
168
160
|
activeChannelProps = {}
|
|
169
|
-
activeComponentOverrides = {}
|
|
170
161
|
avatarRenderCalls.length = 0
|
|
171
162
|
channelActionsMenuProps.length = 0
|
|
172
163
|
messageInputProps.length = 0
|
|
@@ -291,17 +282,6 @@ describe('ChannelView', () => {
|
|
|
291
282
|
).toBeGreaterThan(0)
|
|
292
283
|
})
|
|
293
284
|
|
|
294
|
-
it('suppresses stream unread message UI overrides', () => {
|
|
295
|
-
renderWithProviders(<ChannelView channel={createChannel()} />)
|
|
296
|
-
|
|
297
|
-
expect(activeComponentOverrides.UnreadMessagesSeparator).toBeTypeOf(
|
|
298
|
-
'function'
|
|
299
|
-
)
|
|
300
|
-
expect(activeComponentOverrides.UnreadMessagesNotification).toBeTypeOf(
|
|
301
|
-
'function'
|
|
302
|
-
)
|
|
303
|
-
})
|
|
304
|
-
|
|
305
285
|
it('hides the actions menu (both desktop and mobile) when showChannelInfo is false', () => {
|
|
306
286
|
renderWithProviders(
|
|
307
287
|
<ChannelView channel={createChannel()} showChannelInfo={false} />
|
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}
|
|
@@ -2,7 +2,7 @@ import React from 'react'
|
|
|
2
2
|
import type { Attachment, LocalMessage } from 'stream-chat'
|
|
3
3
|
import { describe, expect, it } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import { renderWithProviders, screen } from '../../test/utils'
|
|
5
|
+
import { fireEvent, renderWithProviders, screen } from '../../test/utils'
|
|
6
6
|
|
|
7
7
|
import StreamAttachmentMessage, {
|
|
8
8
|
linkCaptionDuplicatesAttachmentUrl,
|
|
@@ -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,51 @@ 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
|
+
|
|
188
|
+
it('keeps the original image source in the viewer for channel image attachments', () => {
|
|
189
|
+
renderWithProviders(
|
|
190
|
+
<StreamAttachmentMessage
|
|
191
|
+
groupPosition="single"
|
|
192
|
+
isMyMessage={false}
|
|
193
|
+
message={message([
|
|
194
|
+
{
|
|
195
|
+
type: 'image',
|
|
196
|
+
image_url: 'https://ugc.production.linktr.ee/photo.jpg',
|
|
197
|
+
title: 'Vacation',
|
|
198
|
+
},
|
|
199
|
+
])}
|
|
200
|
+
/>
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
fireEvent.click(screen.getByLabelText('Open image 1 of 1'))
|
|
204
|
+
const viewer = screen.getByTestId('image-viewer')
|
|
205
|
+
|
|
206
|
+
expect(viewer).toHaveAttribute('open')
|
|
207
|
+
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
208
|
+
'https://ugc.production.linktr.ee/photo.jpg'
|
|
209
|
+
)
|
|
210
|
+
})
|
|
211
|
+
|
|
155
212
|
it('renders the accompanying message text next to a link attachment', () => {
|
|
156
213
|
const { container } = renderWithProviders(
|
|
157
214
|
<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,
|
|
@@ -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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { optimizeMessagingAttachmentUrl } from './cdnImageUrl'
|
|
4
|
+
|
|
5
|
+
describe('optimizeMessagingAttachmentUrl', () => {
|
|
6
|
+
it('optimizes Linktree-owned asset hosts', () => {
|
|
7
|
+
expect(
|
|
8
|
+
optimizeMessagingAttachmentUrl('https://assets.linktr.ee/photo.jpg')
|
|
9
|
+
).toBe(
|
|
10
|
+
'https://assets.linktr.ee/photo.jpg?io=true&size=messaging-attachment-v1_0'
|
|
11
|
+
)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('does not optimize unrelated CloudFront hosts', () => {
|
|
15
|
+
expect(
|
|
16
|
+
optimizeMessagingAttachmentUrl('https://d111111abcdef8.cloudfront.net/photo.jpg')
|
|
17
|
+
).toBe('https://d111111abcdef8.cloudfront.net/photo.jpg')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('does not optimize unrelated Cloudinary hosts', () => {
|
|
21
|
+
expect(
|
|
22
|
+
optimizeMessagingAttachmentUrl(
|
|
23
|
+
'https://res.cloudinary.com/demo/image/upload/sample.jpg'
|
|
24
|
+
)
|
|
25
|
+
).toBe('https://res.cloudinary.com/demo/image/upload/sample.jpg')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('does not append the preset twice', () => {
|
|
29
|
+
expect(
|
|
30
|
+
optimizeMessagingAttachmentUrl(
|
|
31
|
+
'https://ugc.production.linktr.ee/photo.jpg?io=true&size=messaging-attachment-v1_0'
|
|
32
|
+
)
|
|
33
|
+
).toBe(
|
|
34
|
+
'https://ugc.production.linktr.ee/photo.jpg?io=true&size=messaging-attachment-v1_0'
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const LINKTREE_CDN_HOSTS = ['linktr.ee']
|
|
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(
|
|
15
|
+
(host) => hostname === host || hostname.endsWith(`.${host}`)
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function optimizeMessagingAttachmentUrl(
|
|
20
|
+
url: string | undefined | null
|
|
21
|
+
): string | undefined {
|
|
22
|
+
const trimmed = url?.trim()
|
|
23
|
+
if (!trimmed) return undefined
|
|
24
|
+
|
|
25
|
+
const hostname = hostnameFromUrl(trimmed)
|
|
26
|
+
if (!hostname || !isLinktreeCdnHost(hostname)) return trimmed
|
|
27
|
+
|
|
28
|
+
if (/[?&]io=true(?:&|$)/.test(trimmed) && /[?&]size=/.test(trimmed)) {
|
|
29
|
+
return trimmed
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const separator = trimmed.includes('?') ? '&' : '?'
|
|
33
|
+
return `${trimmed}${separator}io=true&size=${ATTACHMENT_IMAGE_SIZE_PRESET}`
|
|
34
|
+
}
|
package/dist/Card-Bs3dV_7R.cjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),b=require("@phosphor-icons/react"),h=require("react"),v=require("classnames"),k=require("./index-gl5pV2zJ.cjs"),T=require("./LoadingDots-D9GGphgM.cjs"),P=({variant:t,title:s,placeholderTitle:n="Attachment title",mimeType:i,detail:a,statusBadge:r,action:c,icon:u,trailingAction:d})=>{const l=t==="dark",o=s||n,x=!s,f=u??k.renderTypeIcon(i,{className:v("size-5 shrink-0",l?"text-white/55":"text-black/55"),weight:"regular"});return e.jsxs("div",{className:"px-4 py-3",children:[e.jsxs("div",{className:"flex items-end gap-3",children:[e.jsxs("div",{className:"flex min-w-0 flex-1 flex-col gap-1",children:[o.trim()!==""&&e.jsx("p",{className:v("truncate text-base font-medium leading-6",{"text-black/90":!l&&!x,"text-black/30":!l&&x,"text-white/30":l&&x,"text-white":l&&!x}),children:o}),e.jsxs("div",{className:"flex flex-wrap items-center gap-1",children:[f,a!=null&&a!==""&&e.jsx("span",{className:v("text-xs font-medium",l?"text-white/55":"text-black/55"),children:a}),r]})]}),d&&e.jsx("div",{className:"shrink-0",children:d})]}),c]})},M=({paymentStatus:t,mimeType:s,isPlayable:n,isLocked:i})=>{const a=s?k.getSourceType(s):void 0,c=(n??(a==="video"||a==="audio"))&&!i?b.PlayIcon:t==="paid"?b.LockOpenIcon:b.LockSimpleIcon;return e.jsx("div",{className:"pointer-events-none absolute inset-0 flex items-center justify-center",children:e.jsx("div",{className:v("flex size-12 items-center justify-center rounded-full bg-black/30 text-white",c===b.PlayIcon&&"group-hover:bg-black/40"),children:e.jsx(c,{className:"size-6",weight:"fill"})})})};function q(t){const s=k.getSourceType(t.mimeType);return s==="video"||s==="audio"}const O=({variant:t,gallery:s,title:n,showLocked:i,paymentStatus:a,topRight:r})=>{const[c,u]=h.useState(0),d=s.length,l=Math.min(c,d-1),o=s[l],x=q(o),f=g=>{g.stopPropagation(),u(N=>Math.max(0,N-1))},w=g=>{g.stopPropagation(),u(N=>Math.min(d-1,N+1))},j=l===0,m=l===d-1,p=t==="dark";return e.jsxs("div",{className:v("group relative overflow-hidden rounded-t-md",i?"aspect-[337/386]":void 0),children:[i?e.jsx("div",{className:v("absolute inset-0",p?"bg-white/10":"bg-black/5"),children:o.thumbnailUrl&&e.jsx("img",{src:o.thumbnailUrl,alt:"","aria-hidden":!0,draggable:!1,className:"absolute inset-0 h-full w-full scale-110 object-cover blur-[40px]"})}):e.jsx(k.AttachmentThumbnail,{mimeType:o.mimeType,sourceUrl:o.sourceUrl,thumbnailUrl:o.thumbnailUrl,title:n,variant:t,mediaPlayerProps:{autoPlay:!0}},l),i&&e.jsx(M,{paymentStatus:a,isPlayable:x,isLocked:i}),d>1&&e.jsxs(h.Fragment,{children:[e.jsx("button",{type:"button",onClick:f,disabled:j,"aria-label":"Previous item",className:v("absolute left-3 top-1/2 -translate-y-1/2 flex size-7 items-center justify-center rounded-full bg-black/30 text-white transition-opacity",j?"opacity-40":"hover:bg-black/40"),children:e.jsx(b.CaretLeftIcon,{className:"size-5",weight:"bold"})}),e.jsx("button",{type:"button",onClick:w,disabled:m,"aria-label":"Next item",className:v("absolute right-3 top-1/2 -translate-y-1/2 flex size-7 items-center justify-center rounded-full bg-black/30 text-white transition-opacity",m?"opacity-40":"hover:bg-black/40"),children:e.jsx(b.CaretRightIcon,{className:"size-5",weight:"bold"})}),e.jsx("div",{className:"pointer-events-none absolute bottom-10 left-1/2 flex size-8 -translate-x-1/2 items-center justify-center rounded-full bg-black/30",children:e.jsxs("span",{className:"text-xs font-medium leading-none text-white",children:[l+1,"/",d]})})]}),r&&e.jsx("div",{className:"pointer-events-auto absolute right-3 top-3 z-10",children:r})]})},A=({variant:t,mimeType:s,thumbnailUrl:n,title:i,source:a,showLocked:r,paymentStatus:c,topRight:u,containedImage:d=!1})=>{const l=r||a==null?void 0:a.sourceUrl,o=r?n:(a==null?void 0:a.thumbnailUrl)??n,x=t==="dark";return e.jsxs("div",{className:"relative overflow-hidden rounded-t-md",children:[r?e.jsxs("div",{className:v("relative aspect-video overflow-hidden",x?"bg-white/10":"bg-black/5"),children:[o?e.jsx("img",{src:o,alt:"","aria-hidden":!0,draggable:!1,className:"absolute inset-0 h-full w-full scale-110 object-cover blur-[40px]"}):e.jsx("div",{className:"absolute inset-0 flex items-center justify-center",children:k.renderTypeIcon(s,{className:x?"size-12 text-white/20":"size-12 text-black/20",weight:"regular"})}),e.jsx(M,{paymentStatus:c,mimeType:s,isLocked:r})]}):e.jsx(k.AttachmentThumbnail,{mimeType:s,sourceUrl:l,thumbnailUrl:o,title:i,variant:t,containedImage:d,mediaPlayerProps:l?{autoPlay:!0,loop:!0,controls:!0,muted:!1}:void 0}),u&&e.jsx("div",{className:"pointer-events-auto absolute right-3 top-3 z-10",children:u})]})},y=t=>t.length>=2,I=t=>e.jsx(b.ImagesIcon,{className:t==="dark"?"size-5 shrink-0 text-white/55":"size-5 shrink-0 text-black/55"}),z=({variant:t,gallery:s,title:n,showLocked:i,paymentStatus:a,mimeType:r,thumbnailUrl:c,source:u,containedImage:d,topRight:l})=>y(s)?e.jsx(O,{variant:t,gallery:s,title:n,showLocked:i,paymentStatus:a,topRight:l}):e.jsx(A,{variant:t,mimeType:r,thumbnailUrl:c,title:n,source:u,showLocked:i,paymentStatus:a,topRight:l,containedImage:d}),U=({variant:t,children:s,rootRef:n,"data-testid":i})=>e.jsx("div",{ref:n,"data-testid":i,className:v("relative w-[280px] select-none overflow-hidden rounded-md",t==="dark"?"bg-[#1e2330]":"bg-white","shadow-[0_1px_2px_rgba(0,0,0,0.08)]"),children:s}),C=({onToggle:t,isBusy:s,isPreviewVisible:n,children:i})=>t?e.jsx("div",{role:"button",tabIndex:s?-1:0,"aria-label":"Toggle preview","aria-busy":s,"aria-pressed":n,"aria-disabled":s||void 0,onClick:s?void 0:t,onKeyDown:a=>{s||a.target===a.currentTarget&&(a.key==="Enter"||a.key===" ")&&(a.preventDefault(),t())},className:s?"block w-full text-left":"block w-full cursor-pointer text-left",children:i}):e.jsx(h.Fragment,{children:i});function R({isGallery:t,onFetchSource:s,onPreviewClick:n}){const[i,a]=h.useState(),[r,c]=h.useState(!1),[u,d]=h.useState(!1),l=h.useRef(!1),o=h.useCallback(async()=>{if(n==null||n(),r){c(!1);return}if(t){c(!0);return}if(i){c(!0);return}if(s&&!l.current){l.current=!0,d(!0);try{const f=await s();f&&(a(f),c(!0))}finally{l.current=!1,d(!1)}}},[r,t,i,n,s]),x=t||s!=null||n!=null?o:void 0;return{source:i,isPreviewVisible:r,isLoadingPreview:u,handleToggle:o,togglePreview:x}}function G({paymentStatus:t,onFetchSource:s,onUnlockClick:n}){const[i,a]=h.useState(),r=h.useRef(null),c=h.useRef(!1),u=h.useRef(s);u.current=s;const d=h.useCallback(async()=>{var o;if(!c.current){c.current=!0;try{const x=await((o=u.current)==null?void 0:o.call(u));x&&a(x)}finally{c.current=!1}}},[]),l=h.useCallback(()=>{t==="paid"?d():n==null||n()},[t,d,n]);return h.useEffect(()=>{if(!r.current||t!=="paid"||i!==void 0)return;const o=new IntersectionObserver(([x])=>{x.isIntersecting&&(d(),o.disconnect())},{threshold:1});return o.observe(r.current),()=>o.disconnect()},[t,i,d]),{source:i,cardRef:r,handleUnlockClick:l}}const S="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]",_=({isLocked:t,sourceUrl:s,redeemUrl:n,onUnlockClicked:i,onDownloadClicked:a,isUnlocking:r=!1})=>{if(t&&i!=null)return e.jsx("button",{type:"button",onClick:i,disabled:r,"aria-label":r?"Unlocking":"Unlock","aria-busy":r,title:r?"Unlocking":"Unlock",className:S,children:r?e.jsx(T.LoadingDots,{}):e.jsxs(h.Fragment,{children:[e.jsx(b.LockSimpleIcon,{className:"size-4",weight:"fill"}),"Unlock"]})});if(!t&&a!=null){const c=n??s;return c!=null?e.jsxs("a",{href:c,"aria-label":"Download",target:"_blank",rel:"noopener noreferrer",onClick:a,className:`${S} !text-white`,children:[e.jsx(b.DownloadSimpleIcon,{className:"size-4",weight:"bold"}),"Download"]}):e.jsxs("button",{type:"button",onClick:a,"aria-label":"Download",className:S,children:[e.jsx(b.DownloadSimpleIcon,{className:"size-4",weight:"bold"}),"Download"]})}return null},E=({title:t,mimeType:s="application/octet-stream",thumbnailUrl:n,detail:i,amountText:a,placeholderTitle:r,placeholderAmountText:c,gallery:u=[],onDismiss:d,onPreviewClick:l,onFetchSource:o,onEditClick:x})=>{const f=y(u),{source:w,isPreviewVisible:j,isLoadingPreview:m,togglePreview:p}=R({isGallery:f,onFetchSource:o,onPreviewClick:l}),g=!j,N=m,D=e.jsxs(h.Fragment,{children:[e.jsx("span",{className:"text-xs font-medium text-white/55",children:"•"}),e.jsx("span",{className:a?"text-xs font-medium text-white/55":"text-xs font-medium text-white/30",children:a||c})]}),L=d?e.jsx("button",{type:"button",onClick:V=>{V.stopPropagation(),d()},className:"flex size-6 items-center justify-center rounded-full bg-[#121110] text-white","aria-label":"Dismiss attachment",children:e.jsx(b.XIcon,{className:"size-3",weight:"bold"})}):void 0,B=x?e.jsx("button",{type:"button",onClick:x,"aria-label":"Edit attachment",className:"flex size-10 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/15",children:e.jsx(b.PencilSimpleIcon,{className:"size-5",weight:"regular"})}):void 0;return e.jsxs(U,{variant:"dark",children:[e.jsx(C,{onToggle:p,isBusy:N,isPreviewVisible:!g,children:e.jsx(z,{variant:"dark",gallery:u,title:t,showLocked:g,mimeType:s,thumbnailUrl:n,source:w,topRight:L})}),e.jsx(P,{variant:"dark",title:t,placeholderTitle:r,mimeType:s,detail:i,statusBadge:D,icon:f?I("dark"):void 0,trailingAction:B})]})},$=({title:t,mimeType:s="application/octet-stream",thumbnailUrl:n,detail:i,amountText:a,placeholderTitle:r,placeholderAmountText:c,gallery:u=[]})=>{const d=y(u),l=e.jsxs(h.Fragment,{children:[e.jsx("span",{className:"text-xs font-medium text-black/55",children:"•"}),e.jsx("span",{className:a?"text-xs font-medium text-black/55":"text-xs font-medium text-black/30",children:a||c})]});return e.jsxs(U,{variant:"light",children:[e.jsx(z,{variant:"light",gallery:u,title:t,showLocked:!0,mimeType:s,thumbnailUrl:n}),e.jsx(P,{variant:"light",title:t,placeholderTitle:r,mimeType:s,detail:i,statusBadge:l,icon:d?I("light"):void 0})]})},K=({title:t,mimeType:s="application/octet-stream",thumbnailUrl:n,detail:i,amountText:a,placeholderTitle:r,paymentStatus:c="pending",gallery:u=[],onPreviewClick:d,onFetchSource:l})=>{const o=y(u),{source:x,isPreviewVisible:f,isLoadingPreview:w,togglePreview:j}=R({isGallery:o,onFetchSource:l,onPreviewClick:d}),m=!f,p=w;return e.jsxs(U,{variant:"dark",children:[e.jsx(C,{onToggle:j,isBusy:p,isPreviewVisible:!m,children:e.jsx(z,{variant:"dark",gallery:u,title:t,showLocked:m,paymentStatus:c,mimeType:s,thumbnailUrl:n,source:x})}),e.jsx(P,{variant:"dark",title:t,placeholderTitle:r,mimeType:s,detail:i,statusBadge:e.jsx(T.PurchaseStatusBadge,{isMine:!0,paymentStatus:c,amountText:a}),icon:o?I("dark"):void 0})]})},X=({title:t,amountText:s,thumbnailUrl:n,mimeType:i="application/octet-stream",detail:a,gallery:r=[],onUnlockClick:c,onFetchSource:u,onDownloadClick:d,paymentStatus:l,isUnlocking:o=!1})=>{const x=y(r),{source:f,cardRef:w,handleUnlockClick:j}=G({paymentStatus:l,onFetchSource:u,onUnlockClick:c}),m=f==null?void 0:f.sourceUrl,p=f==null?void 0:f.redeemUrl,g=x?l!=="paid":m===void 0,N=k.getSourceType(i),D=x?void 0:m;return e.jsxs(U,{variant:"light",rootRef:w,"data-testid":"locked-attachment",children:[e.jsx(z,{variant:"light",gallery:r,title:t,showLocked:g,paymentStatus:l,mimeType:i,thumbnailUrl:n,source:f,containedImage:!g&&N==="document"}),e.jsx(P,{variant:"light",title:t,mimeType:i,detail:a,statusBadge:e.jsx(T.PurchaseStatusBadge,{isMine:!1,paymentStatus:l,amountText:s}),icon:x?I("light"):void 0,action:e.jsx(_,{isLocked:g,isUnlocking:o,sourceUrl:D,redeemUrl:p,onUnlockClicked:j,onDownloadClicked:d})})]})},F=t=>t.isDraft?e.jsx(E,{...t}):t.isPreview?e.jsx($,{...t}):t.isMine?e.jsx(K,{...t}):e.jsx(X,{...t});exports.default=F;
|
|
2
|
-
//# sourceMappingURL=Card-Bs3dV_7R.cjs.map
|