@linktr.ee/messaging-react 3.4.2-rc-1782454787 → 3.4.2-rc-1782480798

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-BQZcPeQn.js → Card-BKi2g6x-.js} +2 -2
  2. package/dist/{Card-BQZcPeQn.js.map → Card-BKi2g6x-.js.map} +1 -1
  3. package/dist/{Card-CSqZXHCM.cjs → Card-BMmrCMqJ.cjs} +2 -2
  4. package/dist/{Card-CSqZXHCM.cjs.map → Card-BMmrCMqJ.cjs.map} +1 -1
  5. package/dist/{Card-U9LAeCEA.js → Card-BYu6OBuX.js} +2 -2
  6. package/dist/{Card-U9LAeCEA.js.map → Card-BYu6OBuX.js.map} +1 -1
  7. package/dist/{Card-Cp0Ioui8.cjs → Card-DtIjtWy6.cjs} +2 -2
  8. package/dist/{Card-Cp0Ioui8.cjs.map → Card-DtIjtWy6.cjs.map} +1 -1
  9. package/dist/{Card-DFaRAFV1.js → Card-DvE5BCPf.js} +3 -3
  10. package/dist/{Card-DFaRAFV1.js.map → Card-DvE5BCPf.js.map} +1 -1
  11. package/dist/{Card-DWmbH7oz.cjs → Card-VO-i6CuV.cjs} +2 -2
  12. package/dist/{Card-DWmbH7oz.cjs.map → Card-VO-i6CuV.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-nLWi4Kxt.cjs → LockedThumbnail-Cl8uwCxM.cjs} +2 -2
  14. package/dist/{LockedThumbnail-nLWi4Kxt.cjs.map → LockedThumbnail-Cl8uwCxM.cjs.map} +1 -1
  15. package/dist/{LockedThumbnail-JOZFwdjw.js → LockedThumbnail-DTkd_kEt.js} +2 -2
  16. package/dist/{LockedThumbnail-JOZFwdjw.js.map → LockedThumbnail-DTkd_kEt.js.map} +1 -1
  17. package/dist/index-CSSe9eu7.cjs +2 -0
  18. package/dist/index-CSSe9eu7.cjs.map +1 -0
  19. package/dist/{index-DyG_ZuPp.js → index-IzhF38yj.js} +3440 -3205
  20. package/dist/index-IzhF38yj.js.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.d.ts +25 -13
  23. package/dist/index.js +14 -13
  24. package/package.json +2 -2
  25. package/src/components/ChannelList/CustomChannelPreview.stories.tsx +0 -63
  26. package/src/components/ChannelList/CustomChannelPreview.test.tsx +8 -58
  27. package/src/components/ChannelList/CustomChannelPreview.tsx +12 -7
  28. package/src/components/ChannelView.stories.tsx +7 -38
  29. package/src/components/ChannelView.test.tsx +27 -44
  30. package/src/components/ChannelView.tsx +9 -19
  31. package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +109 -0
  32. package/src/components/CustomMessage/StreamAttachmentMessage.tsx +408 -0
  33. package/src/components/CustomMessage/index.tsx +42 -0
  34. package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +34 -0
  35. package/src/components/CustomMessageInput/index.tsx +23 -1
  36. package/src/components/MessagingShell/index.tsx +2 -0
  37. package/src/hooks/useChannelStar.ts +6 -9
  38. package/src/index.ts +1 -0
  39. package/src/stream-custom-data.ts +1 -16
  40. package/src/types.ts +18 -0
  41. package/dist/index-BSQPos2Q.cjs +0 -2
  42. package/dist/index-BSQPos2Q.cjs.map +0 -1
  43. package/dist/index-DyG_ZuPp.js.map +0 -1
  44. package/src/components/ParticipantBadges.tsx +0 -42
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-BSQPos2Q.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-CSSe9eu7.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
@@ -149,7 +149,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
149
149
  * Props that MessagingShell passes through to ChannelView.
150
150
  * ChannelViewProps is the source of truth for these props.
151
151
  */
152
- declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderHeaderTitleBadges' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'showChannelInfo'>;
152
+ declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderHeaderTitleBadges' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'showChannelInfo' | 'composerInput'>;
153
153
 
154
154
  /**
155
155
  * ChannelView component props
@@ -351,6 +351,22 @@ export declare interface ChannelViewProps {
351
351
  * Use this to render staged paid or media attachments inside the input.
352
352
  */
353
353
  attachmentPreviewList?: ComponentType;
