@linktr.ee/messaging-react 3.5.0-rc-1782408966 → 3.5.0-rc-1782792813

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-iekRe643.js → Card-BWWBaNKF.js} +2 -2
  2. package/dist/{Card-iekRe643.js.map → Card-BWWBaNKF.js.map} +1 -1
  3. package/dist/{Card-DJtDI03Y.cjs → Card-Bj4YaHav.cjs} +2 -2
  4. package/dist/{Card-DJtDI03Y.cjs.map → Card-Bj4YaHav.cjs.map} +1 -1
  5. package/dist/{Card-CUHEDe8O.js → Card-BnGeMt4f.js} +2 -2
  6. package/dist/{Card-CUHEDe8O.js.map → Card-BnGeMt4f.js.map} +1 -1
  7. package/dist/{Card-BNzC_bp8.js → Card-DkXzpFLA.js} +3 -3
  8. package/dist/{Card-BNzC_bp8.js.map → Card-DkXzpFLA.js.map} +1 -1
  9. package/dist/{Card-DuMr5KMP.cjs → Card-DpCwDviq.cjs} +2 -2
  10. package/dist/{Card-DuMr5KMP.cjs.map → Card-DpCwDviq.cjs.map} +1 -1
  11. package/dist/{Card-DEa50m9p.cjs → Card-qrDaFHtJ.cjs} +2 -2
  12. package/dist/{Card-DEa50m9p.cjs.map → Card-qrDaFHtJ.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-CvpRZFi-.js → LockedThumbnail-DJR--PAi.js} +2 -2
  14. package/dist/{LockedThumbnail-CvpRZFi-.js.map → LockedThumbnail-DJR--PAi.js.map} +1 -1
  15. package/dist/{LockedThumbnail-aME0f_ZF.cjs → LockedThumbnail-DhXEk-NX.cjs} +2 -2
  16. package/dist/{LockedThumbnail-aME0f_ZF.cjs.map → LockedThumbnail-DhXEk-NX.cjs.map} +1 -1
  17. package/dist/index-DzOA7Yu8.cjs +2 -0
  18. package/dist/index-DzOA7Yu8.cjs.map +1 -0
  19. package/dist/{index-BlpDX5fl.js → index-ubCRSgQk.js} +1423 -1328
  20. package/dist/index-ubCRSgQk.js.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.d.ts +40 -16
  23. package/dist/index.js +19 -19
  24. package/package.json +2 -2
  25. package/src/components/ChannelList/ChannelList.stories.tsx +5 -3
  26. package/src/components/ChannelList/CustomChannelPreview.stories.tsx +111 -1
  27. package/src/components/ChannelList/CustomChannelPreview.test.tsx +58 -46
  28. package/src/components/ChannelList/CustomChannelPreview.tsx +12 -13
  29. package/src/components/ChannelList/index.test.tsx +25 -2
  30. package/src/components/ChannelList/index.tsx +2 -1
  31. package/src/components/ChannelView.stories.tsx +45 -12
  32. package/src/components/ChannelView.test.tsx +64 -62
  33. package/src/components/ChannelView.tsx +123 -68
  34. package/src/components/CustomMessage/index.tsx +1 -0
  35. package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +34 -0
  36. package/src/components/CustomMessageInput/index.tsx +27 -2
  37. package/src/components/MediaMessage/index.tsx +13 -5
  38. package/src/components/MessagingShell/index.tsx +2 -0
  39. package/src/components/ParticipantBadges.tsx +42 -0
  40. package/src/hooks/useChannelStar.ts +9 -6
  41. package/src/index.ts +1 -2
  42. package/src/stories/mocks.tsx +30 -5
  43. package/src/stream-custom-data.ts +12 -5
  44. package/src/types.ts +27 -1
  45. package/dist/index-BIWS3Inh.cjs +0 -2
  46. package/dist/index-BIWS3Inh.cjs.map +0 -1
  47. package/dist/index-BlpDX5fl.js.map +0 -1
  48. package/src/components/VerifiedBadge/index.tsx +0 -25
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-BIWS3Inh.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.VerifiedBadge=e.VerifiedBadge;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-DzOA7Yu8.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.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Attachment } from 'stream-chat';
2
2
  import { Channel } from 'stream-chat';
