@linktr.ee/messaging-react 4.4.6-rc-1788235358 → 4.4.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "4.4.6-rc-1788235358",
3
+ "version": "4.4.6",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -50,8 +50,8 @@
50
50
  "chromatic:local": "yarn storybook:build && chromatic"
51
51
  },
52
52
  "dependencies": {
53
- "@linktr.ee/messaging-core": "2.6.0-rc-1788235358",
54
- "@linktr.ee/messaging-taxonomy": "0.11.0-rc-1788235358",
53
+ "@linktr.ee/messaging-core": "^2.6.0",
54
+ "@linktr.ee/messaging-taxonomy": "^0.11.0",
55
55
  "@phosphor-icons/react": "^2.1.10"
56
56
  },
57
57
  "devDependencies": {
@@ -109,6 +109,20 @@ export const LinkClassic = Render([
109
109
  linkAttachment({ image_url: undefined, thumb_url: undefined }),
110
110
  ])
111
111
 
112
+ /** Two link shares stacked in one message. */
113
+ export const MultipleLinks = Render([
114
+ linkAttachment({
115
+ og_scrape_url: 'https://linktr.ee/marketplace',
116
+ title: 'Linktree Marketplace',
117
+ image_url: HERO,
118
+ }),
119
+ linkAttachment({
120
+ og_scrape_url: 'https://linktr.ee/creator',
121
+ title: 'Creator profile',
122
+ image_url: PHOTO('multiple-links'),
123
+ }),
124
+ ])
125
+
112
126
  /**
113
127
  * Link share with an emoji-only caption alongside the URL. The emoji is
114
128
  * real content, so the caption is preserved as a separate bubble beneath
@@ -95,6 +95,33 @@ describe('linkCaptionDuplicatesAttachmentUrl', () => {
95
95
  })
96
96
 
97
97
  describe('StreamAttachmentMessage', () => {
98
+ it('spaces multiple link cards inside the attachment rows wrapper', () => {
99
+ renderWithProviders(
100
+ <StreamAttachmentMessage
101
+ groupPosition="single"
102
+ isMyMessage={true}
103
+ message={message([
104
+ {
105
+ type: 'link',
106
+ og_scrape_url: 'https://linktr.ee/first',
107
+ title: 'First Link',
108
+ image_url: 'https://cdn.example.com/first.jpg',
109
+ },
110
+ {
111
+ type: 'link',
112
+ og_scrape_url: 'https://linktr.ee/second',
113
+ title: 'Second Link',
114
+ image_url: 'https://cdn.example.com/second.jpg',
115
+ },
116
+ ])}
117
+ />
118
+ )
119
+
120
+ const rows = screen.getByTestId('message-attachment-rows')
121
+ expect(rows).toHaveClass('flex', 'flex-col', 'gap-[4px]')
122
+ expect(within(rows).getAllByTestId('link-card')).toHaveLength(2)
123
+ })
124
+
98
125
  it('renders a sent link card and suppresses a URL-only caption', () => {
99
126
  renderWithProviders(
100
127
  <StreamAttachmentMessage
@@ -424,7 +424,15 @@ const StreamAttachmentMessage: React.FC<StreamAttachmentMessageProps> = ({
424
424
  padding: 0,
425
425
  }}
426
426
  >
427
- {rows}
427
+ <div
428
+ data-testid="message-attachment-rows"
429
+ className={classNames(
430
+ 'flex flex-col gap-[4px]',
431
+ isMyMessage ? 'items-end' : 'items-start'
432
+ )}
433
+ >
434
+ {rows}
435
+ </div>
428
436
  {!caption && <MessageErrorIcon />}
429
437
  </div>
430
438
  {caption ? (
@@ -105,7 +105,7 @@ describe('MessageAttachment behavior', () => {
105
105
  )
106
106
  })
107
107
 
108
- it('renders video posters and posterless first-frame previews', () => {
108
+ it('renders video poster and fallback, and wires viewer attributes', () => {
109
109
  const onDismiss = vi.fn()
110
110
  const { rerender } = renderWithProviders(
111
111
  <MessageAttachment.Video.Composer
@@ -122,15 +122,11 @@ describe('MessageAttachment behavior', () => {
122
122
  fireEvent.click(screen.getByRole('button', { name: 'Dismiss attachment' }))
123
123
  expect(onDismiss).toHaveBeenCalledOnce()
124
124
  rerender(
125
- <MessageAttachment.Video.Sent src="https://cdn.example.com/video.mp4" />
125
+ <MessageAttachment.Video.Sent
126
+ items={[{ src: 'https://cdn.example.com/video.mp4' }]}
127
+ />
126
128
  )
127
- expect(screen.getByLabelText('Video 1 thumbnail').tagName).toBe('VIDEO')
128
129
  expect(screen.getByLabelText('Play video 1 of 1')).toBeInTheDocument()
129
-
130
- rerender(
131
- <MessageAttachment.Video.Received src="https://cdn.example.com/received-video.mp4" />
132
- )
133
- expect(screen.getByLabelText('Video 1 thumbnail').tagName).toBe('VIDEO')
134
130
  })
135
131
 
136
132
  it('supports PDF titles, download actions, dismiss, and viewer index', () => {
@@ -42,8 +42,9 @@ export interface VideoAttachmentSharedProps extends MessageAttachmentBaseProps {
42
42
  items?: VideoItem[]
43
43
  /**
44
44
  * `<video preload>` hint forwarded into the viewer. Defaults to
45
- * `'none'` — poster images carry the visual weight on the bubble surface,
46
- * while posterless tiles fetch metadata to paint a first frame. Per-item overrides live on
45
+ * `'none'` — the poster `<img>` carries the visual weight on the
46
+ * bubble surface, and we shouldn't fetch any video bytes until the
47
+ * user actually opens the viewer. Per-item overrides live on
47
48
  * `VideoItem.preload`. The opened `VideoViewer` always preloads
48
49
  * metadata for the active item (so duration / first-frame appear
49
50
  * immediately) regardless of this value.
@@ -69,8 +70,9 @@ export interface VideoAttachmentSharedProps extends MessageAttachmentBaseProps {
69
70
  * recorded it, and only otherwise measured off the decoded poster. Measuring
70
71
  * the poster alone was not enough: an uploaded clip may have no poster, and a
71
72
  * portrait video then sat in whatever `FALLBACK_ASPECT` guessed. Reading the
72
- * dimensions Stream already stores costs nothing at render time; posterless
73
- * tiles fetch metadata to paint a first frame, without using it for sizing.
73
+ * dimensions Stream already stores costs nothing at render time and needs no
74
+ * `loadedmetadata` probe, which would fetch video bytes for every card in the
75
+ * thread against the deliberate `preload: 'none'`.
74
76
  *
75
77
  * `FALLBACK_ASPECT` is **square** on purpose. It covers only the case where we
76
78
  * know nothing — no recorded dimensions, no decoded poster — and a square box
@@ -99,10 +101,6 @@ const PlayBadge: React.FC = () => (
99
101
  </div>
100
102
  )
101
103
 
102
- // Safari needs a time fragment to paint a first frame from a remote MP4.
103
- const withFirstFrameHint = (src: string): string =>
104
- src.includes('#') ? src : `${src}#t=0.001`
105
-
106
104
  // The tile shell owns the cluster's geometry: `MediaStackGrid` gives
107
105
  // each tile the 18px radius on the cluster's OUTER corners only and
108
106
  // clips with `overflow-hidden`, so a poster must carry no radius of
@@ -139,15 +137,6 @@ const PosterTile: React.FC<{
139
137
  onLoad={onLoad}
140
138
  className="absolute inset-0 size-full object-cover"
141
139
  />
142
- ) : item.src ? (
143
- <video
144
- src={withFirstFrameHint(item.src)}
145
- muted
146
- playsInline
147
- preload="metadata"
148
- aria-label={`Video ${index + 1} thumbnail`}
149
- className="absolute inset-0 size-full object-cover"
150
- />
151
140
  ) : (
152
141
  <div className="absolute inset-0 flex items-center justify-center">
153
142
  <VideoCameraIcon