@linktr.ee/messaging-react 3.3.4 → 3.3.6-rc-1780987607

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 (56) hide show
  1. package/dist/{Card-DLmUSU4A.cjs → Card-BlviN8Fb.cjs} +2 -2
  2. package/dist/{Card-DLmUSU4A.cjs.map → Card-BlviN8Fb.cjs.map} +1 -1
  3. package/dist/{Card-DmPpcrSU.js → Card-C4ncqjxJ.js} +2 -2
  4. package/dist/{Card-DmPpcrSU.js.map → Card-C4ncqjxJ.js.map} +1 -1
  5. package/dist/{Card-0BgubwgM.cjs → Card-Cn7Zxc6U.cjs} +2 -2
  6. package/dist/{Card-0BgubwgM.cjs.map → Card-Cn7Zxc6U.cjs.map} +1 -1
  7. package/dist/{Card-DchJqvYq.js → Card-DE5bfj0l.js} +2 -2
  8. package/dist/{Card-DchJqvYq.js.map → Card-DE5bfj0l.js.map} +1 -1
  9. package/dist/{Card-B7AF5uOB.js → Card-IjOI7UXs.js} +3 -3
  10. package/dist/{Card-B7AF5uOB.js.map → Card-IjOI7UXs.js.map} +1 -1
  11. package/dist/{Card-CvBbAoUo.cjs → Card-KgQxeR-B.cjs} +2 -2
  12. package/dist/{Card-CvBbAoUo.cjs.map → Card-KgQxeR-B.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-BQjA4HaB.js → LockedThumbnail-4-54cyJG.js} +2 -2
  14. package/dist/{LockedThumbnail-BQjA4HaB.js.map → LockedThumbnail-4-54cyJG.js.map} +1 -1
  15. package/dist/{LockedThumbnail-D9fSb4N-.cjs → LockedThumbnail-DL5NZzWJ.cjs} +2 -2
  16. package/dist/{LockedThumbnail-D9fSb4N-.cjs.map → LockedThumbnail-DL5NZzWJ.cjs.map} +1 -1
  17. package/dist/{index-BcHUpyyw.js → index-C2wfgpUU.js} +855 -823
  18. package/dist/index-C2wfgpUU.js.map +1 -0
  19. package/dist/index-nanry0Io.cjs +2 -0
  20. package/dist/index-nanry0Io.cjs.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.js +1 -1
  23. package/package.json +5 -2
  24. package/src/components/ActionButton/ActionButton.test.tsx +0 -25
  25. package/src/components/AttachmentCard/AttachmentCard.stories.tsx +226 -0
  26. package/src/components/Avatar/Avatar.stories.tsx +20 -0
  27. package/src/components/ChannelActionsMenu/ChannelActionsMenu.test.tsx +33 -8
  28. package/src/components/ChannelList/ChannelList.stories.tsx +5 -0
  29. package/src/components/ChannelList/CustomChannelPreview.stories.tsx +77 -47
  30. package/src/components/ChannelView.stories.tsx +8 -7
  31. package/src/components/ChannelView.test.tsx +12 -1
  32. package/src/components/ChannelView.tsx +34 -17
  33. package/src/components/CloseButton/CloseButton.stories.tsx +31 -0
  34. package/src/components/CustomDateSeparator/CustomDateSeparator.stories.tsx +33 -0
  35. package/src/components/CustomLinkPreviewList/CustomLinkPreviewCard.stories.tsx +63 -0
  36. package/src/components/CustomLinkPreviewList/CustomLinkPreviewCard.tsx +57 -0
  37. package/src/components/CustomLinkPreviewList/index.tsx +2 -54
  38. package/src/components/CustomMessage/CustomMessage.stories.tsx +3 -18
  39. package/src/components/CustomMessage/MessageAttachmentConversations.stories.tsx +13 -0
  40. package/src/components/CustomMessage/MessageTag.stories.tsx +22 -2
  41. package/src/components/CustomMessage/MessageVoteButtons.stories.tsx +9 -0
  42. package/src/components/CustomMessageInput/index.tsx +14 -4
  43. package/src/components/CustomSystemMessage/CustomSystemMessage.stories.tsx +54 -0
  44. package/src/components/CustomTypingIndicator/CustomTypingIndicator.stories.tsx +7 -0
  45. package/src/components/LinkAttachment/LinkAttachment.stories.tsx +11 -1
  46. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +4 -0
  47. package/src/components/MediaMessage/MediaMessage.stories.tsx +4 -2
  48. package/src/components/MediaMessage/MediaMessage.test.tsx +0 -38
  49. package/src/components/MessageAttachment/MessageAttachment.test.tsx +25 -84
  50. package/src/components/SearchInput/SearchInput.test.tsx +0 -8
  51. package/src/hooks/useChannelModerationActions.ts +32 -14
  52. package/src/stories/decorators/storyTime.ts +31 -0
  53. package/src/utils/formatRelativeTime.test.ts +1 -32
  54. package/dist/index-BcHUpyyw.js.map +0 -1
  55. package/dist/index-DTZNltUC.cjs +0 -2
  56. package/dist/index-DTZNltUC.cjs.map +0 -1