354
+ /**
355
+ * Replace the entire inner composer — the textarea + send arrangement — with
356
+ * a host-owned component. Passed to Stream's `<MessageInput>` as the `Input`
357
+ * component, so it runs inside the message-input context and reuses the
358
+ * library's send pipeline (`doSendMessageRequest`, message metadata, DM-agent
359
+ * suppression), attachments, and previews via the Stream hooks
360
+ * (`useMessageInputContext`, `useMessageComposerHasSendableData`,
361
+ * `TextareaComposer`, …). Use it when a surface needs a different composer
362
+ * layout (e.g. a full-width textarea above a toolbar row) than the default.
363
+ *
364
+ * The outer wrapper — frozen handling and the `renderMessageInputActions` /
365
+ * `renderMessageInputFooter` slots — is unchanged. Read the exported
366
+ * `useComposerLocked()` hook to honour the frozen state. Defaults to the
367
+ * built-in composer when omitted.
368
+ */
369
+ composerInput?: ComponentType;
354
370
  /**
355
371
  * Fired when the participant's name in the channel header is clicked. When
356
372
  * provided, the name renders as a button with a trailing caret (an affordance
@@ -1376,6 +1392,14 @@ export declare interface SentCardProps extends LockedAttachmentBaseProps {
1376
1392
  onFetchSource?: () => Promise<LockedAttachmentSource | void>;
1377
1393
  }
1378
1394
 
1395
+ /**
1396
+ * Read the composer's locked (frozen-channel) state from inside a custom
1397
+ * composer supplied via `composerInput`. Returns true when the channel is
1398
+ * frozen; mirror the default composer by rendering the textarea read-only and
1399
+ * disabling the send control.
1400
+ */
1401
+ export declare const useComposerLocked: () => boolean;
1402
+
1379
1403
  export declare function useCustomMessage<K extends keyof CustomMessageRegistry>(key: K): CustomMessageRegistry[K];
1380
1404
 
1381
1405
  /**
@@ -1471,13 +1495,6 @@ declare module 'stream-chat' {
1471
1495
  */
1472
1496
  chatbot_paused?: boolean;
1473
1497
  }
1474
- interface CustomMemberData {
1475
- /**
1476
- * Whether the member should be identified as a customer in participant UI.
1477
- * Stored on the channel member so it is scoped to this conversation.
1478
- */
1479
- customer?: boolean;
1480
- }
1481
1498
  interface CustomMessageData {
1482
1499
  /**
1483
1500
  * When true, hides the date timestamp in system messages.
@@ -1495,10 +1512,5 @@ declare module 'stream-chat' {
1495
1512
  */
1496
1513
  dm_agent_system_type?: DmAgentSystemType;
1497
1514
  }
1498
- interface CustomUserData {
1499
- is_account?: boolean;
1500
- is_follower?: boolean;
1501
- username?: string;
1502
- }
1503
1515
  }
1504
1516
 
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
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-DyG_ZuPp.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-IzhF38yj.js";
2
2
  export {
3
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
- l as FaqListItem,
11
- r as LinkAttachment,
10
+ r as FaqListItem,
11
+ l as LinkAttachment,
12
12
  M as LockedAttachment,
13
13
  u as MediaMessage,
14
14
  L as MessageAttachment,
@@ -16,19 +16,20 @@ export {
16
16
  h as MessagingProvider,
17
17
  d as MessagingShell,
18
18
  p as buildCompactMetaLabel,
19
- v as formatFileSize,
20
- A as formatRelativeTime,
21
- C as getFileExtensionLabel,
22
- F as getMessageDisplayText,
23
- k as isLinkAttachment,
19
+ C as formatFileSize,
20
+ v as formatRelativeTime,
21
+ A as getFileExtensionLabel,
22
+ k as getMessageDisplayText,
23
+ F as isLinkAttachment,
24
24
  b as isUuidLike,
25
25
  f as messageAttachmentGroupPositionFromStream,
26
26
  x as normalizeLanguageCode,
27
27
  y as resolveLinkAttachment,
28
28
  P as resolveMediaFromMessage,
29
29
  S as resolveParticipantDisplayName,
30
- q as useCustomMessage,
31
- z as useMessageVote,
32
- B as useMessaging
30
+ q as useComposerLocked,
31
+ z as useCustomMessage,
32
+ B as useMessageVote,
33
+ D as useMessaging
33
34
  };
34
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.4.2-rc-1782454787",
3
+ "version": "3.4.2-rc-1782480798",
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-1782454787",
41
+ "@linktr.ee/messaging-core": "2.3.0-rc-1782480798",
42
42
  "@phosphor-icons/react": "^2.1.10"
43
43
  },
