@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.
Files changed (30) hide show
  1. package/dist/Card-Dd-lw8HD.cjs +2 -0
  2. package/dist/Card-Dd-lw8HD.cjs.map +1 -0
  3. package/dist/{Card-Za6WfwFP.js → Card-dFpcKsyN.js} +123 -119
  4. package/dist/Card-dFpcKsyN.js.map +1 -0
  5. package/dist/{index-KfZ2Wx3D.js → index-BvuCjzqJ.js} +958 -936
  6. package/dist/index-BvuCjzqJ.js.map +1 -0
  7. package/dist/index-C1eZGd_f.cjs +2 -0
  8. package/dist/index-C1eZGd_f.cjs.map +1 -0
  9. package/dist/index.cjs +1 -1
  10. package/dist/index.js +1 -1
  11. package/package.json +2 -2
  12. package/src/components/AttachmentCard/Thumbnail.tsx +10 -6
  13. package/src/components/ChannelView.test.tsx +3 -23
  14. package/src/components/ChannelView.tsx +0 -1
  15. package/src/components/CustomMessage/LockedAttachment/components/_shared/GalleryThumbnail.tsx +5 -1
  16. package/src/components/CustomMessage/LockedAttachment/components/_shared/SingleThumbnail.tsx +5 -1
  17. package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +58 -1
  18. package/src/components/CustomMessage/StreamAttachmentMessage.tsx +2 -1
  19. package/src/components/LinkAttachment/components/_shared/CardThumbnail.tsx +4 -2
  20. package/src/components/MediaMessage/index.tsx +2 -1
  21. package/src/components/MessageAttachment/Image/index.tsx +3 -2
  22. package/src/components/MessageAttachment/MessageAttachment.test.tsx +20 -0
  23. package/src/utils/cdnImageUrl.test.ts +37 -0
  24. package/src/utils/cdnImageUrl.ts +34 -0
  25. package/dist/Card-Bs3dV_7R.cjs +0 -2
  26. package/dist/Card-Bs3dV_7R.cjs.map +0 -1
  27. package/dist/Card-Za6WfwFP.js.map +0 -1
  28. package/dist/index-KfZ2Wx3D.js.map +0 -1
  29. package/dist/index-gl5pV2zJ.cjs +0 -2
  30. 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-gl5pV2zJ.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;
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, o as C, p as A, q as k, s as F, t as b, u as P, v as f, w as x, x as y, y as B, z as S, B as q, D as z, E as D, G as E, H as V } from "./index-KfZ2Wx3D.js";
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.2",
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": "^2.2.0",
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={thumbnailUrl}
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
- overrides,
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} />
@@ -523,7 +523,6 @@ const ChannelViewInner: React.FC<{
523
523
  overrides={{
524
524
  Message: MessageOverride,
525
525
  MessageActions: CustomMessageActions,
526
- UnreadMessagesNotification: () => null,
527
526
  UnreadMessagesSeparator: () => null,
528
527
  }}
529
528
  >
@@ -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}
@@ -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={thumbnailUrl}
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
+ }
@@ -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