@linktr.ee/messaging-react 3.3.5 → 3.3.6-rc-1780998278

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 (48) hide show
  1. package/dist/{Card-BRRlz4kq.cjs → Card-BkPp2kPW.cjs} +2 -2
  2. package/dist/{Card-BRRlz4kq.cjs.map → Card-BkPp2kPW.cjs.map} +1 -1
  3. package/dist/{Card-C-FCwjGa.cjs → Card-Cc2fNLq9.cjs} +2 -2
  4. package/dist/{Card-C-FCwjGa.cjs.map → Card-Cc2fNLq9.cjs.map} +1 -1
  5. package/dist/{Card-DzjYyrie.js → Card-DcPqOyvb.js} +2 -2
  6. package/dist/{Card-DzjYyrie.js.map → Card-DcPqOyvb.js.map} +1 -1
  7. package/dist/{Card-CVZzYmYW.js → Card-DpFh-e_A.js} +2 -2
  8. package/dist/{Card-CVZzYmYW.js.map → Card-DpFh-e_A.js.map} +1 -1
  9. package/dist/{Card-B9QrjooN.js → Card-IwtTtqVc.js} +3 -3
  10. package/dist/{Card-B9QrjooN.js.map → Card-IwtTtqVc.js.map} +1 -1
  11. package/dist/{Card-D_oLlfPw.cjs → Card-m50_YYfj.cjs} +2 -2
  12. package/dist/{Card-D_oLlfPw.cjs.map → Card-m50_YYfj.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-Cth1yWnH.cjs → LockedThumbnail-B6PfPOy8.cjs} +2 -2
  14. package/dist/{LockedThumbnail-Cth1yWnH.cjs.map → LockedThumbnail-B6PfPOy8.cjs.map} +1 -1
  15. package/dist/{LockedThumbnail-CJfXY_Ut.js → LockedThumbnail-ChZe5UxN.js} +2 -2
  16. package/dist/{LockedThumbnail-CJfXY_Ut.js.map → LockedThumbnail-ChZe5UxN.js.map} +1 -1
  17. package/dist/assets/index.css +1 -1
  18. package/dist/{index-D7eRkXoG.js → index-CaGU074D.js} +585 -551
  19. package/dist/index-CaGU074D.js.map +1 -0
  20. package/dist/index-nVyueEvT.cjs +2 -0
  21. package/dist/index-nVyueEvT.cjs.map +1 -0
  22. package/dist/index.cjs +1 -1
  23. package/dist/index.js +1 -1
  24. package/package.json +5 -2
  25. package/src/components/ActionButton/ActionButton.test.tsx +0 -25
  26. package/src/components/AttachmentCard/AttachmentCard.stories.tsx +122 -0
  27. package/src/components/Avatar/Avatar.stories.tsx +20 -0
  28. package/src/components/ChannelList/ChannelList.stories.tsx +5 -0
  29. package/src/components/ChannelList/CustomChannelPreview.stories.tsx +22 -0
  30. package/src/components/ChannelView.test.tsx +12 -1
  31. package/src/components/ChannelView.tsx +34 -17
  32. package/src/components/CustomMessage/CustomMessage.stories.tsx +0 -16
  33. package/src/components/CustomMessage/MessageAttachmentConversations.stories.tsx +13 -0
  34. package/src/components/CustomMessage/MessageTag.stories.tsx +18 -0
  35. package/src/components/CustomMessage/MessageVoteButtons.stories.tsx +9 -0
  36. package/src/components/CustomMessageInput/index.tsx +14 -4
  37. package/src/components/CustomSystemMessage/CustomSystemMessage.stories.tsx +54 -0
  38. package/src/components/CustomTypingIndicator/CustomTypingIndicator.stories.tsx +7 -0
  39. package/src/components/LinkAttachment/LinkAttachment.stories.tsx +11 -1
  40. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +4 -0
  41. package/src/components/MediaMessage/MediaMessage.test.tsx +0 -38
  42. package/src/components/MessageAttachment/MessageAttachment.test.tsx +25 -84
  43. package/src/components/SearchInput/SearchInput.test.tsx +0 -8
  44. package/src/styles.css +10 -0
  45. package/src/utils/formatRelativeTime.test.ts +1 -32
  46. package/dist/index-CBtOPvxW.cjs +0 -2
  47. package/dist/index-CBtOPvxW.cjs.map +0 -1
  48. package/dist/index-D7eRkXoG.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-CBtOPvxW.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.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.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;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-nVyueEvT.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.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.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;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 n, d as o, e as m, F as g, f as l, L as r, h as M, M as u, i as L, j as c, k as h, l as d, m as p, n as v, o as A, p as C, q as F, s as k, t as b, u as f, v as x, w as y, x as P, y as S, z as q, B as z, D as B } from "./index-D7eRkXoG.js";
