@linktr.ee/messaging-react 3.5.6-rc-1783064039 → 3.5.6-rc-1783097662

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 (44) hide show
  1. package/dist/{Card-BY5GWs2L.cjs → Card-4eHS-HQB.cjs} +2 -2
  2. package/dist/{Card-BY5GWs2L.cjs.map → Card-4eHS-HQB.cjs.map} +1 -1
  3. package/dist/{Card-DdUewLf6.cjs → Card-Bxu71IQM.cjs} +2 -2
  4. package/dist/{Card-DdUewLf6.cjs.map → Card-Bxu71IQM.cjs.map} +1 -1
  5. package/dist/{Card-CsQmLvWE.cjs → Card-By-eDX0U.cjs} +2 -2
  6. package/dist/{Card-CsQmLvWE.cjs.map → Card-By-eDX0U.cjs.map} +1 -1
  7. package/dist/{Card-CuWniEOV.js → Card-CmLNgIdO.js} +2 -2
  8. package/dist/{Card-CuWniEOV.js.map → Card-CmLNgIdO.js.map} +1 -1
  9. package/dist/{Card-CpgkVykU.js → Card-XSDNvOZf.js} +3 -3
  10. package/dist/{Card-CpgkVykU.js.map → Card-XSDNvOZf.js.map} +1 -1
  11. package/dist/{Card-DQdw5-A_.js → Card-mDB7sZiu.js} +2 -2
  12. package/dist/{Card-DQdw5-A_.js.map → Card-mDB7sZiu.js.map} +1 -1
  13. package/dist/{LockedThumbnail-D5hLQ6Kz.js → LockedThumbnail-C44Wkle9.js} +3 -3
  14. package/dist/{LockedThumbnail-D5hLQ6Kz.js.map → LockedThumbnail-C44Wkle9.js.map} +1 -1
  15. package/dist/{LockedThumbnail-s_LZkGm4.cjs → LockedThumbnail-CWhzO3wx.cjs} +2 -2
  16. package/dist/{LockedThumbnail-s_LZkGm4.cjs.map → LockedThumbnail-CWhzO3wx.cjs.map} +1 -1
  17. package/dist/assets/index.css +1 -1
  18. package/dist/{index-3vM1UOPb.js → index-BRV621wo.js} +1601 -1794
  19. package/dist/index-BRV621wo.js.map +1 -0
  20. package/dist/index-DBVgTHrA.cjs +2 -0
  21. package/dist/index-DBVgTHrA.cjs.map +1 -0
  22. package/dist/index.cjs +1 -1
  23. package/dist/index.js +1 -1
  24. package/package.json +2 -2
  25. package/src/components/AttachmentCard/index.tsx +1 -1
  26. package/src/components/ChannelView.stories.tsx +11 -65
  27. package/src/components/ChannelView.test.tsx +12 -30
  28. package/src/components/ChannelView.tsx +22 -50
  29. package/src/components/CustomLinkPreviewList/CustomLinkPreviewCard.tsx +1 -1
  30. package/src/components/CustomLinkPreviewList/index.tsx +6 -12
  31. package/src/components/CustomMessage/CustomMessage.stories.tsx +27 -0
  32. package/src/components/CustomMessage/MessageAttachmentConversations.stories.tsx +2 -2
  33. package/src/components/CustomMessage/MessageTag.stories.tsx +2 -2
  34. package/src/components/CustomMessageInput/index.tsx +4 -12
  35. package/src/components/LinkAttachment/components/_shared/CardShell.tsx +1 -1
  36. package/src/components/LockedAttachment/components/_shared/LockedCardShell.tsx +1 -1
  37. package/src/components/MediaMessage/index.tsx +8 -1
  38. package/src/components/MessageAttachment/_shared/Bubble.tsx +7 -7
  39. package/src/components/MessageAttachment/_shared/CompactDocumentRow.tsx +3 -3
  40. package/src/styles.css +104 -9
  41. package/dist/index-3vM1UOPb.js.map +0 -1
  42. package/dist/index-BIJpO0jL.cjs +0 -2
  43. package/dist/index-BIJpO0jL.cjs.map +0 -1
  44. package/src/components/ChannelHeaderRedesign.tsx +0 -231
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-BIJpO0jL.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.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-DBVgTHrA.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.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 c, i as u, 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 P, u as b, v as f, w as x, x as y, y as S, z as q, B as z, D as B, E as D, G as E } from "./index-3vM1UOPb.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 c, i as u, 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 P, u as b, v as f, w as x, x as y, y as S, z as q, B as z, D as B, E as D, G as E } from "./index-BRV621wo.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.5.6-rc-1783064039",
3
+ "version": "3.5.6-rc-1783097662",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@linktr.ee/component-library": "11.8.6",
41
- "@linktr.ee/messaging-core": "2.3.0-rc-1783064039",
41
+ "@linktr.ee/messaging-core": "2.3.0-rc-1783097662",
42
42
  "@phosphor-icons/react": "^2.1.10"