3
3
  import { ChannelFilters } from 'stream-chat';
4
4
  import { ChannelMemberResponse } from 'stream-chat';
5
+ import { ChannelPreviewUIComponentProps } from 'stream-chat-react';
5
6
  import { ChannelSort } from 'stream-chat';
6
7
  import { ComponentType } from 'react';
7
8
  import { default as default_2 } from 'react';
@@ -132,6 +133,11 @@ export declare interface ChannelListProps {
132
133
  */
133
134
  sort?: ChannelSort;
134
135
  customEmptyStateIndicator?: React.ComponentType<EmptyStateIndicatorProps>;
136
+ /**
137
+ * Custom component rendered for each channel row in the conversation list.
138
+ * Defaults to the package's built-in CustomChannelPreview.
139
+ */
140
+ channelPreview?: ComponentType<ChannelPreviewUIComponentProps>;
135
141
  renderMessagePreview?: (message: LocalMessage | undefined, defaultPreview?: string) => React.ReactNode;
136
142
  /**
137
143
  * Language code used to pick translated message text from Stream Chat i18n.
@@ -149,7 +155,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
149
155
  * Props that MessagingShell passes through to ChannelView.
150
156
  * ChannelViewProps is the source of truth for these props.
151
157
  */
152
- declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderHeaderTitleBadges' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'showChannelInfo'>;
158
+ declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderHeaderTitleBadges' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'showChannelInfo' | 'composerInput'>;
153
159
 
154
160
  /**
155
161
  * ChannelView component props
@@ -351,6 +357,22 @@ export declare interface ChannelViewProps {
351
357
  * Use this to render staged paid or media attachments inside the input.
352
358
  */
353
359
  attachmentPreviewList?: ComponentType;
360
+ /**
361
+ * Replace the entire inner composer — the textarea + send arrangement — with
362
+ * a host-owned component. Passed to Stream's `<MessageInput>` as the `Input`
363
+ * component, so it runs inside the message-input context and reuses the
364
+ * library's send pipeline (`doSendMessageRequest`, message metadata, DM-agent
365
+ * suppression), attachments, and previews via the Stream hooks
366
+ * (`useMessageInputContext`, `useMessageComposerHasSendableData`,
367
+ * `TextareaComposer`, …). Use it when a surface needs a different composer
368
+ * layout (e.g. a full-width textarea above a toolbar row) than the default.
369
+ *
370
+ * The outer wrapper — frozen handling and the `renderMessageInputActions` /
371
+ * `renderMessageInputFooter` slots — is unchanged. Read the exported
372
+ * `useComposerLocked()` hook to honour the frozen state. Defaults to the
373
+ * built-in composer when omitted.
374
+ */
375
+ composerInput?: ComponentType;
354
376
  /**
355
377
  * Fired when the participant's name in the channel header is clicked. When
356
378
  * provided, the name renders as a button with a trailing caret (an affordance
@@ -1376,6 +1398,14 @@ export declare interface SentCardProps extends LockedAttachmentBaseProps {
1376
1398
  onFetchSource?: () => Promise<LockedAttachmentSource | void>;
1377
1399
  }
1378
1400
 
1401
+ /**
1402
+ * Read the composer's locked (frozen-channel) state from inside a custom
1403
+ * composer supplied via `composerInput`. Returns true when the channel is
1404
+ * frozen; mirror the default composer by rendering the textarea read-only and
1405
+ * disabling the send control.
1406
+ */
1407
+ export declare const useComposerLocked: () => boolean;
1408
+
1379
1409
  export declare function useCustomMessage<K extends keyof CustomMessageRegistry>(key: K): CustomMessageRegistry[K];
1380
1410
 
1381
1411
  /**
@@ -1399,17 +1429,6 @@ declare interface UseMessageVoteResult {
1399
1429
  */
1400
1430
  export declare const useMessaging: () => MessagingContextValue;
1401
1431
 
1402
- /**
1403
- * Linktree verification badge shown next to the name of a verified profile.
1404
- * Rendered when a participant's Stream user has `is_verified` set to true.
1405
- */
1406
- export declare const VerifiedBadge: default_2.FC<VerifiedBadgeProps>;
1407
-
1408
- export declare interface VerifiedBadgeProps {
1409
- className?: string;
1410
- size?: number;
1411
- }
1412
-
1413
1432
  declare interface VideoAttachmentSharedProps extends MessageAttachmentBaseProps {
1414
1433
  /** Single video — convenience for the most common case. */
1415
1434
  src?: string;
@@ -1482,12 +1501,12 @@ declare module 'stream-chat' {
1482
1501
  */
1483
1502
  chatbot_paused?: boolean;
1484
1503
  }
1485
- interface CustomUserData {
1504
+ interface CustomMemberData {
1486
1505
  /**
1487
- * Whether the user is a verified Linktree profile. When true, a
1488
- * verification badge is shown next to their name in messaging.
1506
+ * Whether the member should be identified as a customer in participant UI.
1507
+ * Stored on the channel member so it is scoped to this conversation.
1489
1508
  */
1490
- is_verified?: boolean;
1509
+ customer?: boolean;
1491
1510
  }
1492
1511
  interface CustomMessageData {
1493
1512
  /**
@@ -1506,5 +1525,10 @@ declare module 'stream-chat' {
1506
1525
  */
1507
1526
  dm_agent_system_type?: DmAgentSystemType;
1508
1527
  }
1528
+ interface CustomUserData {
1529
+ is_account?: boolean;
1530
+ is_follower?: boolean;
1531
+ username?: string;
1532
+ }
1509
1533
  }
1510
1534
 
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { a as s, b as t, C as i, c as n, d as o, e as m, F as g, f as r, L as l, h as M, M as u, i as L, j as c, k as d, l as h, V as p, m as v, n as A, o as C, p as F, q as k, s as f, t as V, u as b, v as x, w as y, x as B, y as P, z as S, B as q, D as z } from "./index-BlpDX5fl.js";
1
+ import { a as e, b as t, C as i, c as o, d as n, e as m, F as g, f as r, L as l, h as M, M as u, i as L, j as c, k as h, l as d, m as p, n as C, o as v, p as A, q as k, s as F, 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, E as D } from "./index-ubCRSgQk.js";
2
2
  export {
3
- s as ActionButton,
3
+ e as ActionButton,
4
4
  t as Avatar,
5
5
  i as ChannelEmptyState,
6
- n as ChannelList,
7
- o as ChannelView,
6
+ o as ChannelList,
7
+ n as ChannelView,
8
8
  m as CustomMessageProvider,
9
9
  g as FaqList,
10
10
  r as FaqListItem,
@@ -13,23 +13,23 @@ export {
13
13
  u as MediaMessage,
14
14
  L as MessageAttachment,
15
15
  c as MessageVoteButtons,
16
- d as MessagingProvider,
17
- h as MessagingShell,
18
- p as VerifiedBadge,
19
- v as buildCompactMetaLabel,
20
- A as formatFileSize,
21
- C as formatRelativeTime,
22
- F as getFileExtensionLabel,
16
+ h as MessagingProvider,
17
+ d as MessagingShell,
18
+ p as buildCompactMetaLabel,
19
+ C as formatFileSize,
20
+ v as formatRelativeTime,
21
+ A as getFileExtensionLabel,
23
22
  k as getMessageDisplayText,
24
- f as isLinkAttachment,
25
- V as isUuidLike,
26
- b as messageAttachmentGroupPositionFromStream,
23
+ F as isLinkAttachment,
24
+ b as isUuidLike,
25
+ f as messageAttachmentGroupPositionFromStream,
27
26
  x as normalizeLanguageCode,
28
27
  y as resolveLinkAttachment,
29
- B as resolveMediaFromMessage,
30
- P as resolveParticipantDisplayName,
31
- S as useCustomMessage,
32
- q as useMessageVote,
33
- z as useMessaging
28
+ P as resolveMediaFromMessage,
29
+ S as resolveParticipantDisplayName,
30
+ q as useComposerLocked,
31
+ z as useCustomMessage,
32
+ B as useMessageVote,
33
+ D as useMessaging
34
34
  };
35
35
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.5.0-rc-1782408966",
3
+ "version": "3.5.0-rc-1782792813",
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-1782408966",
41
+ "@linktr.ee/messaging-core": "2.3.0-rc-1782792813",
42
42
  "@phosphor-icons/react": "^2.1.10"
43
43
  },
44
44
  "devDependencies": {
@@ -1,7 +1,7 @@
1
1
  import type { Meta, StoryFn } from '@storybook/react'
2
2
  import React from 'react'
3
3
 
4
- import { MockChatProvider } from '../../stories/mocks'
4
+ import { MockChatProviderWithChannels } from '../../stories/mocks'
5
5
 
6
6
  import { ChannelList } from '.'
7
7
 
@@ -21,9 +21,9 @@ const meta: Meta<ComponentProps> = {
21
21
  },
22
22
  decorators: [
23
23
  (Story) => (
24
- <MockChatProvider>
24
+ <MockChatProviderWithChannels>
25
25
  <Story />
26
- </MockChatProvider>
26
+ </MockChatProviderWithChannels>
27
27
  ),
28
28
  ],
29
29
  }
@@ -39,11 +39,13 @@ const Template: StoryFn<ComponentProps> = (args) => {
39
39
 
40
40
  export const Default: StoryFn<ComponentProps> = Template.bind({})
41
41
  Default.args = {
42
+ filters: { type: 'messaging' },
42
43
  onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
43
44
  }
44
45
 
45
46
  export const WithSelection: StoryFn<ComponentProps> = Template.bind({})
46
47
  WithSelection.args = {
48
+ filters: { type: 'messaging' },
47
49
  onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
48
50
  }
49
51
 
@@ -6,6 +6,7 @@ import {
6
6
  LocalMessage,
7
7
  StreamChat,
8
8
  } from 'stream-chat'
9
+ import type { ChannelPreviewUIComponentProps } from 'stream-chat-react'
9
10
 
10
11
  import {
11
12
  daysAgo,
@@ -14,13 +15,16 @@ import {
14
15
  now,
15
16
  secondsAgo,
16
17
  } from '../../stories/decorators/storyTime'
18
+ import { MockChatProviderWithChannels } from '../../stories/mocks'
17
19
 
18
20
  import CustomChannelPreview from './CustomChannelPreview'
19
21
 
22
+ import { ChannelList } from '.'
23
+
20
24
  type ComponentProps = React.ComponentProps<typeof CustomChannelPreview>
21
25
 
22
26
  const meta: Meta<ComponentProps> = {
23
- title: 'ChannelList/CustomChannelPreview',
27
+ title: 'ChannelList/ChannelPreview',
24
28
  component: CustomChannelPreview,
25
29
  parameters: {
26
30
  layout: 'centered',
@@ -55,6 +59,8 @@ const createMockChannel = (options: {
55
59
  thumb_url?: string
56
60
  }>
57
61
  lastMessageMetadata?: CustomMessageData['metadata']
62
+ isCustomer?: boolean
63
+ isStarred?: boolean
58
64
  }): Channel => {
59
65
  const {
60
66
  id,
@@ -66,6 +72,8 @@ const createMockChannel = (options: {
66
72
  unreadCount = 0,
67
73
  lastMessageAttachments,
68
74
  lastMessageMetadata,
75
+ isCustomer = false,
76
+ isStarred = false,
69
77
  } = options
70
78
 
71
79
  const eventListeners = new Map<string, Set<(...args: unknown[]) => void>>()
@@ -89,8 +97,12 @@ const createMockChannel = (options: {
89
97
  image: participantImage,
90
98
  },
91
99
  user_id: participantId,
100
+ ...(isCustomer ? { customer: true } : {}),
92
101
  },
93
102
  },
103
+ membership: {
104
+ pinned_at: isStarred ? now().toISOString() : null,
105
+ },
94
106
  messages:
95
107
  lastMessageText || lastMessageAttachments || lastMessageMetadata
96
108
  ? ([
@@ -249,6 +261,61 @@ ChatbotMessage.args = {
249
261
  }),
250
262
  }
251
263
 
264
+ export const ParticipantBadges: StoryFn = () => {
265
+ const channels = [
266
+ createMockChannel({
267
+ id: 'channel-customer',
268
+ participantName: 'Alice Customer',
269
+ participantId: 'participant-customer',
270
+ participantImage: 'https://i.pravatar.cc/150?img=40',
271
+ lastMessageText: 'I bought this from your Linktree',
272
+ lastMessageTime: minutesAgo(2),
273
+ isCustomer: true,
274
+ }),
275
+ createMockChannel({
276
+ id: 'channel-starred',
277
+ participantName: 'Bob Starred',
278
+ participantId: 'participant-starred',
279
+ participantImage: 'https://i.pravatar.cc/150?img=41',
280
+ lastMessageText: 'Following up on this thread',
281
+ lastMessageTime: minutesAgo(8),
282
+ isStarred: true,
283
+ }),
284
+ createMockChannel({
285
+ id: 'channel-customer-starred',
286
+ participantName: 'Carol Customer',
287
+ participantId: 'participant-customer-starred',
288
+ participantImage: 'https://i.pravatar.cc/150?img=42',
289
+ lastMessageText: 'Thanks for the quick reply',
290
+ lastMessageTime: minutesAgo(15),
291
+ isCustomer: true,
292
+ isStarred: true,
293
+ }),
294
+ ]
295
+
296
+ return (
297
+ <div className="w-[360px] bg-chalk border border-sand rounded-lg overflow-hidden">
298
+ {channels.map((channel) => (
299
+ <CustomChannelPreview
300
+ key={channel.id}
301
+ channel={channel}
302
+ onChannelSelect={(channel) => {
303
+ console.log('Channel selected:', channel.id)
304
+ }}
305
+ />
306
+ ))}
307
+ </div>
308
+ )
309
+ }
310
+ ParticipantBadges.parameters = {
311
+ docs: {
312
+ description: {
313
+ story:
314
+ 'Shows the built-in customer badge from participant.member customer data and the starred badge from the current user membership pinned_at value.',
315
+ },
316
+ },
317
+ }
318
+
252
319
  export const TipMessage: StoryFn<ComponentProps> = Template.bind({})
253
320
  TipMessage.args = {
254
321
  channel: createMockChannel({
@@ -799,3 +866,46 @@ WithLongAttachmentUrl.args = {
799
866
  }),
800
867
  }
801
868
  WithLongAttachmentUrl.parameters = skipInChromatic
869
+
870
+ const HostChannelPreview: React.FC<ChannelPreviewUIComponentProps> = ({
871
+ channel,
872
+ unread,
873
+ }) => {
874
+ const participant = Object.values(channel?.state?.members || {}).find(
875
+ (member) => member.user?.id !== channel?._client?.userID
876
+ )
877
+
878
+ return (
879
+ <div className="mx-3 mt-3 rounded-[20px] border border-[#e0dfdb] bg-white p-4 shadow-[0_4px_16px_rgba(0,0,0,0.06)]">
880
+ <div className="flex items-center justify-between">
881
+ <div className="min-w-0">
882
+ <p className="truncate text-sm font-semibold text-[#191918]">
883
+ {participant?.user?.name ?? 'Unknown visitor'}
884
+ </p>
885
+ <p className="mt-1 line-clamp-1 text-xs text-[#717070]">
886
+ Host-owned channel preview
887
+ </p>
888
+ </div>
889
+ {Boolean(unread) && (
890
+ <span className="ml-3 rounded-full bg-[#7f22fe] px-2 py-1 text-xs font-medium text-white">
891
+ {unread}
892
+ </span>
893
+ )}
894
+ </div>
895
+ </div>
896
+ )
897
+ }
898
+
899
+ export const Custom: StoryFn = () => (
900
+ <MockChatProviderWithChannels>
901
+ <div className="h-screen w-full md:w-[360px] bg-chalk">
902
+ <ChannelList
903
+ filters={{ type: 'messaging' }}
904
+ onChannelSelect={(channel) => {
905
+ console.log('Channel selected:', channel.id)
906
+ }}
907
+ channelPreview={HostChannelPreview}
908
+ />
909
+ </div>
910
+ </MockChatProviderWithChannels>
911
+ )
@@ -12,12 +12,15 @@ const mockUser = {
12
12
  }
13
13
 
14
14
  type MockChannel = Channel & {
15
- emitMemberUpdated: (member?: { pinned_at?: string | null }) => void
15
+ emitMemberUpdated: (member?: {
16
+ pinned_at?: string | null
17
+ user_id?: string
18
+ }) => void
16
19
  }
17
20
 
18
21
  const createMockChannel = (messages: Partial<LocalMessage>[]): MockChannel => {
19
22
  const memberUpdatedListeners = new Set<
20
- (event: { member?: { pinned_at?: string | null } }) => void
23
+ (event: { member?: { pinned_at?: string | null; user_id?: string } }) => void
21
24
  >()
22
25
 
23
26
  return {
@@ -38,24 +41,34 @@ const createMockChannel = (messages: Partial<LocalMessage>[]): MockChannel => {
38
41
  on: vi.fn(
39
42
  (
40
43
  eventName: string,
41
- listener: (event: { member?: { pinned_at?: string | null } }) => void
44
+ listener: (event: {
45
+ member?: { pinned_at?: string | null; user_id?: string }
46
+ }) => void
42
47
  ) => {
43
48
  if (eventName === 'member.updated') {
44
49
  memberUpdatedListeners.add(listener)
45
50
  }
51
+ return {
52
+ unsubscribe: () => memberUpdatedListeners.delete(listener),
53
+ }
46
54
  }
47
55
  ),
48
56
  off: vi.fn(
49
57
  (
50
58
  eventName: string,
51
- listener: (event: { member?: { pinned_at?: string | null } }) => void
59
+ listener: (event: {
60
+ member?: { pinned_at?: string | null; user_id?: string }
61
+ }) => void
52
62
  ) => {
53
63
  if (eventName === 'member.updated') {
54
64
  memberUpdatedListeners.delete(listener)
55
65
  }
56
66
  }
57
67
  ),
58
- emitMemberUpdated: (member?: { pinned_at?: string | null }) => {
68
+ emitMemberUpdated: (member?: {
69
+ pinned_at?: string | null
70
+ user_id?: string
71
+ }) => {
59
72
  memberUpdatedListeners.forEach((listener) => listener({ member }))
60
73
  },
61
74
  } as unknown as MockChannel
@@ -101,44 +114,6 @@ describe('CustomChannelPreview', () => {
101
114
  expect(screen.getByText('Unknown member')).toBeInTheDocument()
102
115
  })
103
116
 
104
- it('shows the verification badge when the participant is verified', () => {
105
- const channel = createMockChannel([])
106
- channel.state.members['participant-1'] = {
107
- user: { id: 'participant-1', name: 'Alice', is_verified: true },
108
- user_id: 'participant-1',
109
- }
110
-
111
- renderWithProviders(
112
- <CustomChannelPreview {...defaultProps} channel={channel} />
113
- )
114
-
115
- expect(screen.getByLabelText('Verified')).toBeInTheDocument()
116
- })
117
-
118
- it('does not show the verification badge when the participant is not verified', () => {
119
- const channel = createMockChannel([])
120
- channel.state.members['participant-1'] = {
121
- user: { id: 'participant-1', name: 'Alice', is_verified: false },
122
- user_id: 'participant-1',
123
- }
124
-
125
- renderWithProviders(
126
- <CustomChannelPreview {...defaultProps} channel={channel} />
127
- )
128
-
129
- expect(screen.queryByLabelText('Verified')).not.toBeInTheDocument()
130
- })
131
-
132
- it('does not show the verification badge when is_verified is absent', () => {
133
- const channel = createMockChannel([])
134
-
135
- renderWithProviders(
136
- <CustomChannelPreview {...defaultProps} channel={channel} />
137
- )
138
-
139
- expect(screen.queryByLabelText('Verified')).not.toBeInTheDocument()
140
- })
141
-
142
117
  it('shows the latest non-system message when the last message is a system message', () => {
143
118
  const channel = createMockChannel([
144
119
  {
@@ -291,6 +266,21 @@ describe('CustomChannelPreview', () => {
291
266
  expect(screen.getByText('No messages yet')).toBeInTheDocument()
292
267
  })
293
268
 
269
+ it('shows a customer badge from participant member data', () => {
270
+ const channel = createMockChannel([])
271
+ channel.state.members['participant-1'] = {
272
+ user: { id: 'participant-1', name: 'Alice' },
273
+ user_id: 'participant-1',
274
+ customer: true,
275
+ }
276
+
277
+ renderWithProviders(
278
+ <CustomChannelPreview {...defaultProps} channel={channel} />
279
+ )
280
+
281
+ expect(screen.getByRole('img', { name: 'Customer' })).toBeInTheDocument()
282
+ })
283
+
294
284
  it('updates starred state when the channel membership changes', () => {
295
285
  const channel = createMockChannel([])
296
286
 
@@ -299,15 +289,37 @@ describe('CustomChannelPreview', () => {
299
289
  )
300
290
 
301
291
  expect(
302
- screen.queryByRole('heading', { name: /starred conversation/i })
292
+ screen.queryByRole('img', { name: 'Starred conversation' })
303
293
  ).not.toBeInTheDocument()
304
294
 
305
295
  act(() => {
306
- channel.emitMemberUpdated({ pinned_at: new Date().toISOString() })
296
+ channel.emitMemberUpdated({
297
+ pinned_at: new Date().toISOString(),
298
+ user_id: mockUser.id,
299
+ })
307
300
  })
308
301
 
309
302
  expect(
310
- screen.getByRole('heading', { name: /starred conversation/i })
303
+ screen.getByRole('img', { name: 'Starred conversation' })
311
304
  ).toBeInTheDocument()
312
305
  })
306
+
307
+ it('ignores starred updates for other members', () => {
308
+ const channel = createMockChannel([])
309
+
310
+ renderWithProviders(
311
+ <CustomChannelPreview {...defaultProps} channel={channel} />
312
+ )
313
+
314
+ act(() => {
315
+ channel.emitMemberUpdated({
316
+ pinned_at: new Date().toISOString(),
317
+ user_id: 'participant-1',
318
+ })
319
+ })
320
+
321
+ expect(
322
+ screen.queryByRole('img', { name: 'Starred conversation' })
323
+ ).not.toBeInTheDocument()
324
+ })
313
325
  })
@@ -2,13 +2,12 @@ import classNames from 'classnames'
2
2
  import React from 'react'
3
3
  import { ChannelPreviewUIComponentProps } from 'stream-chat-react'
4
4
 
5
- import { useChannelStar } from '../../hooks/useChannelStar'
6
5
  import { formatRelativeTime } from '../../utils/formatRelativeTime'
7
6
  import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
8
7
  import { resolveParticipantDisplayName } from '../../utils/resolveParticipantDisplayName'
9
8
  import { Avatar } from '../Avatar'
10
9
  import { isChatbotMessage } from '../CustomMessage/MessageTag'
11
- import { VerifiedBadge } from '../VerifiedBadge'
10
+ import { ParticipantBadges } from '../ParticipantBadges'
12
11
 
13
12
  import { useChannelListContext } from './ChannelListContext'
14
13
 
@@ -49,7 +48,6 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
49
48
  )
50
49
  const participantName = resolveParticipantDisplayName(participant?.user)
51
50
  const participantImage = participant?.user?.image
52
- const isParticipantVerified = participant?.user?.is_verified === true
53
51
 
54
52
  // Get last non-system message for preview
55
53
  const lastMessage = (() => {
@@ -71,11 +69,13 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
71
69
  if (isTip) return displayText ? `💵 ${displayText}` : '💵 Sent a tip'
72
70
  const isPaid = lastMessage?.metadata?.custom_type === 'MESSAGE_PAID'
73
71
  if (isPaid) return displayText ? `💰 ${displayText}` : '💰 Sent a message'
74
- const isPaidAttachment = lastMessage?.metadata?.custom_type === 'MESSAGE_ATTACHMENT'
75
- if (isPaidAttachment) return displayText ? `📎 ${displayText}` : '📎 Sent an attachment'
76
-
72
+ const isPaidAttachment =
73
+ lastMessage?.metadata?.custom_type === 'MESSAGE_ATTACHMENT'
74
+ if (isPaidAttachment)
75
+ return displayText ? `📎 ${displayText}` : '📎 Sent an attachment'
76
+
77
77
  if (displayText) return displayText
78
-
78
+
79
79
  const attachment = lastMessage?.attachments?.[0]
80
80
  if (attachment) {
81
81
  // Link previews - show the actual URL
@@ -105,7 +105,6 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
105
105
  const messagePreview = renderMessagePreview
106
106
  ? renderMessagePreview(lastMessage, lastMessageText)
107
107
  : `${isLastMessageFromChatbot ? '✨ ' : ''}${lastMessageText}`
108
- const isChannelStarred = useChannelStar(channel)
109
108
 
110
109
  // Use the unread prop passed by Stream Chat (reactive and updates automatically)
111
110
  const unreadCount = unread ?? 0
@@ -133,6 +132,7 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
133
132
  'hover:bg-black/[0.02]': !isSelected,
134
133
  }
135
134
  )}
135
+ data-dd-privacy="mask"
136
136
  >
137
137
  <div className="flex items-start gap-4">
138
138
  {/* Avatar */}
@@ -141,7 +141,6 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
141
141
  name={participantName}
142
142
  image={participantImage}
143
143
  size={48}
144
- starred={isChannelStarred}
145
144
  />
146
145
 
147
146
  {/* Content column */}
@@ -149,11 +148,11 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
149
148
  {/* Name and timestamp row */}
150
149
  <div className="flex items-center justify-between gap-2">
151
150
  <h3 className="flex min-w-0 items-center gap-1 text-sm font-medium text-[#191918]">
152
- {isChannelStarred && (
153
- <span className="sr-only">Starred conversation. </span>
154
- )}
155
151
  <span className="min-w-0 truncate">{participantName}</span>
156
- {isParticipantVerified && <VerifiedBadge />}
152
+ <ParticipantBadges
153
+ channel={channel}
154
+ participant={participant}
155
+ />
157
156
  </h3>
158
157
  {lastMessageTime && (
159
158
  <span className="text-xs text-[#717070] flex-shrink-0">