1
+ import { a as e, b as t, C as i, c as n, d as o, e as m, F as g, f as l, L as r, h as M, M as u, i as L, j as c, k as h, l as d, m as p, n as v, o as A, p as C, q as F, s as k, t as b, u as f, v as x, w as y, x as P, y as S, z as q, B as z, D as B } from "./index-CaGU074D.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.3.5",
3
+ "version": "3.3.6-rc-1780998278",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -32,7 +32,9 @@
32
32
  "lint:fix": "npx --no-install eslint . --ext .ts,.tsx --fix",
33
33
  "verify": "yarn type-check && yarn lint && yarn test:ci && yarn build",
34
34
  "storybook": "storybook dev -p 6006",
35
- "storybook:build": "storybook build"
35
+ "storybook:build": "storybook build --stats-json",
36
+ "chromatic:ci:feature": "chromatic --ci --exit-zero-on-changes --exit-once-uploaded",
37
+ "chromatic:ci:main": "chromatic --ci --auto-accept-changes"
36
38
  },
37
39
  "dependencies": {
38
40
  "@linktr.ee/component-library": "11.8.6",
@@ -56,6 +58,7 @@
56
58
  "@vitejs/plugin-react": "^4.2.1",
57
59
  "@vitest/ui": "^1.0.4",
58
60
  "autoprefixer": "^10.4.20",
61
+ "chromatic": "^11.20.0",
59
62
  "classnames": "^2.3.2",
60
63
  "jsdom": "^23.0.1",
61
64
  "postcss": "^8.4.49",
@@ -10,24 +10,6 @@ describe('ActionButton', () => {
10
10
  renderWithProviders(<ActionButton>Click me</ActionButton>);
11
11
  expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
12
12
  });
13
-
14
- it('renders with default variant', () => {
15
- renderWithProviders(<ActionButton>Default</ActionButton>);
16
- const button = screen.getByRole('button');
17
- expect(button).toHaveClass('text-charcoal');
18
- });
19
-
20
- it('renders with danger variant', () => {
21
- renderWithProviders(<ActionButton variant="danger">Delete</ActionButton>);
22
- const button = screen.getByRole('button');
23
- expect(button).toHaveClass('text-danger');
24
- });
25
-
26
- it('has full width by default', () => {
27
- renderWithProviders(<ActionButton>Full Width</ActionButton>);
28
- const button = screen.getByRole('button');
29
- expect(button).toHaveClass('w-full');
30
- });
31
13
  });
32
14
 
33
15
  describe('Disabled State', () => {
@@ -52,13 +34,6 @@ describe('ActionButton', () => {
52
34
 
53
35
  expect(handleClick).not.toHaveBeenCalled();
54
36
  });
55
-
56
- it('applies disabled styles', () => {
57
- renderWithProviders(<ActionButton disabled>Disabled</ActionButton>);
58
- const button = screen.getByRole('button');
59
- expect(button).toHaveClass('disabled:opacity-60');
60
- expect(button).toHaveClass('disabled:cursor-not-allowed');
61
- });
62
37
  });
63
38
 