@@ -0,0 +1,63 @@
1
+ import type { Meta, StoryFn } from '@storybook/react'
2
+ import React from 'react'
3
+ import { LinkPreview, LinkPreviewStatus } from 'stream-chat'
4
+
5
+ import CustomLinkPreviewCard from './CustomLinkPreviewCard'
6
+
7
+ type ComponentProps = React.ComponentProps<typeof CustomLinkPreviewCard>
8
+
9
+ const meta: Meta<ComponentProps> = {
10
+ title: 'CustomLinkPreviewCard',
11
+ component: CustomLinkPreviewCard,
12
+ parameters: { layout: 'centered' },
13
+ argTypes: {
14
+ onDismiss: { action: 'dismissed' },
15
+ },
16
+ }
17
+ export default meta
18
+
19
+ const makePreview = (overrides: Partial<LinkPreview>): LinkPreview => ({
20
+ og_scrape_url: 'https://example.com',
21
+ status: LinkPreviewStatus.LOADED,
22
+ ...overrides,
23
+ })
24
+
25
+ const Template: StoryFn<ComponentProps> = (args) => (
26
+ <div className="p-12">
27
+ <CustomLinkPreviewCard {...args} />
28
+ </div>
29
+ )
30
+
31
+ export const WithTitleAndImage: StoryFn<ComponentProps> = Template.bind({})
32
+ WithTitleAndImage.args = {
33
+ link: makePreview({
34
+ og_scrape_url: 'https://linktr.ee/example',
35
+ title: 'Linktree — the link in your bio',
36
+ image_url: '/image-thumbnail.jpg',
37
+ }),
38
+ }
39
+
40
+ export const TitleOnly: StoryFn<ComponentProps> = Template.bind({})
41
+ TitleOnly.args = {
42
+ link: makePreview({
43
+ og_scrape_url: 'https://example.com/blog/post',
44
+ title: 'A long article title that explains why widgets are important',
45
+ }),
46
+ }
47
+
48
+ export const UrlOnly: StoryFn<ComponentProps> = Template.bind({})
49
+ UrlOnly.args = {
50
+ link: makePreview({
51
+ og_scrape_url: 'https://example.com/raw',
52
+ }),
53
+ }
54
+
55
+ export const VeryLongUrl: StoryFn<ComponentProps> = Template.bind({})
56
+ VeryLongUrl.args = {
57
+ link: makePreview({
58
+ og_scrape_url:
59
+ 'https://example.com/very/long/path/with/many/segments/and/query/parameters?param1=value1&param2=value2',
60
+ title: 'Page with an extremely long URL',
61
+ image_url: '/image-thumbnail.jpg',
62
+ }),
63
+ }
@@ -0,0 +1,57 @@
1
+ import { XIcon } from '@phosphor-icons/react'
2
+ import React from 'react'
3
+ import { LinkPreview } from 'stream-chat'
4
+
5
+ interface CustomLinkPreviewCardProps {
6
+ link: LinkPreview
7
+ onDismiss: (url: string) => void
8
+ }
9
+
10
+ const CustomLinkPreviewCard: React.FC<CustomLinkPreviewCardProps> = ({
11
+ link,
12
+ onDismiss,
13
+ }) => {
14
+ const { og_scrape_url, title, image_url } = link
15
+
16
+ const handleDismissLink = (e: React.MouseEvent) => {
17
+ e.preventDefault()
18
+ onDismiss(og_scrape_url)
19
+ }
20
+
21
+ return (
22
+ <a
23
+ href={og_scrape_url}
24
+ target="_blank"
25
+ rel="noopener noreferrer"
26
+ className="relative block w-[280px] max-w-full rounded-[24px] bg-[#121110] p-2 no-underline transition-opacity hover:opacity-90"
27
+ >
28
+ {image_url && (
29
+ <img
30
+ src={image_url}
31
+ alt={title || ''}
32
+ className="h-[180px] w-full rounded-[20px] object-cover"
33
+ />
34
+ )}
35
+ <button
36
+ type="button"
37
+ onClick={handleDismissLink}
38
+ className="absolute right-4 top-4 flex size-6 items-center justify-center rounded-full border border-white/40 bg-white/70 backdrop-blur-2xl focus-ring"
39
+ aria-label="Close link preview"
40
+ >
41
+ <XIcon className="size-4 text-black/90" />
42
+ </button>
43
+ <div className="p-2">
44
+ {title && (
45
+ <div className="text-[14px] font-medium leading-5 text-white">
46
+ {title}
47
+ </div>
48
+ )}
49
+ <div className="text-[12px] leading-4 text-white/55">
50
+ {og_scrape_url}
51
+ </div>
52
+ </div>
53
+ </a>
54
+ )
55
+ }
56
+
57
+ export default CustomLinkPreviewCard
@@ -1,12 +1,12 @@
1
- import { XIcon } from '@phosphor-icons/react'
2
1
  import React from 'react'