43
43
  },
44
44
  "devDependencies": {
@@ -57,7 +57,7 @@ const AttachmentCard: React.FC<AttachmentCardProps> = ({
57
57
  data-testid={dataTestId}
58
58
  className={classNames(
59
59
  'relative w-[280px] select-none overflow-hidden rounded-[24px] shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]',
60
- isDark ? 'bg-[#121110]' : 'bg-white'
60
+ isDark ? 'bg-[#1e2330]' : 'bg-white'
61
61
  )}
62
62
  >
63
63
  {topLeft ? (
@@ -16,7 +16,6 @@ import { ChannelView } from './ChannelView'
16
16
  import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
17
17
 
18
18
  type ComponentProps = React.ComponentProps<typeof ChannelView>
19
- const SCROLLABLE_MESSAGE_COUNT = 10
20
19
 
21
20
  const meta: Meta<ComponentProps> = {
22
21
  title: 'ChannelView',
@@ -42,9 +41,7 @@ const createMockChannel = async (
42
41
  client: StreamChat,
43
42
  hasMessages = true,
44
43
  followerStatus?: string | boolean,
45
- isFrozen = false,
46
- participantCustomerTag?: string,
47
- hasScrollableMessages = false
44
+ isFrozen = false
48
45
  ) => {
49
46
  const participant = mockParticipants[0]
50
47
 
@@ -104,34 +101,6 @@ const createMockChannel = async (
104
101
  cid: 'messaging:storybook-channel-1',
105
102
  mentioned_users: [],
106
103
  },
107
- ...(hasScrollableMessages
108
- ? Array.from({ length: SCROLLABLE_MESSAGE_COUNT }, (_, index) => {
109
- const isParticipantMessage = index % 2 === 0
110
- const user = isParticipantMessage ? participant : mockUser
111
- const text = isParticipantMessage
112
- ? `Here is another message to show how the redesigned header floats above the thread (${index + 1}).`
113
- : `Replying so the conversation has enough content to scroll under the header (${index + 1}).`
114
-
115
- return {
116
- id: `msg-extra-${index + 1}`,
117
- text,
118
- type: 'regular' as const,
119
- created_at: minutesAgo(Math.max(1, 25 - index)),
120
- updated_at: minutesAgo(Math.max(1, 25 - index)),
121
- user,
122
- html: `<p>${text}</p>`,
123
- attachments: [],
124
- latest_reactions: [],
125
- own_reactions: [],
126
- reaction_counts: {},
127
- reaction_scores: {},
128
- reply_count: 0,
129
- status: 'received',
130
- cid: 'messaging:storybook-channel-1',
131
- mentioned_users: [],
132
- }
133
- })
134
- : []),
135
104
  ]
136
105
  : []
137
106
 
@@ -149,11 +118,7 @@ const createMockChannel = async (
149
118
  }
150
119
 
151
120
  // Create a real channel using the client
152
- const channel = client.channel(
153
- 'messaging',
154
- 'storybook-channel-1',
155
- channelData
156
- )
121
+ const channel = client.channel('messaging', 'storybook-channel-1', channelData)
157
122
 
158
123
  // Mock the watch method to return mocked data
159
124
  channel.watch = async () => {
@@ -177,7 +142,6 @@ const createMockChannel = async (
177
142
  user_id: participant.id,
178
143
  role: 'member',
179
144
  is_account: true,
180
- customerTag: participantCustomerTag,
181
145
  },
182
146
  }
183
147
  return {
@@ -212,16 +176,12 @@ type TemplateProps = ComponentProps & {
212
176
  name?: string
213
177
  image?: string
214
178
  }
215
- participantCustomerTag?: string
216
- hasScrollableMessages?: boolean
217
179
  }
218
180
 
219
181
  const Template: StoryFn<TemplateProps> = (args) => {
220
182
  const {
221
183
  followerStatus,
222
184
  isFrozen = false,
223
- participantCustomerTag,
224
- hasScrollableMessages,
225
185
  typingUser,
226
186
  ...channelViewProps
227
187
  } = args
@@ -233,27 +193,16 @@ const Template: StoryFn<TemplateProps> = (args) => {
233
193
  client.user = mockUser
234
194
  return client
235
195
  })
236
-
196
+
237
197
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
238
198
 
239
199
  useEffect(() => {
240
- createMockChannel(
241
- client,
242
- true,
243
- followerStatus,
244
- isFrozen,
245
- participantCustomerTag,
246
- hasScrollableMessages
247
- ).then((mockChannel) => {
248
- setChannel(mockChannel)
249
- })
250
- }, [
251
- client,
252
- followerStatus,
253
- isFrozen,
254
- participantCustomerTag,
255
- hasScrollableMessages,
256
- ])
200
+ createMockChannel(client, true, followerStatus, isFrozen).then(
201
+ (mockChannel) => {
202
+ setChannel(mockChannel)
203
+ }
204
+ )
205
+ }, [client, followerStatus, isFrozen])
257
206
 
258
207
  useEffect(() => {
259
208
  if (!channel || !typingUser) {
@@ -307,9 +256,6 @@ export const ParticipantIdentityTrigger: StoryFn<TemplateProps> = Template.bind(
307
256
  )
308
257
  ParticipantIdentityTrigger.args = {
309
258
  showBackButton: false,
310
- showStarButton: true,
311
- participantCustomerTag: 'CUSTOMER_PAID',
312
- hasScrollableMessages: true,
313
259
  onParticipantNameClick: () =>
314
260
  // eslint-disable-next-line no-alert
315
261
  window.alert('Participant details clicked — open Thread content'),
@@ -318,7 +264,7 @@ ParticipantIdentityTrigger.parameters = {
318
264
  docs: {
319
265
  description: {
320
266
  story:
321
- 'Header with `onParticipantNameClick` set: the redesigned floating header centers the avatar and name pill, shows paid/star badges when present, and fires the callback when clicked (shows an alert here). With the prop omitted (see Default) the previous header remains in use.',
267
+ 'Header with `onParticipantNameClick` set: the participant avatar and name render as one button with a trailing caret in both the mobile and desktop header layouts. Click the identity block to fire the callback (shows an alert here). With the prop omitted (see Default) the header remains non-interactive.',
322
268
  },
323
269
  },
324
270
  }
@@ -596,7 +542,7 @@ const EmptyTemplate: StoryFn<ComponentProps> = (args) => {
596
542
  client.user = mockUser
597
543
  return client
598
544
  })
599
-
545
+
600
546
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
601
547
 
602
548
  useEffect(() => {
@@ -22,9 +22,7 @@ vi.mock('stream-chat-react', () => ({
22
22
  Window: ({ children }: { children: React.ReactNode }) => (
23
23
  <div data-testid="window">{children}</div>
24
24
  ),
25
- MessageList: ({ head }: { head?: React.ReactElement }) => (
26
- <div data-testid="message-list">{head}</div>
27
- ),
25
+ MessageList: () => <div data-testid="message-list" />,
28
26
  WithComponents: ({ children }: { children: React.ReactNode }) => (
29
27
  <>{children}</>
30
28
  ),
@@ -102,11 +100,9 @@ const createChannel = ({
102
100
  username: undefined as string | undefined,
103
101
  is_account: true,
104
102
  },
105
- otherMemberCustomerTag,
106
103
  }: {
107
104
  currentUserId?: 'visitor-1' | 'linker-1'
108
105
  currentUserRole?: 'owner' | 'member'
109
- otherMemberCustomerTag?: string
110
106
  otherUser?: {
111
107
  id: string
112
108
  name?: string
@@ -138,7 +134,6 @@ const createChannel = ({
138
134
  is_account:
139
135
  otherUser.is_account ?? currentUserId === 'visitor-1',
140
136
  },
141
- customerTag: otherMemberCustomerTag,
142
137
  role: currentUserRole === 'owner' ? 'member' : 'owner',
143
138
  },
144
139
  },
@@ -208,7 +203,7 @@ describe('ChannelView', () => {
208
203
  )
209
204
  })
210
205
 
211
- it('renders the redesigned header button when onParticipantNameClick is provided', () => {
206
+ it('renders the participant name as a button that fires onParticipantNameClick', () => {
212
207
  const onParticipantNameClick = vi.fn()
213
208
  renderWithProviders(
214
209
  <ChannelView
@@ -217,42 +212,29 @@ describe('ChannelView', () => {
217
212
  />
218
213
  )
219
214
 
220
- const button = screen.getByRole('button', {
215
+ // Both the mobile and desktop header layouts render the button (one is
216
+ // hidden via CSS), so there is at least one match.
217
+ const buttons = screen.getAllByRole('button', {
221
218
  name: /view details for linker/i,
222
219
  })
223
- expect(button).toHaveClass(
220
+ expect(buttons.length).toBeGreaterThanOrEqual(1)
221
+ expect(buttons[0]).toHaveClass(
224
222
  'appearance-none',
225
223
  'border-0',
226
224
  'bg-transparent',
227
- 'flex-col'
225
+ 'rounded-[12px]',
226
+ 'px-1',
227
+ 'py-0.5'
228
228
  )
229
229
 
230
230
  const avatars = screen.getAllByTestId('avatar')
231
231
  expect(avatars.length).toBeGreaterThanOrEqual(1)
232
- expect(avatars[0].closest('button')).toBe(button)
233
- expect(screen.getByTestId('channel-actions-menu')).toBeInTheDocument()
232
+ expect(avatars[0].closest('button')).toBe(buttons[0])
234
233
 
235
- button.click()
234
+ buttons[0].click()
236
235
  expect(onParticipantNameClick).toHaveBeenCalledTimes(1)
237
236
  })
238
237
 
239
- it('renders redesigned header badges for paid and starred conversations', () => {
240
- mockIsStarred = true
241
-
242
- renderWithProviders(
243
- <ChannelView
244
- channel={createChannel({
245
- otherMemberCustomerTag: 'CUSTOMER_PAID',
246
- })}
247
- onParticipantNameClick={vi.fn()}
248
- showStarButton={true}
249
- />
250
- )
251
-
252
- expect(screen.getByLabelText('Paid customer')).toBeInTheDocument()
253
- expect(screen.getByLabelText('Starred conversation')).toBeInTheDocument()
254
- })
255
-
256
238
  it('renders the actions menu by default', () => {
257
239
  renderWithProviders(<ChannelView channel={createChannel()} />)
258
240
 
@@ -25,7 +25,6 @@ import { resolveParticipantDisplayName } from '../utils/resolveParticipantDispla
25
25
 
26
26
  import { Avatar } from './Avatar'
27
27
  import { ChannelActionsMenu } from './ChannelActionsMenu'
28
- import { ChannelHeaderRedesign } from './ChannelHeaderRedesign'
29
28
  import { CustomDateSeparator } from './CustomDateSeparator'
30
29
  import { CustomMessage } from './CustomMessage'
31
30
  import { CustomMessageActions } from './CustomMessage/CustomMessageActions'
@@ -498,28 +497,27 @@ const ChannelViewInner: React.FC<{
498
497
  >
499
498
  <Window>
500
499
  {/* Custom Channel Header */}
501
- {!onParticipantNameClick && (
502
- <div key="lt-channel-header">
503
- <CustomChannelHeader
504
- onBack={onBack}
505
- showBackButton={showBackButton}
506
- showChannelInfo={showChannelInfo}
507
- showStarButton={showStarButton}
508
- dmAgentEnabled={showDmAgentHeader}
509
- onLeaveConversation={onLeaveConversation}
510
- onBlockParticipant={onBlockParticipant}
511
- showDeleteConversation={showDeleteConversation}
512
- showBlockParticipant={showBlockParticipant}
513
- showReportParticipant={showReportParticipant}
514
- onDeleteConversationClick={onDeleteConversationClick}
515
- onBlockParticipantClick={onBlockParticipantClick}
516
- onReportParticipantClick={onReportParticipantClick}
517
- customChannelActions={customChannelActions}
518
- renderChannelActions={renderChannelActions}
519
- renderHeaderTitleBadges={renderHeaderTitleBadges}
520
- />
521
- </div>
522
- )}
500
+ <div key="lt-channel-header">
501
+ <CustomChannelHeader
502
+ onBack={onBack}
503
+ showBackButton={showBackButton}
504
+ showChannelInfo={showChannelInfo}
505
+ showStarButton={showStarButton}
506
+ dmAgentEnabled={showDmAgentHeader}
507
+ onLeaveConversation={onLeaveConversation}
508
+ onBlockParticipant={onBlockParticipant}
509
+ showDeleteConversation={showDeleteConversation}
510
+ showBlockParticipant={showBlockParticipant}
511
+ showReportParticipant={showReportParticipant}
512
+ onDeleteConversationClick={onDeleteConversationClick}
513
+ onBlockParticipantClick={onBlockParticipantClick}
514
+ onReportParticipantClick={onReportParticipantClick}
515
+ customChannelActions={customChannelActions}
516
+ renderChannelActions={renderChannelActions}
517
+ onParticipantNameClick={onParticipantNameClick}
518
+ renderHeaderTitleBadges={renderHeaderTitleBadges}
519
+ />
520
+ </div>
523
521
 
524
522
  {/* Custom Banner/Summary */}
525
523
  {renderChannelBanner ? (
@@ -533,33 +531,7 @@ const ChannelViewInner: React.FC<{
533
531
  key="lt-channel-message-list"
534
532
  className="flex-1 overflow-hidden relative"
535
533
  >
536
- <MessageList
537
- head={
538
- onParticipantNameClick ? (
539
- <ChannelHeaderRedesign
540
- onBack={onBack}
541
- showBackButton={showBackButton}
542
- showChannelInfo={showChannelInfo}
543
- showStarButton={showStarButton}
544
- dmAgentEnabled={showDmAgentHeader}
545
- onLeaveConversation={onLeaveConversation}
546
- onBlockParticipant={onBlockParticipant}
547
- showDeleteConversation={showDeleteConversation}
548
- showBlockParticipant={showBlockParticipant}
549
- showReportParticipant={showReportParticipant}
550
- onDeleteConversationClick={onDeleteConversationClick}
551
- onBlockParticipantClick={onBlockParticipantClick}
552
- onReportParticipantClick={onReportParticipantClick}
553
- customChannelActions={customChannelActions}
554
- renderChannelActions={renderChannelActions}
555
- onParticipantNameClick={onParticipantNameClick}
556
- renderHeaderTitleBadges={renderHeaderTitleBadges}
557
- />
558
- ) : undefined
559
- }
560
- hideDeletedMessages
561
- hideNewMessageSeparator={false}
562
- />
534
+ <MessageList hideDeletedMessages hideNewMessageSeparator={false} />
563
535
  </div>
564
536
 
565
537
  {renderConversationFooter ? (
@@ -23,7 +23,7 @@ const CustomLinkPreviewCard: React.FC<CustomLinkPreviewCardProps> = ({
23
23
  href={og_scrape_url}
24
24
  target="_blank"
25
25
  rel="noopener noreferrer"
26
- className="relative block w-full rounded-[24px] bg-[#121110] p-2 no-underline transition-opacity hover:opacity-90"
26
+ className="relative block w-full rounded-[24px] bg-[#1e2330] p-2 no-underline transition-opacity hover:opacity-90"
27
27
  >
28
28
  {image_url && (
29
29
  <img
@@ -32,19 +32,13 @@ export const CustomLinkPreviewList = () => {
32
32
  if (!showLinkPreviews) return null
33
33
 
34
34
  return (
35
- // Layout contract: this list is the composer's *shrinkable* region. The
36
- // mobile visitor surface sizes itself to the keyboard-shrunk
37
- // visualViewport, so when vertical space runs out this container gives
38
- // way (min-h-0) and scrolls, while the textarea + send row stays visible
39
- // (shrink-0 in CustomMessageInput). Each card is wrapped in a shrink-0
40
- // block so it keeps its intrinsic height and the container scrolls — a
41
- // bare flex-column child would be shrunk on the column axis and its
42
- // fixed-height hero would spill out of the collapsed card box.
43
- <div className="messaging-link-preview-list flex min-h-0 flex-col items-stretch w-full gap-2 mb-4 overflow-y-auto overscroll-contain">
35
+ <div className="flex flex-col items-stretch w-full gap-2 mb-4">
44
36
  {stateLinkPreviews.map((linkPreview) => (
45
- <div key={linkPreview.og_scrape_url} className="w-full shrink-0">
46
- <CustomLinkPreviewCard link={linkPreview} onDismiss={handleDismiss} />
47
- </div>
37
+ <CustomLinkPreviewCard
38
+ key={linkPreview.og_scrape_url}
39
+ link={linkPreview}
40
+ onDismiss={handleDismiss}
41
+ />
48
42
  ))}
49
43
  </div>
50
44
  )
@@ -247,6 +247,33 @@ WithTipTag.args = {
247
247
  ],
248
248
  }
249
249
 
250
+ export const ThreeConsecutiveSentMessages: StoryFn<TemplateProps> =
251
+ Template.bind({})
252
+ ThreeConsecutiveSentMessages.args = {
253
+ messages: [
254
+ {
255
+ id: 'msg-1',
256
+ text: 'why is it that the sun',
257
+ user: storyUsers.visitor,
258
+ },
259
+ {
260
+ id: 'msg-2',
261
+ text: 'has rays',
262
+ user: storyUsers.creator,
263
+ },
264
+ {
265
+ id: 'msg-3',
266
+ text: 'how how how',
267
+ user: storyUsers.creator,
268
+ },
269
+ {
270
+ id: 'msg-4',
271
+ text: 'thanks for the note',
272
+ user: storyUsers.visitor,
273
+ },
274
+ ],
275
+ }
276
+
250
277
  export const TipOnly: StoryFn<TemplateProps> = Template.bind({})
251
278
  TipOnly.args = {
252
279
  messages: [
@@ -42,8 +42,8 @@ interface ChatNode {
42
42
  }
43
43
 
44
44
  const TEXT_BG_BY_ROLE: Record<ChatRole, string> = {
45
- sender: 'bg-[#121110] text-white',
46
- receiver: 'bg-[#e9eaed] text-[#080707]',
45
+ sender: 'bg-[#1e2330] text-white',
46
+ receiver: 'bg-[#f2f1ef] text-[#000000]',
47
47
  }
48
48
 
49
49
  const TextBubble: React.FC<{ role: ChatRole; children: React.ReactNode }> = ({
@@ -83,7 +83,7 @@ ChatbotReceiverText.parameters = skipInChromatic
83
83
  export const ChatbotSenderText: StoryFn<ComponentProps> = (args) => {
84
84
  return (
85
85
  <div className="p-12">
86
- <div className="bg-[#121110] rounded-[24px] px-4 py-3 w-[280px]">
86
+ <div className="bg-[#1e2330] rounded-[24px] px-4 py-3 w-[280px]">
87
87
  <MessageTag {...args} />
88
88
  </div>
89
89
  </div>
@@ -150,7 +150,7 @@ export const AllVariants: StoryFn = () => {
150
150
  </div>
151
151
  <div className="flex items-center gap-4">
152
152
  <span className="text-sm w-32">Chatbot (sender):</span>
153
- <div className="bg-[#121110] rounded-[24px] px-4 py-3 w-[280px]">
153
+ <div className="bg-[#1e2330] rounded-[24px] px-4 py-3 w-[280px]">
154
154
  <MessageTag
155
155
  message={createMockMessage({
156
156
  metadata: { custom_type: 'MESSAGE_CHATBOT' },
@@ -61,22 +61,14 @@ const CustomMessageInputInner: React.FC = () => {
61
61
  } = useComponentContext('CustomMessageInput')
62
62
 
63
63
  return (
64
- // min-h-0 lets the composer shrink under keyboard squeeze (the link
65
- // preview list inside is the scrollable region); the textarea + send row
66
- // is shrink-0 so it can never be pushed out of view. self-stretch matters
67
- // because the parent `.message-input` row uses items-end, which suppresses
68
- // the default cross-axis stretch — without it this container keeps its
69
- // content height and the squeeze never reaches the preview list. Stream's
70
- // <MessageInput> renders its Input directly (context providers only, no
71
- // wrapper element), so this is a direct flex item of that row.
72
64
  <div
73
- className="central-container flex min-h-0 flex-col gap-2 min-w-0 w-full self-stretch p-2 bg-white rounded-[1.5rem] shadow-[0_4px_16px_0_rgba(0,0,0,0.08),0_1px_2px_0_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.04)]"
65
+ className="central-container flex flex-col gap-2 min-w-0 w-full p-2 bg-white rounded-[1.5rem] shadow-[0_4px_16px_0_rgba(0,0,0,0.08),0_1px_2px_0_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.04)]"
74
66
  data-dd-privacy="mask"
75
67
  >
76
68
  <QuotedMessagePreview />
77
69
  <CustomLinkPreviewList />
78
70
  <AttachmentPreviewList />
79
- <div className="flex shrink-0">
71
+ <div className="flex">
80
72
  <div className="w-full ml-2 mr-4 self-center leading-[0]">
81
73
  <TextareaComposer
82
74
  aria-disabled={disabled || undefined}
@@ -163,12 +155,12 @@ export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
163
155
  }
164
156
 
165
157
  return (
166
- <div className="flex min-h-0 flex-col gap-4 p-4">
158
+ <div className="flex flex-col gap-4 p-4">
167
159
  <div
168
160
  // @ts-expect-error Only React 19 onwards has `inert` in its types.
169
161
  inert={isFrozen ? '' : undefined}
170
162
  aria-disabled={isFrozen || undefined}
171
- className="message-input flex min-h-0 items-end gap-4 aria-disabled:opacity-40"
163
+ className="message-input flex items-end gap-4 aria-disabled:opacity-40"
172
164
  >
173
165
  {renderActions && (
174
166
  <div className="flex h-12 shrink-0 items-center justify-center">
@@ -64,7 +64,7 @@ const CardShell: React.FC<CardShellProps> = ({
64
64
  const isInteractive = href != null || onClick != null
65
65
  const className = classNames(
66
66
  SHELL_CLASS,
67
- bgClassName ?? (variant === 'dark' ? 'bg-[#121110]' : 'bg-white'),
67
+ bgClassName ?? (variant === 'dark' ? 'bg-[#1e2330]' : 'bg-white'),
68
68
  // `focus-ring` is a design-system utility from the component-library
69
69
  // tailwind preset — outline-none + a black 2px focus-visible ring
70
70
  // with offset, so keyboard users can see the focused card.
@@ -26,7 +26,7 @@ const LockedCardShell: React.FC<LockedCardShellProps> = ({
26
26
  className={classNames(
27
27
  'relative w-[280px] select-none overflow-hidden rounded-md',
28
28
  'shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_1px_2px_rgba(0,0,0,0.04),0_8px_32px_rgba(0,0,0,0.1)]',
29
- variant === 'dark' ? 'bg-[#121110]' : 'bg-white'
29
+ variant === 'dark' ? 'bg-[#1e2330]' : 'bg-white'
30
30
  )}
31
31
  >
32
32
  {children}
@@ -15,7 +15,7 @@ function formatBytes(bytes: number): string {
15
15
  }
16
16
 
17
17
  function linkCardShellClass(isMyMessage: boolean) {
18
- const bg = isMyMessage ? 'bg-[#121110]' : 'bg-[#F3F3F1]'
18
+ const bg = isMyMessage ? 'bg-[#1e2330]' : 'bg-[#F3F3F1]'
19
19
  return `w-[280px] select-none overflow-hidden rounded-[24px] ${bg} shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]`
20
20
  }
21
21
 
@@ -318,6 +318,13 @@ const MediaMessageRoot: React.FC<MediaMessageProps> = ({
318
318
  <div className="str-chat__message-bubble-wrapper">
319
319
  <div
320
320
  className="str-chat__message-bubble"
321
+ // The visible child is a self-contained AttachmentCard (white /
322
+ // #1e2330 card with its own shadow + radius), not a speech
323
+ // bubble, and this wrapper is transparent. Mark it so the
324
+ // received/sent tail in `styles.css` skips it — otherwise a
325
+ // tail tinted from the text-bubble surface (#f2f1ef) hangs off
326
+ // a white card and reads as a detached grey nub.
327
+ data-attachment-bubble="true"
321
328
  style={{ padding: 0, borderRadius: 0, overflow: 'visible', background: 'transparent' }}
322
329
  >
323
330
  {linkAttachment ? (
@@ -29,8 +29,8 @@ export interface BubbleProps {
29
29
  // Colors and metrics tracked from `stream-chat-react`'s default
30
30
  // `.str-chat__message-bubble` token set so attachments visually drop
31
31
  // into a normal `CustomMessage` thread without any seams:
32
- // - light → `--str-chat__secondary-surface-color` = grey200 (#e9eaed)
33
- // - dark → `.str-chat__message--me` override in `styles.css` (#121110)
32
+ // - light → `--str-chat__message-bubble-background-color` in `styles.css` (#f2f1ef)
33
+ // - dark → `.str-chat__message--me` override in `styles.css` (#1e2330)
34
34
  // - text padding → `--str-chat__spacing-2 --str-chat__spacing-4` = 8px 16px
35
35
  // - border-radius → `--str-chat__border-radius-md` = 18px
36
36
  //
@@ -41,13 +41,13 @@ export interface BubbleProps {
41
41
  // rebrand would need to touch every site. A central token would
42
42
  // collapse the migration to one definition.
43
43
  const BUBBLE_BG_BY_VARIANT: Record<BubbleVariant, string> = {
44
- dark: 'bg-[#121110]',
45
- light: 'bg-[#e9eaed]',
44
+ dark: 'bg-[#1e2330]',
45
+ light: 'bg-[#f2f1ef]',
46
46
  }
47
47
 
48
48
  const BUBBLE_TEXT_BY_VARIANT: Record<BubbleVariant, string> = {
49
49
  dark: 'text-white',
50
- light: 'text-[#080707]',
50
+ light: 'text-[#000000]',
51
51
  }
52
52
 
53
53
  const BUBBLE_BORDER_BY_VARIANT: Record<BubbleVariant, string> = {
@@ -104,8 +104,8 @@ const CORNER_CLASSES_BY_SIDE_AND_POSITION: Record<
104
104
  * is the file', 'Here is the image').
105
105
  *
106
106
  * Two visual variants:
107
- * - `dark` — sender-side (Composer / Sent), white text on `#121110`.
108
- * - `light` — recipient-side (Received), dark text on `#e9eaed`.
107
+ * - `dark` — sender-side (Composer / Sent), white text on `#1e2330`.
108
+ * - `light` — recipient-side (Received), dark text on `#f2f1ef`.
109
109
  *
110
110
  * Background colors, padding, border-radius, and inherited font come
111
111
  * from the stream-chat-react `.str-chat__message-bubble` token set so
@@ -41,11 +41,11 @@ export interface CompactDocumentRowProps {
41
41
  // so they stay variant-aware. The title inherits the bubble's text
42
42
  // color (set on the parent `Bubble`).
43
43
  //
44
- // `text-black/55` on the `#e9eaed` light bubble lands right at the
44
+ // `text-black/55` on the `#f2f1ef` light bubble lands right at the
45
45
  // WCAG AA 4.5:1 small-text threshold (≈4.5:1) — bump to `/65` so
46
46
  // the meta line has a comfortable margin on the light bubble. The
47
- // dark variant (`text-white/55` on `#121110`) already passes AA at
48
- // ~6:1, so we leave it alone.
47
+ // dark variant (`text-white/55` on `#1e2330`) already passes AA,
48
+ // so we leave it alone.
49
49
  const META_CLASS_BY_VARIANT: Record<BubbleVariant, string> = {
50
50
  dark: 'text-white/55',
51
51
  light: 'text-black/65',