64
39
  describe('Click Handling', () => {
@@ -19,6 +19,10 @@ const IMAGE_URL = '/image-thumbnail.jpg'
19
19
 
20
20
  const Template: StoryFn<ComponentProps> = (args) => <AttachmentCard {...args} />
21
21
 
22
+ // Per-variant stories stay in Storybook for browsing/design review but are
23
+ // individually covered by cells in AllVariants — skip in Chromatic.
24
+ const skipInChromatic = { chromatic: { disableSnapshot: true } }
25
+
22
26
  export const LightImage: StoryFn<ComponentProps> = Template.bind({})
23
27
  LightImage.args = {
24
28
  variant: 'light',
@@ -34,6 +38,7 @@ LightImage.args = {
34
38
  />
35
39
  ),
36
40
  }
41
+ LightImage.parameters = skipInChromatic
37
42
 
38
43
  export const DarkImage: StoryFn<ComponentProps> = Template.bind({})
39
44
  DarkImage.args = {
@@ -50,6 +55,7 @@ DarkImage.args = {
50
55
  />
51
56
  ),
52
57
  }
58
+ DarkImage.parameters = skipInChromatic
53
59
 
54
60
  export const PdfPlaceholder: StoryFn<ComponentProps> = Template.bind({})
55
61
  PdfPlaceholder.args = {
@@ -59,6 +65,7 @@ PdfPlaceholder.args = {
59
65
  detail: '120 KB',
60
66
  thumbnail: <AttachmentThumbnail mimeType="application/pdf" variant="light" />,
61
67
  }
68
+ PdfPlaceholder.parameters = skipInChromatic
62
69
 
63
70
  export const AudioPosterOnly: StoryFn<ComponentProps> = Template.bind({})
64
71
  AudioPosterOnly.args = {
@@ -75,6 +82,7 @@ AudioPosterOnly.args = {
75
82
  />
76
83
  ),
77
84
  }
85
+ AudioPosterOnly.parameters = skipInChromatic
78
86
 
79
87
  export const UntitledDark: StoryFn<ComponentProps> = Template.bind({})
80
88
  UntitledDark.args = {
@@ -83,6 +91,7 @@ UntitledDark.args = {
83
91
  detail: '1.1 MB',
84
92
  thumbnail: <AttachmentThumbnail mimeType="image/png" variant="dark" />,
85
93
  }
94
+ UntitledDark.parameters = skipInChromatic
86
95
 
87
96
  export const WithTopBadges: StoryFn<ComponentProps> = Template.bind({})
88
97
  WithTopBadges.args = {
@@ -102,3 +111,116 @@ WithTopBadges.args = {
102
111
  </span>
103
112
  ),
104
113
  }
114
+ WithTopBadges.parameters = skipInChromatic
115
+
116
+ export const AllVariants: StoryFn = () => {
117
+ const cards: { label: string; props: ComponentProps }[] = [
118
+ {
119
+ label: 'Light image',
120
+ props: {
121
+ variant: 'light',
122
+ title: 'sunset.jpg',
123
+ mimeType: 'image/jpeg',
124
+ detail: '2.4 MB',
125
+ thumbnail: (
126
+ <AttachmentThumbnail
127
+ mimeType="image/jpeg"
128
+ sourceUrl={IMAGE_URL}
129
+ title="sunset"
130
+ variant="light"
131
+ />
132
+ ),
133
+ },
134
+ },
135
+ {
136
+ label: 'Dark image',
137
+ props: {
138
+ variant: 'dark',
139
+ title: 'sunset.jpg',
140
+ mimeType: 'image/jpeg',
141
+ detail: '2.4 MB',
142
+ thumbnail: (
143
+ <AttachmentThumbnail
144
+ mimeType="image/jpeg"
145
+ sourceUrl={IMAGE_URL}
146
+ title="sunset"
147
+ variant="dark"
148
+ />
149
+ ),
150
+ },
151
+ },
152
+ {
153
+ label: 'PDF placeholder',
154
+ props: {
155
+ variant: 'light',
156
+ title: 'invoice.pdf',
157
+ mimeType: 'application/pdf',
158
+ detail: '120 KB',
159
+ thumbnail: (
160
+ <AttachmentThumbnail mimeType="application/pdf" variant="light" />
161
+ ),
162
+ },
163
+ },
164
+ {
165
+ label: 'Audio poster',
166
+ props: {
167
+ variant: 'dark',
168
+ title: 'voice-memo.m4a',
169
+ mimeType: 'audio/m4a',
170
+ detail: '0:42',
171
+ thumbnail: (
172
+ <AttachmentThumbnail
173
+ mimeType="audio/m4a"
174
+ thumbnailUrl={IMAGE_URL}
175
+ title="voice memo"
176
+ variant="dark"
177
+ />
178
+ ),
179
+ },
180
+ },
181
+ {
182
+ label: 'Untitled (dark)',
183
+ props: {
184
+ variant: 'dark',
185
+ mimeType: 'image/png',
186
+ detail: '1.1 MB',
187
+ thumbnail: (
188
+ <AttachmentThumbnail mimeType="image/png" variant="dark" />
189
+ ),
190
+ },
191
+ },
192
+ {
193
+ label: 'With top badges',
194
+ props: {
195
+ variant: 'light',
196
+ title: 'receipt.pdf',
197
+ mimeType: 'application/pdf',
198
+ detail: 'Paid',
199
+ thumbnail: (
200
+ <AttachmentThumbnail mimeType="application/pdf" variant="light" />
201
+ ),
202
+ topLeft: (
203
+ <span className="rounded-full bg-black/80 px-2 py-0.5 text-xs font-medium text-white">
204
+ Locked
205
+ </span>
206
+ ),
207
+ topRight: (
208
+ <span className="rounded-full bg-white/90 px-2 py-0.5 text-xs font-medium text-black">
209
+ $5.00
210
+ </span>
211
+ ),
212
+ },
213
+ },
214
+ ]
215
+
216
+ return (
217
+ <div className="grid grid-cols-2 gap-6 p-6">
218
+ {cards.map(({ label, props }) => (
219
+ <div key={label} className="flex flex-col gap-2">
220
+ <span className="text-xs text-stone">{label}</span>
221
+ <AttachmentCard {...props} />
222
+ </div>
223
+ ))}
224
+ </div>
225
+ )
226
+ }
@@ -29,6 +29,11 @@ Default.args = {
29
29
  image: 'https://i.pravatar.cc/150?img=1',
30
30
  }
31
31
 
32
+ // Per-size stories (SmallSize…ExtraLargeSize) are each a single cell of the
33
+ // VariousSizes grid; AIActive is the size=40 cell of AIActiveVariousSizes.
34
+ // Keep them in Storybook for browsing but skip in Chromatic.
35
+ const skipInChromatic = { chromatic: { disableSnapshot: true } }
36
+
32
37
  export const AIActive: StoryFn<ComponentProps> = Template.bind({})
33
38
  AIActive.args = {
34
39
  id: 'ai-agent',
@@ -36,6 +41,7 @@ AIActive.args = {
36
41
  image: 'https://i.pravatar.cc/150?img=12',
37
42
  dmAgentEnabled: true,
38
43
  }
44
+ AIActive.parameters = skipInChromatic
39
45
 
40
46
  export const WithoutImage: StoryFn<ComponentProps> = Template.bind({})
41
47
  WithoutImage.args = {
@@ -56,6 +62,7 @@ SmallSize.args = {
56
62
  name: 'Carol Williams',
57
63
  size: 20,
58
64
  }
65
+ SmallSize.parameters = skipInChromatic
59
66
 
60
67
  export const MediumSize: StoryFn<ComponentProps> = Template.bind({})
61
68
  MediumSize.args = {
@@ -63,6 +70,7 @@ MediumSize.args = {
63
70
  name: 'David Brown',
64
71
  size: 32,
65
72
  }
73
+ MediumSize.parameters = skipInChromatic
66
74
 
67
75
  export const DefaultSize: StoryFn<ComponentProps> = Template.bind({})
68
76
  DefaultSize.args = {
@@ -70,6 +78,7 @@ DefaultSize.args = {
70
78
  name: 'Emma Davis',
71
79
  size: 40,
72
80
  }
81
+ DefaultSize.parameters = skipInChromatic
73
82
 
74
83
  export const LargeSize: StoryFn<ComponentProps> = Template.bind({})
75
84
  LargeSize.args = {
@@ -77,6 +86,7 @@ LargeSize.args = {
77
86
  name: 'Frank Miller',
78
87
  size: 56,
79
88
  }
89
+ LargeSize.parameters = skipInChromatic
80
90
 
81
91
  export const ExtraLargeSize: StoryFn<ComponentProps> = Template.bind({})
82
92
  ExtraLargeSize.args = {
@@ -84,12 +94,17 @@ ExtraLargeSize.args = {
84
94
  name: 'Grace Lee',
85
95
  size: 80,
86
96
  }
97
+ ExtraLargeSize.parameters = skipInChromatic
87
98
 
99
+ // Avatars don't render the participant name in the bubble — only the initial.
100
+ // LongName stays as a Storybook fixture for documentation but adds no visual
101
+ // regression value beyond what Default + WithoutImage already cover.
88
102
  export const LongName: StoryFn<ComponentProps> = Template.bind({})
89
103
  LongName.args = {
90
104
  id: 'user-8',
91
105
  name: 'Alexander Christopher Wellington-Montgomery III',
92
106
  }
107
+ LongName.parameters = { chromatic: { disableSnapshot: true } }
93
108
 
94
109
  export const DifferentColors: StoryFn = () => {
95
110
  const users = [
@@ -117,6 +132,10 @@ export const DifferentColors: StoryFn = () => {
117
132
  )
118
133
  }
119
134
 
135
+ // Showcase grid that crosses image-present/absent with different users.
136
+ // Each individual case is already covered by `Default` (image) and
137
+ // `WithoutImage` (initial) — skip the showcase in Chromatic so a single
138
+ // pravatar avatar flake doesn't diff the whole grid.
120
139
  export const MixedAvatars: StoryFn = () => {
121
140
  const users = [
122
141
  { id: 'user-1', name: 'Alice Anderson', image: 'https://i.pravatar.cc/150?img=1' },
@@ -140,6 +159,7 @@ export const MixedAvatars: StoryFn = () => {
140
159
  </div>
141
160
  )
142
161
  }
162
+ MixedAvatars.parameters = { chromatic: { disableSnapshot: true } }
143
163
 
144
164
  export const VariousSizes: StoryFn = () => {
145
165
  const sizes = [20, 32, 40, 56, 80]
@@ -8,11 +8,16 @@ import { ChannelList } from '.'
8
8
 
9
9
  type ComponentProps = React.ComponentProps<typeof ChannelList>
10
10
 
11
+ // ChannelList renders a list of mocked channels driven by MockChatProvider —
12
+ // useful as a browsable Storybook fixture but the snapshot reflects the mock
13
+ // fixture rather than any reusable component contract. Skip the whole story
14
+ // file in Chromatic; per-channel-row visuals are covered by CustomChannelPreview.
11
15
  const meta: Meta<ComponentProps> = {
12
16
  title: 'ChannelList',
13
17
  component: ChannelList,
14
18
  parameters: {
15
19
  layout: 'fullscreen',
20
+ chromatic: { disableSnapshot: true },
16
21
  },
17
22
  decorators: [
18
23
  (Story) => (
@@ -483,6 +483,13 @@ WithVeryLongUrl.args = {
483
483
  }
484
484
 
485
485
  // Time-based stories
486
+ //
487
+ // Each story below covers exactly one row of the `formatRelativeTime`
488
+ // output matrix. The `TimeVariations` story further down already snapshots
489
+ // all of them together in a labeled table — kept here for Storybook
490
+ // browsing but skipped in Chromatic to avoid 9 redundant snapshots.
491
+ const skipInChromatic = { chromatic: { disableSnapshot: true } }
492
+
486
493
  export const JustNow: StoryFn<ComponentProps> = Template.bind({})
487
494
  JustNow.args = {
488
495
  channel: createMockChannel({
@@ -494,6 +501,7 @@ JustNow.args = {
494
501
  lastMessageTime: secondsAgo(30),
495
502
  }),
496
503
  }
504
+ JustNow.parameters = skipInChromatic
497
505
 
498
506
  export const FiveMinutesAgo: StoryFn<ComponentProps> = Template.bind({})
499
507
  FiveMinutesAgo.args = {
@@ -506,6 +514,7 @@ FiveMinutesAgo.args = {
506
514
  lastMessageTime: minutesAgo(5),
507
515
  }),
508
516
  }
517
+ FiveMinutesAgo.parameters = skipInChromatic
509
518
 
510
519
  export const ThreeHoursAgo: StoryFn<ComponentProps> = Template.bind({})
511
520
  ThreeHoursAgo.args = {
@@ -518,6 +527,7 @@ ThreeHoursAgo.args = {
518
527
  lastMessageTime: hoursAgo(3),
519
528
  }),
520
529
  }
530
+ ThreeHoursAgo.parameters = skipInChromatic
521
531
 
522
532
  export const Yesterday: StoryFn<ComponentProps> = Template.bind({})
523
533
  Yesterday.args = {
@@ -530,6 +540,7 @@ Yesterday.args = {
530
540
  lastMessageTime: hoursAgo(30),
531
541
  }),
532
542
  }
543
+ Yesterday.parameters = skipInChromatic
533
544
 
534
545
  export const ThreeDaysAgo: StoryFn<ComponentProps> = Template.bind({})
535
546
  ThreeDaysAgo.args = {
@@ -542,6 +553,7 @@ ThreeDaysAgo.args = {
542
553
  lastMessageTime: daysAgo(3),
543
554
  }),
544
555
  }
556
+ ThreeDaysAgo.parameters = skipInChromatic
545
557
 
546
558
  export const LastWeek: StoryFn<ComponentProps> = Template.bind({})
547
559
  LastWeek.args = {
@@ -554,6 +566,7 @@ LastWeek.args = {
554
566
  lastMessageTime: daysAgo(7),
555
567
  }),
556
568
  }
569
+ LastWeek.parameters = skipInChromatic
557
570
 
558
571
  export const TwoWeeksAgo: StoryFn<ComponentProps> = Template.bind({})
559
572
  TwoWeeksAgo.args = {
@@ -566,6 +579,7 @@ TwoWeeksAgo.args = {
566
579
  lastMessageTime: daysAgo(14),
567
580
  }),
568
581
  }
582
+ TwoWeeksAgo.parameters = skipInChromatic
569
583
 
570
584
  export const OneMonthAgo: StoryFn<ComponentProps> = Template.bind({})
571
585
  OneMonthAgo.args = {
@@ -578,6 +592,7 @@ OneMonthAgo.args = {
578
592
  lastMessageTime: daysAgo(30),
579
593
  }),
580
594
  }
595
+ OneMonthAgo.parameters = skipInChromatic
581
596
 
582
597
  export const SixMonthsAgo: StoryFn<ComponentProps> = Template.bind({})
583
598
  SixMonthsAgo.args = {
@@ -590,6 +605,7 @@ SixMonthsAgo.args = {
590
605
  lastMessageTime: daysAgo(180),
591
606
  }),
592
607
  }
608
+ SixMonthsAgo.parameters = skipInChromatic
593
609
 
594
610
  export const TimeVariations: StoryFn = () => {
595
611
  const [selectedChannelId, _setSelectedChannelId] = React.useState<
@@ -723,6 +739,10 @@ WithAttachmentImageUrl.args = {
723
739
  ],
724
740
  }),
725
741
  }
742
+ // Same fallback render as WithAttachmentAssetUrl ("📎 Sent an attachment") —
743
+ // kept as a separate story to document the field, skipped in Chromatic to
744
+ // avoid a redundant snapshot.
745
+ WithAttachmentImageUrl.parameters = skipInChromatic
726
746
 
727
747
  export const WithAttachmentOgScrapeUrl: StoryFn<ComponentProps> = Template.bind(
728
748
  {}
@@ -759,6 +779,7 @@ WithAttachmentThumbUrl.args = {
759
779
  ],
760
780
  }),
761
781
  }
782
+ WithAttachmentThumbUrl.parameters = skipInChromatic
762
783
 
763
784
  export const WithLongAttachmentUrl: StoryFn<ComponentProps> = Template.bind({})
764
785
  WithLongAttachmentUrl.args = {
@@ -777,3 +798,4 @@ WithLongAttachmentUrl.args = {
777
798
  ],
778
799
  }),
779
800
  }
801
+ WithLongAttachmentUrl.parameters = skipInChromatic
@@ -22,7 +22,18 @@ vi.mock('stream-chat-react', () => ({
22
22
  Window: ({ children }: { children: React.ReactNode }) => (
23
23
  <div data-testid="window">{children}</div>
24
24
  ),
25
- MessageList: () => <div data-testid="message-list" />,
25
+ MessageList: ({
26
+ renderMessages,
27
+ }: {
28
+ renderMessages?: (options: Record<string, unknown>) => React.ReactNode[]
29
+ }) => (
30
+ <ul data-testid="message-list">
31
+ {renderMessages?.({ messages: [] }) ?? null}
32
+ </ul>
33
+ ),
34
+ defaultRenderMessages: () => [
35
+ <li key="message-list-item" data-testid="message-list-item" />,
36
+ ],
26
37
  WithComponents: ({ children }: { children: React.ReactNode }) => (
27
38
  <>{children}</>
28
39
  ),
@@ -10,6 +10,7 @@ import {
10
10
  useChannelStateContext,
11
11
  WithComponents,
12
12
  MessageUIComponentProps,
13
+ defaultRenderMessages,
13
14
  } from 'stream-chat-react'
14
15
 
15
16
  import { useChannelStar } from '../hooks/useChannelStar'
@@ -399,25 +400,41 @@ const ChannelViewInner: React.FC<{
399
400
  key="lt-channel-message-list"
400
401
  className="flex-1 overflow-hidden relative"
401
402
  >
402
- <MessageList hideDeletedMessages hideNewMessageSeparator={false} />
403
- </div>
403
+ <MessageList
404
+ hideDeletedMessages
405
+ hideNewMessageSeparator={false}
406
+ renderMessages={(options) => {
407
+ const elements = defaultRenderMessages(options)
404
408
 
405
- {renderConversationFooter ? (
406
- <React.Fragment key="lt-channel-conversation-footer">
407
- {renderConversationFooter(channel)}
408
- </React.Fragment>
409
- ) : null}
409
+ if (renderConversationFooter) {
410
+ elements.push(
411
+ <li key="lt-channel-conversation-footer">
412
+ {renderConversationFooter(channel)}
413
+ </li>
414
+ )
415
+ }
410
416
 
411
- {/* Message Input */}
412
- <CustomMessageInput
413
- key="lt-channel-message-input"
414
- {...(renderMessageInputActions && {
415
- renderActions: () => renderMessageInputActions?.(channel),
416
- })}
417
- renderFooter={() => renderMessageInputFooter?.(channel)}
418
- disabled={composerDisabled}
419
- disabledReason={composerDisabledReason}
420
- />
417
+ elements.push(
418
+ // Message Input
419
+ <li
420
+ key="lt-channel-message-input"
421
+ className="sticky bottom-0 mt-auto flex p-4 pt-1 inset-x-0 mx-[calc(-1*var(--str-chat\_\_spacing-2))] md:mx-[calc(-1*min(var(--str-chat\_\_spacing-10),4%))]"
422
+ >
423
+ <div className="absolute bottom-0 inset-x-0 w-full h-4/5 backdrop-blur-[16px] [mask-image:linear-gradient(to_top,black_0%,transparent_100%)] [-webkit-mask-image:linear-gradient(to_top,black_0%,transparent_100%)]" />
424
+ <CustomMessageInput
425
+ className="isolate w-full"
426
+ renderActions={() => renderMessageInputActions?.(channel)}
427
+ renderFooter={() => renderMessageInputFooter?.(channel)}
428
+ disabled={composerDisabled}
429
+ disabledReason={composerDisabledReason}
430
+ />
431
+ </li>
432
+ )
433
+
434
+ return elements
435
+ }}
436
+ />
437
+ </div>
421
438
  </Window>
422
439
  </WithComponents>
423
440
  </>
@@ -1,7 +1,6 @@
1
1
  import '../../stream-custom-data'
2
2
 
3
3
  import type { Meta, StoryFn } from '@storybook/react'
4
- import { expect, userEvent, within } from '@storybook/test'
5
4
  import React, { useEffect } from 'react'
6
5
  import {
7
6
  Channel as ChannelType,
@@ -568,21 +567,6 @@ ConversationWithPaidAttachments.args = {
568
567
  },
569
568
  ],
570
569
  }
571
- ConversationWithPaidAttachments.play = async ({ canvasElement }) => {
572
- const canvas = within(canvasElement)
573
- const attachmentMsg = canvasElement.querySelector('[data-message-id="msg-3"]')
574
- await userEvent.hover(attachmentMsg!)
575
- const toggle = await canvas.findByTestId('message-actions-toggle-button')
576
- await userEvent.click(toggle)
577
- const deleteButton = await canvas.findByRole('button', { name: 'Delete' })
578
- await userEvent.click(deleteButton)
579
- await expect(
580
- canvas.getByRole('heading', { name: 'Delete attachment?' })
581
- ).toBeInTheDocument()
582
- await expect(
583
- canvas.getByText(/Deleting it will remove access for the buyer/)
584
- ).toBeInTheDocument()
585
- }
586
570
  ConversationWithPaidAttachments.parameters = {
587
571
  docs: {
588
572
  description: {
@@ -223,6 +223,14 @@ interface ConversationStoryArgs {
223
223
  currentUser: StoryUser
224
224
  }
225
225
 
226
+ // Single-attachment-type conversations (`ConversationWithImages`,
227
+ // `ConversationWithVideos`, etc.) are useful for design review but the
228
+ // per-attachment-type RENDERING is already covered by the dedicated
229
+ // `MessageAttachment/{Image,Video,Audio,Pdf,File}` stories. The
230
+ // "mixed" conversations below exercise composition that the per-type
231
+ // stories don't, so those keep their snapshots.
232
+ const skipInChromatic = { chromatic: { disableSnapshot: true } }
233
+
226
234
  const HERO_PHOTO = 'https://picsum.photos/seed/portrait/720/720'
227
235
  const STUDIO_PHOTOS = [
228
236
  { src: 'https://picsum.photos/seed/studio-1/720/720', alt: 'Studio shot 1' },
@@ -291,6 +299,7 @@ export const ConversationWithImages: StoryFn<ConversationStoryArgs> = ({
291
299
  />
292
300
  )
293
301
  }
302
+ ConversationWithImages.parameters = skipInChromatic
294
303
 
295
304
  /**
296
305
  * Video attachments — receiver sends a single clip with a caption,
@@ -355,6 +364,7 @@ export const ConversationWithVideos: StoryFn<ConversationStoryArgs> = ({
355
364
  />
356
365
  )
357
366
  }
367
+ ConversationWithVideos.parameters = skipInChromatic
358
368
 
359
369
  /**
360
370
  * Audio attachments — voice memo back-and-forth, plus a stacked
@@ -426,6 +436,7 @@ export const ConversationWithAudio: StoryFn<ConversationStoryArgs> = ({
426
436
  />
427
437
  )
428
438
  }
439
+ ConversationWithAudio.parameters = skipInChromatic
429
440
 
430
441
  /**
431
442
  * PDF attachments — single document with a caption and a stacked
@@ -498,6 +509,7 @@ export const ConversationWithPdfs: StoryFn<ConversationStoryArgs> = ({
498
509
  />
499
510
  )
500
511
  }
512
+ ConversationWithPdfs.parameters = skipInChromatic
501
513
 
502
514
  /**
503
515
  * Generic file attachments — non-PDF documents with a download
@@ -583,6 +595,7 @@ export const ConversationWithFiles: StoryFn<ConversationStoryArgs> = ({
583
595
  />
584
596
  )
585
597
  }
598
+ ConversationWithFiles.parameters = skipInChromatic
586
599
 
587
600
  /**
588
601
  * Mixed conversation — a realistic chat where the same thread carries