3
2
  import {
4
- LinkPreview,
5
3
  LinkPreviewsManager,
6
4
  LinkPreviewsManagerState,
7
5
  } from 'stream-chat'
8
6
  import { useMessageComposer, useStateStore } from 'stream-chat-react'
9
7
 
8
+ import CustomLinkPreviewCard from './CustomLinkPreviewCard'
9
+
10
10
  const linkPreviewsManagerStateSelector = (state: LinkPreviewsManagerState) => ({
11
11
  linkPreviews: Array.from(state.previews.values()).filter(
12
12
  (preview) =>
@@ -15,58 +15,6 @@ const linkPreviewsManagerStateSelector = (state: LinkPreviewsManagerState) => ({
15
15
  ),
16
16
  })
17
17
 
18
- interface CustomLinkPreviewCardProps {
19
- link: LinkPreview
20
- onDismiss: (url: string) => void
21
- }
22
-
23
- const CustomLinkPreviewCard: React.FC<CustomLinkPreviewCardProps> = ({
24
- link,
25
- onDismiss,
26
- }) => {
27
- const { og_scrape_url, title, image_url } = link
28
-
29
- const handleDismissLink = (e: React.MouseEvent) => {
30
- e.preventDefault()
31
- onDismiss(og_scrape_url)
32
- }
33
-
34
- return (
35
- <a
36
- href={og_scrape_url}
37
- target="_blank"
38
- rel="noopener noreferrer"
39
- className="relative block w-[280px] max-w-full rounded-[24px] bg-[#121110] p-2 no-underline transition-opacity hover:opacity-90"
40
- >
41
- {image_url && (
42
- <img
43
- src={image_url}
44
- alt={title || ''}
45
- className="h-[180px] w-full rounded-[20px] object-cover"
46
- />
47
- )}
48
- <button
49
- type="button"
50
- onClick={handleDismissLink}
51
- className="absolute right-4 top-4 flex size-6 items-center justify-center rounded-full border border-white/40 bg-white/70 backdrop-blur-2xl focus-ring"
52
- aria-label="Close link preview"
53
- >
54
- <XIcon className="size-4 text-black/90" />
55
- </button>
56
- <div className="p-2">
57
- {title && (
58
- <div className="text-[14px] font-medium leading-5 text-white">
59
- {title}
60
- </div>
61
- )}
62
- <div className="text-[12px] leading-4 text-white/55">
63
- {og_scrape_url}
64
- </div>
65
- </div>
66
- </a>
67
- )
68
- }
69
-
70
18
  export const CustomLinkPreviewList = () => {
71
19
  const { linkPreviewsManager } = useMessageComposer()
72
20
 
@@ -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,
@@ -17,6 +16,7 @@ import {
17
16
  WithComponents,
18
17
  } from 'stream-chat-react'
19
18
 
19
+ import { minutesAgo } from '../../stories/decorators/storyTime'
20
20
  import {
21
21
  currentUserArgType,
22
22
  StoryUser,
@@ -46,8 +46,8 @@ const createMockChannel = async (
46
46
  const mockMessages = messages.map((msg, index) => ({
47
47
  ...msg,
48
48
  type: msg.type ?? ('regular' as const),
49
- created_at: new Date(Date.now() - 1000 * 60 * (messages.length - index)),
50
- updated_at: new Date(Date.now() - 1000 * 60 * (messages.length - index)),
49
+ created_at: minutesAgo(messages.length - index),
50
+ updated_at: minutesAgo(messages.length - index),
51
51
  html: `<p>${msg.text}</p>`,
52
52
  attachments: msg.attachments ?? [],
53
53
  latest_reactions: [],
@@ -567,21 +567,6 @@ ConversationWithPaidAttachments.args = {
567
567
  },
568
568
  ],
569
569
  }
570
- ConversationWithPaidAttachments.play = async ({ canvasElement }) => {
571
- const canvas = within(canvasElement)
572
- const attachmentMsg = canvasElement.querySelector('[data-message-id="msg-3"]')
573
- await userEvent.hover(attachmentMsg!)
574
- const toggle = await canvas.findByTestId('message-actions-toggle-button')
575
- await userEvent.click(toggle)
576
- const deleteButton = await canvas.findByRole('button', { name: 'Delete' })
577
- await userEvent.click(deleteButton)
578
- await expect(
579
- canvas.getByRole('heading', { name: 'Delete attachment?' })
580
- ).toBeInTheDocument()
581
- await expect(
582
- canvas.getByText(/Deleting it will remove access for the buyer/)
583
- ).toBeInTheDocument()
584
- }
585
570
  ConversationWithPaidAttachments.parameters = {
586
571
  docs: {
587
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
@@ -2,6 +2,8 @@ import type { Meta, StoryFn } from '@storybook/react'
2
2
  import React from 'react'
3
3
  import { LocalMessage } from 'stream-chat'
4
4
 
5
+ import { now } from '../../stories/decorators/storyTime'
6
+
5
7
  import { MessageTag } from './MessageTag'
6
8
 
7
9
  type ComponentProps = React.ComponentProps<typeof MessageTag>
@@ -28,8 +30,8 @@ const createMockMessage = (options?: MockMessageOptions): LocalMessage =>
28
30
  id: 'msg-1',
29
31
  text: options?.text ?? 'Hello world',
30
32
  type: 'regular',
31
- created_at: new Date(),
32
- updated_at: new Date(),
33
+ created_at: now(),
34
+ updated_at: now(),
33
35
  metadata: options?.metadata,
34
36
  }) as LocalMessage
35
37
 
@@ -41,12 +43,18 @@ const Template: StoryFn<ComponentProps> = (args) => {
41
43
  )
42
44
  }
43
45
 
46
+ // Per-variant stories stay in Storybook for browsing/design review but are
47
+ // individually covered by rows in AllVariants — skip in Chromatic to keep
48
+ // the variant matrix as the single snapshot source of truth.
49
+ const skipInChromatic = { chromatic: { disableSnapshot: true } }
50
+
44
51
  export const Tip: StoryFn<ComponentProps> = Template.bind({})
45
52
  Tip.args = {
46
53
  message: createMockMessage({
47
54
  metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.50' },
48
55
  }),
49
56
  }
57
+ Tip.parameters = skipInChromatic
50
58
 
51
59
  export const TipStandalone: StoryFn<ComponentProps> = Template.bind({})
52
60
  TipStandalone.args = {
@@ -56,6 +64,7 @@ TipStandalone.args = {
56
64
  }),
57
65
  standalone: true,
58
66
  }
67
+ TipStandalone.parameters = skipInChromatic
59
68
 
60
69
  export const Paid: StoryFn<ComponentProps> = Template.bind({})
61
70
  Paid.args = {
@@ -63,11 +72,13 @@ Paid.args = {
63
72
  metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$25.00' },
64
73
  }),
65
74
  }
75
+ Paid.parameters = skipInChromatic
66
76
 
67
77
  export const ChatbotReceiverText: StoryFn<ComponentProps> = Template.bind({})
68
78
  ChatbotReceiverText.args = {
69
79
  message: createMockMessage({ metadata: { custom_type: 'MESSAGE_CHATBOT' } }),
70
80
  }
81
+ ChatbotReceiverText.parameters = skipInChromatic
71
82
 
72
83
  export const ChatbotSenderText: StoryFn<ComponentProps> = (args) => {
73
84
  return (
@@ -82,6 +93,7 @@ ChatbotSenderText.args = {
82
93
  message: createMockMessage({ metadata: { custom_type: 'MESSAGE_CHATBOT' } }),
83
94
  isMyMessage: true,
84
95
  }
96
+ ChatbotSenderText.parameters = skipInChromatic
85
97
 
86
98
  export const ChatbotSenderAttachment: StoryFn<ComponentProps> = Template.bind(
87
99
  {}
@@ -91,11 +103,13 @@ ChatbotSenderAttachment.args = {
91
103
  isMyMessage: true,
92
104
  hasAttachment: true,
93
105
  }
106
+ ChatbotSenderAttachment.parameters = skipInChromatic
94
107
 
95
108
  export const NoTag: StoryFn<ComponentProps> = Template.bind({})
96
109
  NoTag.args = {
97
110
  message: createMockMessage(),
98
111
  }
112
+ NoTag.parameters = skipInChromatic
99
113
 
100
114
  export const AllVariants: StoryFn = () => {
101
115
  return (
@@ -165,3 +179,9 @@ export const AllVariants: StoryFn = () => {
165
179
  </div>
166
180
  )
167
181
  }
182
+ // AllVariants is a design-review kitchen sink — every per-variant story
183
+ // (Tip, Paid, TipStandalone, ChatbotReceiverText, ChatbotSenderText,
184
+ // ChatbotSenderAttachment, NoTag) already covers its own row independently.
185
+ // Skip in Chromatic to avoid a "any one row changes → whole snapshot diffs"
186
+ // problem that masks which variant actually drifted.
187
+ AllVariants.parameters = { chromatic: { disableSnapshot: true } }
@@ -22,12 +22,18 @@ const Template: StoryFn<ComponentProps> = (args) => (
22
22
  </div>
23
23
  )
24
24
 
25
+ // Per-state stories (Unselected, GoodResponse, BadResponse) are individually
26
+ // covered by rows of AllVariants — skip in Chromatic. Interactive has no
27
+ // useful steady-state snapshot.
28
+ const skipInChromatic = { chromatic: { disableSnapshot: true } }
29
+
25
30
  export const Unselected: StoryFn<ComponentProps> = Template.bind({})
26
31
  Unselected.args = {
27
32
  selected: null,
28
33
  onVoteUp: () => {},
29
34
  onVoteDown: () => {},
30
35
  }
36
+ Unselected.parameters = skipInChromatic
31
37
 
32
38
  export const GoodResponse: StoryFn<ComponentProps> = Template.bind({})
33
39
  GoodResponse.args = {
@@ -35,6 +41,7 @@ GoodResponse.args = {
35
41
  onVoteUp: () => {},
36
42
  onVoteDown: () => {},
37
43
  }
44
+ GoodResponse.parameters = skipInChromatic
38
45
 
39
46
  export const BadResponse: StoryFn<ComponentProps> = Template.bind({})
40
47
  BadResponse.args = {
@@ -42,6 +49,7 @@ BadResponse.args = {
42
49
  onVoteUp: () => {},
43
50
  onVoteDown: () => {},
44
51
  }
52
+ BadResponse.parameters = skipInChromatic
45
53
 
46
54
  export const Interactive: StoryFn = () => {
47
55
  const [selected, setSelected] = useState<VoteSelection>(null)
@@ -55,6 +63,7 @@ export const Interactive: StoryFn = () => {
55
63
  </div>
56
64
  )
57
65
  }
66
+ Interactive.parameters = skipInChromatic
58
67
 
59
68
  export const AllVariants: StoryFn = () => (
60
69
  <div className="p-12 flex flex-col gap-6">
@@ -1,4 +1,5 @@
1
1
  import { ArrowUpIcon } from '@phosphor-icons/react'
2
+ import classNames from 'classnames'
2
3
  import React, { useContext } from 'react'
3
4
  import {
4
5
  AttachmentPreviewList as DefaultAttachmentPreviewList,
@@ -101,9 +102,11 @@ export interface CustomMessageInputProps {
101
102
  * `disabled` is true.
102
103
  */
103
104
  disabledReason?: string
105
+ className?: string
104
106
  }
105
107
 
106
108
  export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
109
+ className,
107
110
  renderActions,
108
111
  renderFooter,
109
112
  disabled = false,
@@ -117,7 +120,12 @@ export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
117
120
  if (disabled) {
118
121
  return (
119
122
  <>
120
- <div className="messaging-composer-locked-panel flex w-full flex-col items-center justify-center gap-3 px-6 py-4">
123
+ <div
124
+ className={classNames(
125
+ 'messaging-composer-locked-panel flex w-full flex-col items-center justify-center gap-3',
126
+ className
127
+ )}
128
+ >
121
129
  {disabledReason ? (
122
130
  <p className="max-w-[345px] text-center text-xs font-normal leading-[1.3] tracking-[0.12px] text-black/40">
123
131
  {disabledReason}
@@ -129,17 +137,19 @@ export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
129
137
  )
130
138
  }
131
139
 
140
+ const actions = renderActions?.()
141
+
132
142
  return (
133
- <div className="flex flex-col gap-4 p-4">
143
+ <div className={classNames('flex flex-col gap-4', className)}>
134
144
  <div
135
145
  // @ts-expect-error Only React 19 onwards has `inert` in its types.
136
146
  inert={isFrozen ? '' : undefined}
137
147
  aria-disabled={isFrozen || undefined}
138
148
  className="message-input flex items-end gap-4 aria-disabled:opacity-40"
139
149
  >
140
- {renderActions && (
150
+ {actions && (
141
151
  <div className="flex h-12 shrink-0 items-center justify-center">
142
- {renderActions()}
152
+ {actions}
143
153
  </div>
144
154
  )}
145
155
  <ComposerLockedContext.Provider value={isFrozen}>
@@ -31,6 +31,10 @@ const Template: StoryFn<EventComponentProps> = (args) => (
31
31
  </div>
32
32
  )
33
33
 
34
+ // Per-variant stories stay in Storybook for browsing/design review but are
35
+ // individually covered by rows in AllVariants — skip in Chromatic.
36
+ const skipInChromatic = { chromatic: { disableSnapshot: true } }
37
+
34
38
  export const DmAgentPaused: StoryFn<EventComponentProps> = Template.bind({})
35
39
  DmAgentPaused.args = createStoryProps({
36
40
  text: 'DM Agent paused for this conversation',
@@ -38,6 +42,7 @@ DmAgentPaused.args = createStoryProps({
38
42
  custom_type: 'SYSTEM_DM_AGENT_PAUSED',
39
43
  },
40
44
  })
45
+ DmAgentPaused.parameters = skipInChromatic
41
46
 
42
47
  export const DmAgentResumed: StoryFn<EventComponentProps> = Template.bind({})
43
48
  DmAgentResumed.args = createStoryProps({
@@ -46,6 +51,7 @@ DmAgentResumed.args = createStoryProps({
46
51
  custom_type: 'SYSTEM_DM_AGENT_RESUMED',
47
52
  },
48
53
  })
54
+ DmAgentResumed.parameters = skipInChromatic
49
55
 
50
56
  export const AgeSafetyBlocked: StoryFn<EventComponentProps> = Template.bind({})
51
57
  AgeSafetyBlocked.args = createStoryProps({
@@ -54,6 +60,7 @@ AgeSafetyBlocked.args = createStoryProps({
54
60
  custom_type: 'SYSTEM_AGE_SAFETY_BLOCKED',
55
61
  },
56
62
  })
63
+ AgeSafetyBlocked.parameters = skipInChromatic
57
64
 
58
65
  export const GenericFallback: StoryFn<EventComponentProps> = Template.bind({})
59
66
  GenericFallback.args = createStoryProps({
@@ -62,3 +69,50 @@ GenericFallback.args = createStoryProps({
62
69
  custom_type: 'MESSAGE_CHATBOT',
63
70
  },
64
71
  })
72
+ GenericFallback.parameters = skipInChromatic
73
+
74
+ export const AllVariants: StoryFn = () => {
75
+ const variants: { label: string; props: EventComponentProps }[] = [
76
+ {
77
+ label: 'DM Agent paused',
78
+ props: createStoryProps({
79
+ text: 'DM Agent paused for this conversation',
80
+ metadata: { custom_type: 'SYSTEM_DM_AGENT_PAUSED' },
81
+ }),
82
+ },
83
+ {
84
+ label: 'DM Agent resumed',
85
+ props: createStoryProps({
86
+ text: 'DM Agent resumed for this conversation',
87
+ metadata: { custom_type: 'SYSTEM_DM_AGENT_RESUMED' },
88
+ }),
89
+ },
90
+ {
91
+ label: 'Age safety blocked',
92
+ props: createStoryProps({
93
+ text: ' ',
94
+ metadata: { custom_type: 'SYSTEM_AGE_SAFETY_BLOCKED' },
95
+ }),
96
+ },
97
+ {
98
+ label: 'Generic fallback',
99
+ props: createStoryProps({
100
+ text: 'Message activity event',
101
+ metadata: { custom_type: 'MESSAGE_CHATBOT' },
102
+ }),
103
+ },
104
+ ]
105
+
106
+ return (
107
+ <div className="flex flex-col gap-4 bg-white p-6">
108
+ {variants.map(({ label, props }) => (
109
+ <div key={label} className="flex flex-col gap-1">
110
+ <span className="text-xs text-stone">{label}</span>
111
+ <div className="w-[420px]">
112
+ <CustomSystemMessage {...props} />
113
+ </div>
114
+ </div>
115
+ ))}
116
+ </div>
117
+ )
118
+ }
@@ -123,6 +123,13 @@ const meta: Meta<StoryProps> = {
123
123
  component: CustomTypingIndicator,
124
124
  parameters: {
125
125
  layout: 'centered',
126
+ // CustomTypingIndicator is animation-dominant (SMIL spinner + DOM
127
+ // appear/disappear on typing-state transitions). Even with the SMIL
128
+ // pause decorator, a snapshot here is mostly testing what frame the
129
+ // animation paused at — low value for visual regression, high flake
130
+ // potential. Skip it in Chromatic; the stories stay browsable in
131
+ // Storybook for design review.
132
+ chromatic: { disableSnapshot: true },
126
133
  },
127
134
  }
128
135
  export default meta
@@ -13,7 +13,17 @@ const IMAGE_THUMBNAIL = '/image-thumbnail.jpg'
13
13
 
14
14
  const meta: Meta = {
15
15
  title: 'LinkAttachment',
16
- parameters: { layout: 'fullscreen' },
16
+ parameters: {
17
+ layout: 'fullscreen',
18
+ // The single LinkApps story is a large matrix of every link-app card
19
+ // crossed with featured/classic layouts and locked/unlocked states.
20
+ // It's intended as a design-review canvas, not a regression target —
21
+ // a single pixel change in any cell would diff the whole thing. Skip
22
+ // it in Chromatic; the per-app/state behavior is exercised
23
+ // implicitly through the message-attachment stories that use these
24
+ // cards.
25
+ chromatic: { disableSnapshot: true },
26
+ },
17
27
  }
18
28
  export default meta
19
29
 
@@ -444,3 +444,7 @@ export const Received: StoryFn = () => {
444
444
  </Table>
445
445
  )
446
446
  }
447
+ // LockedAttachment.Received autoplays the video preview once paid; even with
448
+ // our SMIL/CSS animation freeze the <video> element advances frames
449
+ // independently and produces a non-deterministic snapshot. Skip in Chromatic.
450
+ Received.parameters = { chromatic: { disableSnapshot: true } }
@@ -2,6 +2,8 @@ import type { Meta, StoryFn } from '@storybook/react'
2
2
  import React from 'react'
3
3
  import type { LocalMessage } from 'stream-chat'
4
4
 
5
+ import { now } from '../../stories/decorators/storyTime'
6
+
5
7
  import { MediaMessage } from '.'
6
8
 
7
9
  const meta: Meta<typeof MediaMessage> = {
@@ -20,8 +22,8 @@ const base = (overrides: Partial<LocalMessage> = {}): LocalMessage => {
20
22
  id: 'msg-1',
21
23
  text: '',
22
24
  type: 'regular',
23
- created_at: new Date(),
24
- updated_at: new Date(),
25
+ created_at: now(),
26
+ updated_at: now(),
25
27
  deleted_at: null,
26
28
  pinned_at: null,
27
29
  status: 'received',
@@ -253,44 +253,6 @@ describe('MediaMessage', () => {
253
253
  expect(container.firstChild).toBeNull()
254
254
  })
255
255
 
256
- it('uses dark card background for sent (Creator) messages', () => {
257
- const { container } = renderWithProviders(
258
- <MediaMessage
259
- isMyMessage={true}
260
- message={msg({
261
- attachments: [
262
- {
263
- type: 'image',
264
- image_url: 'https://cdn.example.com/photo.jpg',
265
- mime_type: 'image/jpeg',
266
- },
267
- ],
268
- })}
269
- />
270
- )
271
-
272
- expect(container.querySelector('.bg-\\[\\#121110\\]')).toBeInTheDocument()
273
- })
274
-
275
- it('uses light card background for received (Visitor) messages', () => {
276
- const { container } = renderWithProviders(
277
- <MediaMessage
278
- isMyMessage={false}
279
- message={msg({
280
- attachments: [
281
- {
282
- type: 'image',
283
- image_url: 'https://cdn.example.com/photo.jpg',
284
- mime_type: 'image/jpeg',
285
- },
286
- ],
287
- })}
288
- />
289
- )
290
-
291
- expect(container.querySelector('.bg-white')).toBeInTheDocument()
292
- })
293
-
294
256
  it('shows Download action for received (Visitor) image attachment', () => {
295
257
  renderWithProviders(
296
258
  <MediaMessage