44
44
  "devDependencies": {
@@ -55,8 +55,6 @@ const createMockChannel = (options: {
55
55
  thumb_url?: string
56
56
  }>
57
57
  lastMessageMetadata?: CustomMessageData['metadata']
58
- isCustomer?: boolean
59
- isStarred?: boolean
60
58
  }): Channel => {
61
59
  const {
62
60
  id,
@@ -68,8 +66,6 @@ const createMockChannel = (options: {
68
66
  unreadCount = 0,
69
67
  lastMessageAttachments,
70
68
  lastMessageMetadata,
71
- isCustomer = false,
72
- isStarred = false,
73
69
  } = options
74
70
 
75
71
  const eventListeners = new Map<string, Set<(...args: unknown[]) => void>>()
@@ -93,12 +89,8 @@ const createMockChannel = (options: {
93
89
  image: participantImage,
94
90
  },
95
91
  user_id: participantId,
96
- ...(isCustomer ? { customer: true } : {}),
97
92
  },
98
93
  },
99
- membership: {
100
- pinned_at: isStarred ? now().toISOString() : null,
101
- },
102
94
  messages:
103
95
  lastMessageText || lastMessageAttachments || lastMessageMetadata
104
96
  ? ([
@@ -257,61 +249,6 @@ ChatbotMessage.args = {
257
249
  }),
258
250
  }
259
251
 
260
- export const ParticipantBadges: StoryFn = () => {
261
- const channels = [
262
- createMockChannel({
263
- id: 'channel-customer',
264
- participantName: 'Alice Customer',
265
- participantId: 'participant-customer',
266
- participantImage: 'https://i.pravatar.cc/150?img=40',
267
- lastMessageText: 'I bought this from your Linktree',
268
- lastMessageTime: minutesAgo(2),
269
- isCustomer: true,
270
- }),
271
- createMockChannel({
272
- id: 'channel-starred',
273
- participantName: 'Bob Starred',
274
- participantId: 'participant-starred',
275
- participantImage: 'https://i.pravatar.cc/150?img=41',
276
- lastMessageText: 'Following up on this thread',
277
- lastMessageTime: minutesAgo(8),
278
- isStarred: true,
279
- }),
280
- createMockChannel({
281
- id: 'channel-customer-starred',
282
- participantName: 'Carol Customer',
283
- participantId: 'participant-customer-starred',
284
- participantImage: 'https://i.pravatar.cc/150?img=42',
285
- lastMessageText: 'Thanks for the quick reply',
286
- lastMessageTime: minutesAgo(15),
287
- isCustomer: true,
288
- isStarred: true,
289
- }),
290
- ]
291
-
292
- return (
293
- <div className="w-[360px] bg-chalk border border-sand rounded-lg overflow-hidden">
294
- {channels.map((channel) => (
295
- <CustomChannelPreview
296
- key={channel.id}
297
- channel={channel}
298
- onChannelSelect={(channel) => {
299
- console.log('Channel selected:', channel.id)
300
- }}
301
- />
302
- ))}
303
- </div>
304
- )
305
- }
306
- ParticipantBadges.parameters = {
307
- docs: {
308
- description: {
309
- story:
310
- 'Shows the built-in customer badge from participant.member customer data and the starred badge from the current user membership pinned_at value.',
311
- },
312
- },
313
- }
314
-
315
252
  export const TipMessage: StoryFn<ComponentProps> = Template.bind({})
316
253
  TipMessage.args = {
317
254
  channel: createMockChannel({
@@ -12,15 +12,12 @@ const mockUser = {
12
12
  }
13
13
 
14
14
  type MockChannel = Channel & {
15
- emitMemberUpdated: (member?: {
16
- pinned_at?: string | null
17
- user_id?: string
18
- }) => void
15
+ emitMemberUpdated: (member?: { pinned_at?: string | null }) => void
19
16
  }
20
17
 
21
18
  const createMockChannel = (messages: Partial<LocalMessage>[]): MockChannel => {
22
19
  const memberUpdatedListeners = new Set<
23
- (event: { member?: { pinned_at?: string | null; user_id?: string } }) => void
20
+ (event: { member?: { pinned_at?: string | null } }) => void
24
21
  >()
25
22
 
26
23
  return {
@@ -41,34 +38,24 @@ const createMockChannel = (messages: Partial<LocalMessage>[]): MockChannel => {
41
38
  on: vi.fn(
42
39
  (
43
40
  eventName: string,
44
- listener: (event: {
45
- member?: { pinned_at?: string | null; user_id?: string }
46
- }) => void
41
+ listener: (event: { member?: { pinned_at?: string | null } }) => void
47
42
  ) => {
48
43
  if (eventName === 'member.updated') {
49
44
  memberUpdatedListeners.add(listener)
50
45
  }
51
- return {
52
- unsubscribe: () => memberUpdatedListeners.delete(listener),
53
- }
54
46
  }
55
47
  ),
56
48
  off: vi.fn(
57
49
  (
58
50
  eventName: string,
59
- listener: (event: {
60
- member?: { pinned_at?: string | null; user_id?: string }
61
- }) => void
51
+ listener: (event: { member?: { pinned_at?: string | null } }) => void
62
52
  ) => {
63
53
  if (eventName === 'member.updated') {
64
54
  memberUpdatedListeners.delete(listener)
65
55
  }
66
56
  }
67
57
  ),
68
- emitMemberUpdated: (member?: {
69
- pinned_at?: string | null
70
- user_id?: string
71
- }) => {
58
+ emitMemberUpdated: (member?: { pinned_at?: string | null }) => {
72
59
  memberUpdatedListeners.forEach((listener) => listener({ member }))
73
60
  },
74
61
  } as unknown as MockChannel
@@ -266,21 +253,6 @@ describe('CustomChannelPreview', () => {
266
253
  expect(screen.getByText('No messages yet')).toBeInTheDocument()
267
254
  })
268
255
 
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
-
284
256
  it('updates starred state when the channel membership changes', () => {
285
257
  const channel = createMockChannel([])
286
258
 
@@ -289,37 +261,15 @@ describe('CustomChannelPreview', () => {
289
261
  )
290
262
 
291
263
  expect(
292
- screen.queryByRole('img', { name: 'Starred conversation' })
264
+ screen.queryByRole('heading', { name: /starred conversation/i })
293
265
  ).not.toBeInTheDocument()
294
266
 
295
267
  act(() => {
296
- channel.emitMemberUpdated({
297
- pinned_at: new Date().toISOString(),
298
- user_id: mockUser.id,
299
- })
268
+ channel.emitMemberUpdated({ pinned_at: new Date().toISOString() })
300
269
  })
301
270
 
302
271
  expect(
303
- screen.getByRole('img', { name: 'Starred conversation' })
272
+ screen.getByRole('heading', { name: /starred conversation/i })
304
273
  ).toBeInTheDocument()
305
274
  })
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
- })
325
275
  })
@@ -2,12 +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'
5
6
  import { formatRelativeTime } from '../../utils/formatRelativeTime'
6
7
  import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
7
8
  import { resolveParticipantDisplayName } from '../../utils/resolveParticipantDisplayName'
8
9
  import { Avatar } from '../Avatar'
9
10
  import { isChatbotMessage } from '../CustomMessage/MessageTag'
10
- import { ParticipantBadges } from '../ParticipantBadges'
11
11
 
12
12
  import { useChannelListContext } from './ChannelListContext'
13
13
 
@@ -103,6 +103,7 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
103
103
  const messagePreview = renderMessagePreview
104
104
  ? renderMessagePreview(lastMessage, lastMessageText)
105
105
  : `${isLastMessageFromChatbot ? '✨ ' : ''}${lastMessageText}`
106
+ const isChannelStarred = useChannelStar(channel)
106
107
 
107
108
  // Use the unread prop passed by Stream Chat (reactive and updates automatically)
108
109
  const unreadCount = unread ?? 0
@@ -138,18 +139,22 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
138
139
  name={participantName}
139
140
  image={participantImage}
140
141
  size={48}
142
+ starred={isChannelStarred}
141
143
  />
142
144
 
143
145
  {/* Content column */}
144
146
  <div className="flex-1 min-w-0 flex flex-col gap-1">
145
147
  {/* Name and timestamp row */}
146
148
  <div className="flex items-center justify-between gap-2">
147
- <h3 className="flex min-w-0 items-center gap-1 text-sm font-medium text-[#191918]">
148
- <span className="min-w-0 truncate">{participantName}</span>
149
- <ParticipantBadges
150
- channel={channel}
151
- participant={participant}
152
- />
149
+ <h3
150
+ className={classNames(
151
+ 'text-sm font-medium truncate text-[#191918]'
152
+ )}
153
+ >
154
+ {isChannelStarred && (
155
+ <span className="sr-only">Starred conversation. </span>
156
+ )}
157
+ {participantName}
153
158
  </h3>
154
159
  {lastMessageTime && (
155
160
  <span className="text-xs text-[#717070] flex-shrink-0">
@@ -41,14 +41,7 @@ const createMockChannel = async (
41
41
  client: StreamChat,
42
42
  hasMessages = true,
43
43
  followerStatus?: string | boolean,
44
- isFrozen = false,
45
- {
46
- isCustomer = false,
47
- isStarred = false,
48
- }: {
49
- isCustomer?: boolean
50
- isStarred?: boolean
51
- } = {}
44
+ isFrozen = false
52
45
  ) => {
53
46
  const participant = mockParticipants[0]
54
47
 
@@ -149,12 +142,8 @@ const createMockChannel = async (
149
142
  user_id: participant.id,
150
143
  role: 'member',
151
144
  is_account: true,
152
- ...(isCustomer ? { customer: true } : {}),
153
145
  },
154
146
  }
155
- channel.state.membership = {
156
- pinned_at: isStarred ? now().toISOString() : null,
157
- } as ChannelMemberResponse
158
147
  return {
159
148
  channel: channelData,
160
149
  members: [],
@@ -182,8 +171,6 @@ const createMockChannel = async (
182
171
  type TemplateProps = ComponentProps & {
183
172
  followerStatus?: string | boolean
184
173
  isFrozen?: boolean
185
- isCustomer?: boolean
186
- isStarred?: boolean
187
174
  typingUser?: {
188
175
  id: string
189
176
  name?: string
@@ -195,8 +182,6 @@ const Template: StoryFn<TemplateProps> = (args) => {
195
182
  const {
196
183
  followerStatus,
197
184
  isFrozen = false,
198
- isCustomer = false,
199
- isStarred = false,
200
185
  typingUser,
201
186
  ...channelViewProps
202
187
  } = args
@@ -212,13 +197,12 @@ const Template: StoryFn<TemplateProps> = (args) => {
212
197
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
213
198
 
214
199
  useEffect(() => {
215
- createMockChannel(client, true, followerStatus, isFrozen, {
216
- isCustomer,
217
- isStarred,
218
- }).then((mockChannel) => {
219
- setChannel(mockChannel)
220
- })
221
- }, [client, followerStatus, isCustomer, isFrozen, isStarred])
200
+ createMockChannel(client, true, followerStatus, isFrozen).then(
201
+ (mockChannel) => {
202
+ setChannel(mockChannel)
203
+ }
204
+ )
205
+ }, [client, followerStatus, isFrozen])
222
206
 
223
207
  useEffect(() => {
224
208
  if (!channel || !typingUser) {
@@ -323,21 +307,6 @@ WithHeaderTitleBadges.parameters = {
323
307
  },
324
308
  }
325
309
 
326
- export const ParticipantBadges: StoryFn<TemplateProps> = Template.bind({})
327
- ParticipantBadges.args = {
328
- showBackButton: false,
329
- isCustomer: true,
330
- isStarred: true,
331
- }
332
- ParticipantBadges.parameters = {
333
- docs: {
334
- description: {
335
- story:
336
- 'Shows the built-in customer badge from participant.customer and the starred badge from the current user membership pinned_at value in the channel header.',
337
- },
338
- },
339
- }
340
-
341
310
  export const RestrictedOfficialChannel: StoryFn<TemplateProps> = Template.bind(
342
311
  {}
343
312
  )
@@ -84,9 +84,13 @@ vi.mock('./Avatar', () => ({
84
84
  },
85
85
  }))
86
86
 
87
+ let mockIsStarred = false
88
+ vi.mock('../hooks/useChannelStar', () => ({
89
+ useChannelStar: () => mockIsStarred,
90
+ }))
91
+
87
92
  const createChannel = ({
88
93
  currentUserId = 'visitor-1',
89
- currentUserPinnedAt,
90
94
  currentUserRole = 'owner',
91
95
  otherUser = {
92
96
  id: 'linker-1' as const,
@@ -94,12 +98,9 @@ const createChannel = ({
94
98
  username: undefined as string | undefined,
95
99
  is_account: true,
96
100
  },
97
- otherMemberCustomer = false,
98
101
  }: {
99
102
  currentUserId?: 'visitor-1' | 'linker-1'
100
- currentUserPinnedAt?: string
101
103
  currentUserRole?: 'owner' | 'member'
102
- otherMemberCustomer?: boolean
103
104
  otherUser?: {
104
105
  id: string
105
106
  name?: string
@@ -132,15 +133,12 @@ const createChannel = ({
132
133
  otherUser.is_account ?? currentUserId === 'visitor-1',
133
134
  },
134
135
  role: currentUserRole === 'owner' ? 'member' : 'owner',
135
- customer: otherMemberCustomer,
136
136
  },
137
137
  },
138
- membership: {
139
- pinned_at: currentUserPinnedAt,
140
- },
138
+ membership: {},
141
139
  messages: [],
142
140
  },
143
- on: vi.fn(() => ({ unsubscribe: vi.fn() })),
141
+ on: vi.fn(),
144
142
  off: vi.fn(),
145
143
  sendMessage: vi.fn(),
146
144
  hide: vi.fn(),
@@ -155,6 +153,7 @@ describe('ChannelView', () => {
155
153
  avatarRenderCalls.length = 0
156
154
  channelActionsMenuProps.length = 0
157
155
  messageInputProps.length = 0
156
+ mockIsStarred = false
158
157
  })
159
158
 
160
159
  const lastActionsMenuProps = () =>
@@ -288,54 +287,38 @@ describe('ChannelView', () => {
288
287
  expect(screen.queryByTestId('conversation-footer')).not.toBeInTheDocument()
289
288
  })
290
289
 
291
- it('does not pass starred to avatar when channel is starred', () => {
292
- const channel = createChannel({
293
- currentUserPinnedAt: '2026-01-01T00:00:00.000Z',
294
- })
290
+ it('does not pass starred to avatar when showStarButton is false', () => {
291
+ mockIsStarred = true
292
+ const channel = createChannel()
295
293
 
296
294
  renderWithProviders(
297
- <ChannelView channel={channel} showStarButton={true} />
295
+ <ChannelView channel={channel} showStarButton={false} />
298
296
  )
299
297
 
300
298
  expect(avatarRenderCalls.length).toBeGreaterThan(0)
301
- expect(avatarRenderCalls.every((call) => !('starred' in call))).toBe(true)
299
+ expect(avatarRenderCalls.every((call) => call.starred === false)).toBe(true)
302
300
  })
303
301
 
304
- it('renders built-in customer badges from participant member data', () => {
302
+ it('passes starred to avatar when showStarButton is true and channel is starred', () => {
303
+ mockIsStarred = true
304
+ const channel = createChannel()
305
+
305
306
  renderWithProviders(
306
- <ChannelView
307
- channel={createChannel({ otherMemberCustomer: true })}
308
- renderHeaderTitleBadges={() => (
309
- <span data-testid="custom-header-badge">VIP</span>
310
- )}
311
- />
307
+ <ChannelView channel={channel} showStarButton={true} />
312
308
  )
313
309
 
314
- const customerBadges = screen.getAllByRole('img', { name: 'Customer' })
315
- const customBadges = screen.getAllByTestId('custom-header-badge')
316
-
317
- expect(customerBadges.length).toBeGreaterThan(0)
318
- expect(customBadges.length).toBe(customerBadges.length)
319
- customerBadges.forEach((badge, index) => {
320
- expect(
321
- badge.compareDocumentPosition(customBadges[index]) &
322
- Node.DOCUMENT_POSITION_FOLLOWING
323
- ).toBeTruthy()
324
- })
310
+ expect(avatarRenderCalls.length).toBeGreaterThan(0)
311
+ expect(avatarRenderCalls.every((call) => call.starred === true)).toBe(true)
325
312
  })
326
313
 
327
- it('renders built-in starred badges from channel star state', () => {
328
- renderWithProviders(
329
- <ChannelView
330
- channel={createChannel({
331
- currentUserPinnedAt: '2026-01-01T00:00:00.000Z',
332
- })}
333
- />
334
- )
314
+ it('does not pass starred to avatar by default (showStarButton defaults to false)', () => {
315
+ mockIsStarred = true
316
+ const channel = createChannel()
335
317
 
336
- expect(
337
- screen.getAllByRole('img', { name: 'Starred conversation' }).length
338
- ).toBeGreaterThan(0)
318
+ renderWithProviders(<ChannelView channel={channel} />)
319
+
320
+ expect(avatarRenderCalls.length).toBeGreaterThan(0)
321
+ expect(avatarRenderCalls.every((call) => call.starred === false)).toBe(true)
339
322
  })
340
323
 
341
324
  it('passes dmAgentEnabled to avatar when dmAgentEnabled is true for visitor view', () => {