@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
@@ -7,7 +7,8 @@ import { renderWithProviders } from '../../test/utils'
7
7
  import { ChannelList } from '.'
8
8
 
9
9
  const streamChannelListMock = vi.fn()
10
- const { InfiniteScrollMock } = vi.hoisted(() => ({
10
+ const { CustomChannelPreviewMock, InfiniteScrollMock } = vi.hoisted(() => ({
11
+ CustomChannelPreviewMock: () => null,
11
12
  InfiniteScrollMock: () => null,
12
13
  }))
13
14
 
@@ -24,7 +25,7 @@ vi.mock('../../providers/MessagingProvider', () => ({
24
25
  }))
25
26
 
26
27
  vi.mock('./CustomChannelPreview', () => ({
27
- default: () => null,
28
+ default: CustomChannelPreviewMock,
28
29
  }))
29
30
 
30
31
  describe('ChannelList', () => {
@@ -55,6 +56,28 @@ describe('ChannelList', () => {
55
56
  })
56
57
  })
57
58
 
59
+ it('uses the built-in channel preview by default', () => {
60
+ renderWithProviders(<ChannelList {...defaultProps} />)
61
+
62
+ expect(streamChannelListMock).toHaveBeenCalledOnce()
63
+ expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
64
+ Preview: CustomChannelPreviewMock,
65
+ })
66
+ })
67
+
68
+ it('passes a custom channel preview through to Stream ChannelList', () => {
69
+ const ChannelPreview = () => null
70
+
71
+ renderWithProviders(
72
+ <ChannelList {...defaultProps} channelPreview={ChannelPreview} />
73
+ )
74
+
75
+ expect(streamChannelListMock).toHaveBeenCalledOnce()
76
+ expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
77
+ Preview: ChannelPreview,
78
+ })
79
+ })
80
+
58
81
  it('passes allowNewMessagesFromUnfilteredChannels through to Stream ChannelList', () => {
59
82
  const props: React.ComponentProps<typeof ChannelList> = {
60
83
  ...defaultProps,
@@ -28,6 +28,7 @@ export const ChannelList = React.memo<ChannelListProps>(
28
28
  sort = DEFAULT_SORT,
29
29
  className,
30
30
  customEmptyStateIndicator,
31
+ channelPreview,
31
32
  renderMessagePreview,
32
33
  viewerLanguage,
33
34
  }) => {
@@ -97,7 +98,7 @@ export const ChannelList = React.memo<ChannelListProps>(
97
98
  }
98
99
  channelRenderFilterFn={wrappedChannelRenderFilterFn}
99
100
  Paginator={InfiniteScroll}
100
- Preview={CustomChannelPreview}
101
+ Preview={channelPreview ?? CustomChannelPreview}
101
102
  EmptyStateIndicator={customEmptyStateIndicator}
102
103
  />
103
104
  </ChannelListProvider>
@@ -41,7 +41,14 @@ const createMockChannel = async (
41
41
  client: StreamChat,
42
42
  hasMessages = true,
43
43
  followerStatus?: string | boolean,
44
- isFrozen = false
44
+ isFrozen = false,
45
+ {
46
+ isCustomer = false,
47
+ isStarred = false,
48
+ }: {
49
+ isCustomer?: boolean
50
+ isStarred?: boolean
51
+ } = {}
45
52
  ) => {
46
53
  const participant = mockParticipants[0]
47
54
 
@@ -142,8 +149,12 @@ const createMockChannel = async (
142
149
  user_id: participant.id,
143
150
  role: 'member',
144
151
  is_account: true,
152
+ ...(isCustomer ? { customer: true } : {}),
145
153
  },
146
154
  }
155
+ channel.state.membership = {
156
+ pinned_at: isStarred ? now().toISOString() : null,
157
+ } as ChannelMemberResponse
147
158
  return {
148
159
  channel: channelData,
149
160
  members: [],
@@ -171,6 +182,8 @@ const createMockChannel = async (
171
182
  type TemplateProps = ComponentProps & {
172
183
  followerStatus?: string | boolean
173
184
  isFrozen?: boolean
185
+ isCustomer?: boolean
186
+ isStarred?: boolean
174
187
  typingUser?: {
175
188
  id: string
176
189
  name?: string
@@ -182,6 +195,8 @@ const Template: StoryFn<TemplateProps> = (args) => {
182
195
  const {
183
196
  followerStatus,
184
197
  isFrozen = false,
198
+ isCustomer = false,
199
+ isStarred = false,
185
200
  typingUser,
186
201
  ...channelViewProps
187
202
  } = args
@@ -197,12 +212,13 @@ const Template: StoryFn<TemplateProps> = (args) => {
197
212
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
198
213
 
199
214
  useEffect(() => {
200
- createMockChannel(client, true, followerStatus, isFrozen).then(
201
- (mockChannel) => {
202
- setChannel(mockChannel)
203
- }
204
- )
205
- }, [client, followerStatus, isFrozen])
215
+ createMockChannel(client, true, followerStatus, isFrozen, {
216
+ isCustomer,
217
+ isStarred,
218
+ }).then((mockChannel) => {
219
+ setChannel(mockChannel)
220
+ })
221
+ }, [client, followerStatus, isCustomer, isFrozen, isStarred])
206
222
 
207
223
  useEffect(() => {
208
224
  if (!channel || !typingUser) {
@@ -251,18 +267,20 @@ Default.parameters = {
251
267
  },
252
268
  }
253
269
 
254
- export const ParticipantNameTrigger: StoryFn<TemplateProps> = Template.bind({})
255
- ParticipantNameTrigger.args = {
270
+ export const ParticipantIdentityTrigger: StoryFn<TemplateProps> = Template.bind(
271
+ {}
272
+ )
273
+ ParticipantIdentityTrigger.args = {
256
274
  showBackButton: false,
257
275
  onParticipantNameClick: () =>
258
276
  // eslint-disable-next-line no-alert
259
- window.alert('Participant name clicked — open Thread content'),
277
+ window.alert('Participant details clicked — open Thread content'),
260
278
  }
261
- ParticipantNameTrigger.parameters = {
279
+ ParticipantIdentityTrigger.parameters = {
262
280
  docs: {
263
281
  description: {
264
282
  story:
265
- 'Header with `onParticipantNameClick` set: the participant name renders as a button with a trailing caret (both the mobile and desktop header layouts). Click the name to fire the callback (shows an alert here). With the prop omitted (see Default) the name is plain, non-interactive text.',
283
+ '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.',
266
284
  },
267
285
  },
268
286
  }
@@ -307,6 +325,21 @@ WithHeaderTitleBadges.parameters = {
307
325
  },
308
326
  }
309
327
 
328
+ export const ParticipantBadges: StoryFn<TemplateProps> = Template.bind({})
329
+ ParticipantBadges.args = {
330
+ showBackButton: false,
331
+ isCustomer: true,
332
+ isStarred: true,
333
+ }
334
+ ParticipantBadges.parameters = {
335
+ docs: {
336
+ description: {
337
+ story:
338
+ '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.',
339
+ },
340
+ },
341
+ }
342
+
310
343
  export const RestrictedOfficialChannel: StoryFn<TemplateProps> = Template.bind(
311
344
  {}
312
345
  )
@@ -84,13 +84,9 @@ vi.mock('./Avatar', () => ({
84
84
  },
85
85
  }))
86
86
 
87
- let mockIsStarred = false
88
- vi.mock('../hooks/useChannelStar', () => ({
89
- useChannelStar: () => mockIsStarred,
90
- }))
91
-
92
87
  const createChannel = ({
93
88
  currentUserId = 'visitor-1',
89
+ currentUserPinnedAt,
94
90
  currentUserRole = 'owner',
95
91
  otherUser = {
96
92
  id: 'linker-1' as const,
@@ -98,15 +94,17 @@ const createChannel = ({
98
94
  username: undefined as string | undefined,
99
95
  is_account: true,
100
96
  },
97
+ otherMemberCustomer = false,
101
98
  }: {
102
99
  currentUserId?: 'visitor-1' | 'linker-1'
100
+ currentUserPinnedAt?: string
103
101
  currentUserRole?: 'owner' | 'member'
102
+ otherMemberCustomer?: boolean
104
103
  otherUser?: {
105
104
  id: string
106
105
  name?: string
107
106
  username?: string
108
107
  is_account?: boolean
109
- is_verified?: boolean
110
108
  }
111
109
  } = {}) =>
112
110
  ({
@@ -134,12 +132,15 @@ const createChannel = ({
134
132
  otherUser.is_account ?? currentUserId === 'visitor-1',
135
133
  },
136
134
  role: currentUserRole === 'owner' ? 'member' : 'owner',
135
+ customer: otherMemberCustomer,
137
136
  },
138
137
  },
139
- membership: {},
138
+ membership: {
139
+ pinned_at: currentUserPinnedAt,
140
+ },
140
141
  messages: [],
141
142
  },
142
- on: vi.fn(),
143
+ on: vi.fn(() => ({ unsubscribe: vi.fn() })),
143
144
  off: vi.fn(),
144
145
  sendMessage: vi.fn(),
145
146
  hide: vi.fn(),
@@ -154,7 +155,6 @@ describe('ChannelView', () => {
154
155
  avatarRenderCalls.length = 0
155
156
  channelActionsMenuProps.length = 0
156
157
  messageInputProps.length = 0
157
- mockIsStarred = false
158
158
  })
159
159
 
160
160
  const lastActionsMenuProps = () =>
@@ -192,6 +192,14 @@ describe('ChannelView', () => {
192
192
  screen.queryByRole('button', { name: /view info for/i })
193
193
  ).not.toBeInTheDocument()
194
194
  expect(screen.getAllByText('Linker').length).toBeGreaterThanOrEqual(1)
195
+
196
+ const avatars = screen.getAllByTestId('avatar')
197
+ expect(avatars.length).toBeGreaterThanOrEqual(1)
198
+ expect(avatars[0].parentElement).toHaveClass(
199
+ 'rounded-[12px]',
200
+ 'px-1',
201
+ 'py-0.5'
202
+ )
195
203
  })
196
204
 
197
205
  it('renders the participant name as a button that fires onParticipantNameClick', () => {
@@ -209,6 +217,18 @@ describe('ChannelView', () => {
209
217
  name: /view details for linker/i,
210
218
  })
211
219
  expect(buttons.length).toBeGreaterThanOrEqual(1)
220
+ expect(buttons[0]).toHaveClass(
221
+ 'appearance-none',
222
+ 'border-0',
223
+ 'bg-transparent',
224
+ 'rounded-[12px]',
225
+ 'px-1',
226
+ 'py-0.5'
227
+ )
228
+
229
+ const avatars = screen.getAllByTestId('avatar')
230
+ expect(avatars.length).toBeGreaterThanOrEqual(1)
231
+ expect(avatars[0].closest('button')).toBe(buttons[0])
212
232
 
213
233
  buttons[0].click()
214
234
  expect(onParticipantNameClick).toHaveBeenCalledTimes(1)
@@ -288,38 +308,54 @@ describe('ChannelView', () => {
288
308
  expect(screen.queryByTestId('conversation-footer')).not.toBeInTheDocument()
289
309
  })
290
310
 
291
- it('does not pass starred to avatar when showStarButton is false', () => {
292
- mockIsStarred = true
293
- const channel = createChannel()
311
+ it('does not pass starred to avatar when channel is starred', () => {
312
+ const channel = createChannel({
313
+ currentUserPinnedAt: '2026-01-01T00:00:00.000Z',
314
+ })
294
315
 
295
316
  renderWithProviders(
296
- <ChannelView channel={channel} showStarButton={false} />
317
+ <ChannelView channel={channel} showStarButton={true} />
297
318
  )
298
319
 
299
320
  expect(avatarRenderCalls.length).toBeGreaterThan(0)
300
- expect(avatarRenderCalls.every((call) => call.starred === false)).toBe(true)
321
+ expect(avatarRenderCalls.every((call) => !('starred' in call))).toBe(true)
301
322
  })
302
323
 
303
- it('passes starred to avatar when showStarButton is true and channel is starred', () => {
304
- mockIsStarred = true
305
- const channel = createChannel()
306
-
324
+ it('renders built-in customer badges from participant member data', () => {
307
325
  renderWithProviders(
308
- <ChannelView channel={channel} showStarButton={true} />
326
+ <ChannelView
327
+ channel={createChannel({ otherMemberCustomer: true })}
328
+ renderHeaderTitleBadges={() => (
329
+ <span data-testid="custom-header-badge">VIP</span>
330
+ )}
331
+ />
309
332
  )
310
333
 
311
- expect(avatarRenderCalls.length).toBeGreaterThan(0)
312
- expect(avatarRenderCalls.every((call) => call.starred === true)).toBe(true)
313
- })
334
+ const customerBadges = screen.getAllByRole('img', { name: 'Customer' })
335
+ const customBadges = screen.getAllByTestId('custom-header-badge')
314
336
 
315
- it('does not pass starred to avatar by default (showStarButton defaults to false)', () => {
316
- mockIsStarred = true
317
- const channel = createChannel()
337
+ expect(customerBadges.length).toBeGreaterThan(0)
338
+ expect(customBadges.length).toBe(customerBadges.length)
339
+ customerBadges.forEach((badge, index) => {
340
+ expect(
341
+ badge.compareDocumentPosition(customBadges[index]) &
342
+ Node.DOCUMENT_POSITION_FOLLOWING
343
+ ).toBeTruthy()
344
+ })
345
+ })
318
346
 
319
- renderWithProviders(<ChannelView channel={channel} />)
347
+ it('renders built-in starred badges from channel star state', () => {
348
+ renderWithProviders(
349
+ <ChannelView
350
+ channel={createChannel({
351
+ currentUserPinnedAt: '2026-01-01T00:00:00.000Z',
352
+ })}
353
+ />
354
+ )
320
355
 
321
- expect(avatarRenderCalls.length).toBeGreaterThan(0)
322
- expect(avatarRenderCalls.every((call) => call.starred === false)).toBe(true)
356
+ expect(
357
+ screen.getAllByRole('img', { name: 'Starred conversation' }).length
358
+ ).toBeGreaterThan(0)
323
359
  })
324
360
 
325
361
  it('passes dmAgentEnabled to avatar when dmAgentEnabled is true for visitor view', () => {
@@ -476,40 +512,6 @@ describe('ChannelView', () => {
476
512
  expect(screen.getAllByText('Unknown member').length).toBeGreaterThan(0)
477
513
  })
478
514
 
479
- it('shows the verification badge in the header when the participant is verified', () => {
480
- renderWithProviders(
481
- <ChannelView
482
- channel={createChannel({
483
- otherUser: {
484
- id: 'linker-1',
485
- name: 'Linker',
486
- is_account: true,
487
- is_verified: true,
488
- },
489
- })}
490
- />
491
- )
492
-
493
- expect(screen.getAllByLabelText('Verified').length).toBeGreaterThan(0)
494
- })
495
-
496
- it('does not show the verification badge in the header when the participant is not verified', () => {
497
- renderWithProviders(
498
- <ChannelView
499
- channel={createChannel({
500
- otherUser: {
501
- id: 'linker-1',
502
- name: 'Linker',
503
- is_account: true,
504
- is_verified: false,
505
- },
506
- })}
507
- />
508
- )
509
-
510
- expect(screen.queryByLabelText('Verified')).not.toBeInTheDocument()
511
- })
512
-
513
515
  it('fires onMessageLinkClick with the href and undefined message when the link is not inside a message wrapper', () => {
514
516
  const handler = vi.fn()
515
517
  const { container } = renderWithProviders(
@@ -32,7 +32,7 @@ import CustomTypingIndicator from './CustomTypingIndicator'
32
32
  import { DmAgentEnabledContext } from './CustomTypingIndicator/DmAgentContext'
33
33
  import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
34
34
  import { LoadingState } from './MessagingShell/LoadingState'
35
- import { VerifiedBadge } from './VerifiedBadge'
35
+ import { ParticipantBadges } from './ParticipantBadges'
36
36
 
37
37
  const ICON_BTN_CLASS =
38
38
  'size-10 rounded-full hover:bg-[#E5E4E1] flex items-center justify-center transition-colors duration-150 focus-ring'
@@ -93,12 +93,24 @@ const CustomChannelHeader: React.FC<{
93
93
 
94
94
  const participantName = resolveParticipantDisplayName(participant?.user)
95
95
  const participantImage = participant?.user?.image
96
- const isParticipantVerified = participant?.user?.is_verified === true
97
96
  const isStarred = useChannelStar(channel)
98
97
  const headerTitleBadges = renderHeaderTitleBadges?.({
99
98
  channel,
100
99
  participant,
101
100
  })
101
+ const mobileParticipantIdentityClassName =
102
+ 'flex max-w-full flex-col items-center gap-1 rounded-[12px] px-1 py-0.5 text-center text-xs font-medium text-black/90'
103
+ const desktopParticipantIdentityClassName =
104
+ 'flex min-w-0 max-w-full items-center gap-4 rounded-[12px] px-1 py-0.5 text-left text-black/90'
105
+ const participantTitle = (
106
+ <>
107
+ <span className="min-w-0 truncate">{participantName}</span>
108
+ <ParticipantBadges channel={channel} participant={participant} />
109
+ {headerTitleBadges && (
110
+ <span className="shrink-0">{headerTitleBadges}</span>
111
+ )}
112
+ </>
113
+ )
102
114
 
103
115
  const handleStarClick = async () => {
104
116
  try {
@@ -133,42 +145,60 @@ const CustomChannelHeader: React.FC<{
133
145
  </button>
134
146
  )}
135
147
  </div>
136
- <div className="flex flex-col gap-1 items-center">
137
- <Avatar
138
- id={participant?.user?.id || channel.id || 'unknown'}
139
- name={participantName}
140
- image={participantImage}
141
- starred={showStarButton && isStarred}
142
- dmAgentEnabled={dmAgentEnabled}
143
- size={48}
144
- />
148
+ <div
149
+ className="flex flex-col gap-1 items-center"
150
+ data-dd-privacy="mask"
151
+ >
145
152
  {onParticipantNameClick ? (
146
153
  <button
147
154
  type="button"
148
155
  onClick={onParticipantNameClick}
149
156
  aria-label={`View details for ${participantName}`}
150
- className="flex max-w-full items-center gap-0.5 rounded text-center text-xs font-medium text-black/90 transition-opacity hover:opacity-70"
157
+ className={classNames(
158
+ mobileParticipantIdentityClassName,
159
+ 'appearance-none border-0 bg-transparent transition-opacity hover:opacity-70 focus-ring focus-visible:outline-none'
160
+ )}
151
161
  >
152
- <span className="min-w-0 truncate">{participantName}</span>
153
- {isParticipantVerified && <VerifiedBadge />}
154
- {headerTitleBadges && (
155
- <span className="shrink-0">{headerTitleBadges}</span>
162
+ <Avatar
163
+ id={participant?.user?.id || channel.id || 'unknown'}
164
+ name={participantName}
165
+ image={participantImage}
166
+ dmAgentEnabled={dmAgentEnabled}
167
+ size={48}
168
+ />
169
+ <span className="flex max-w-full items-center gap-0.5">
170
+ {participantTitle}
171
+ <CaretRightIcon className="size-3 shrink-0" weight="bold" />
172
+ </span>
173
+ {dmAgentEnabled && (
174
+ <div className="flex items-center gap-1 text-[10px] leading-3 text-black/55">
175
+ <SparkleIcon className="size-3 shrink-0 text-black/55" />
176
+ <span>{DM_AGENT_HEADER_HELPER_TEXT}</span>
177
+ </div>
156
178
  )}
157
- <CaretRightIcon className="size-3 shrink-0" weight="bold" />
158
179
  </button>
159
180
  ) : (
160
- <p className="flex max-w-full items-center justify-center gap-1 text-center text-xs font-medium text-black/90">
161
- <span className="min-w-0 truncate">{participantName}</span>
162
- {isParticipantVerified && <VerifiedBadge />}
163
- {headerTitleBadges && (
164
- <span className="shrink-0">{headerTitleBadges}</span>
181
+ <div className={mobileParticipantIdentityClassName}>
182
+ <Avatar
183
+ id={participant?.user?.id || channel.id || 'unknown'}
184
+ name={participantName}
185
+ image={participantImage}
186
+ dmAgentEnabled={dmAgentEnabled}
187
+ size={48}
188
+ />
189
+ <p className="flex max-w-full items-center justify-center gap-0.5">
190
+ {participantTitle}
191
+ <CaretRightIcon
192
+ className="invisible size-3 shrink-0"
193
+ weight="bold"
194
+ />
195
+ </p>
196
+ {dmAgentEnabled && (
197
+ <div className="flex items-center gap-1 text-[10px] leading-3 text-black/55">
198
+ <SparkleIcon className="size-3 shrink-0 text-black/55" />
199
+ <span>{DM_AGENT_HEADER_HELPER_TEXT}</span>
200
+ </div>
165
201
  )}
166
- </p>
167
- )}
168
- {dmAgentEnabled && (
169
- <div className="flex items-center gap-1 text-[10px] leading-3 text-black/55">
170
- <SparkleIcon className="size-3 shrink-0 text-black/55" />
171
- <span>{DM_AGENT_HEADER_HELPER_TEXT}</span>
172
202
  </div>
173
203
  )}
174
204
  </div>
@@ -213,7 +243,7 @@ const CustomChannelHeader: React.FC<{
213
243
  </div>
214
244
  </div>
215
245
  <div className="px-6 py-3 hidden @lg:flex items-center justify-between gap-3 min-h-12 border-b border-b-black/[0.08]">
216
- <div className="flex items-center gap-4 min-w-0">
246
+ <div className="flex items-center gap-4 min-w-0" data-dd-privacy="mask">
217
247
  {showBackButton && onBack && (
218
248
  <button
219
249
  className={classNames(
@@ -227,46 +257,66 @@ const CustomChannelHeader: React.FC<{
227
257
  <ArrowLeftIcon className="size-5 text-black/90" />
228
258
  </button>
229
259
  )}
230
- {/* Avatar */}
231
- <Avatar
232
- id={participant?.user?.id || channel.id || 'unknown'}
233
- name={participantName}
234
- image={participantImage}
235
- starred={showStarButton && isStarred}
236
- dmAgentEnabled={dmAgentEnabled}
237
- size={48}
238
- />
239
- <div className="min-w-0">
240
- {onParticipantNameClick ? (
241
- <button
242
- type="button"
243
- onClick={onParticipantNameClick}
244
- aria-label={`View details for ${participantName}`}
245
- className="flex min-w-0 max-w-full items-center gap-1 rounded font-medium text-black/90 transition-opacity hover:opacity-70"
246
- >
247
- <span className="min-w-0 truncate">{participantName}</span>
248
- {isParticipantVerified && <VerifiedBadge />}
249
- {headerTitleBadges && (
250
- <span className="shrink-0">{headerTitleBadges}</span>
260
+ {onParticipantNameClick ? (
261
+ <button
262
+ type="button"
263
+ onClick={onParticipantNameClick}
264
+ aria-label={`View details for ${participantName}`}
265
+ className={classNames(
266
+ desktopParticipantIdentityClassName,
267
+ 'appearance-none border-0 bg-transparent transition-opacity hover:opacity-70 focus-ring focus-visible:outline-none'
268
+ )}
269
+ >
270
+ <Avatar
271
+ id={participant?.user?.id || channel.id || 'unknown'}
272
+ name={participantName}
273
+ image={participantImage}
274
+ dmAgentEnabled={dmAgentEnabled}
275
+ size={48}
276
+ />
277
+ <div className="min-w-0">
278
+ <h1 className="flex min-w-0 items-center gap-1 font-medium text-black/90">
279
+ {participantTitle}
280
+ <CaretRightIcon className="size-4 shrink-0" weight="bold" />
281
+ </h1>
282
+ {dmAgentEnabled && (
283
+ <div className="mt-0.5 flex items-center gap-1 text-[10px] leading-3 text-black/55">
284
+ <SparkleIcon className="size-3 shrink-0 text-black/55" />
285
+ <span className="truncate">
286
+ {DM_AGENT_HEADER_HELPER_TEXT}
287
+ </span>
288
+ </div>
251
289
  )}
252
- <CaretRightIcon className="size-4 shrink-0" weight="bold" />
253
- </button>
254
- ) : (
255
- <h1 className="flex min-w-0 items-center gap-1 font-medium text-black/90">
256
- <span className="min-w-0 truncate">{participantName}</span>
257
- {isParticipantVerified && <VerifiedBadge />}
258
- {headerTitleBadges && (
259
- <span className="shrink-0">{headerTitleBadges}</span>
290
+ </div>
291
+ </button>
292
+ ) : (
293
+ <div className={desktopParticipantIdentityClassName}>
294
+ <Avatar
295
+ id={participant?.user?.id || channel.id || 'unknown'}
296
+ name={participantName}
297
+ image={participantImage}
298
+ dmAgentEnabled={dmAgentEnabled}
299
+ size={48}
300
+ />
301
+ <div className="min-w-0">
302
+ <h1 className="flex min-w-0 items-center gap-1 font-medium text-black/90">
303
+ {participantTitle}
304
+ <CaretRightIcon
305
+ className="invisible size-4 shrink-0"
306
+ weight="bold"
307
+ />
308
+ </h1>
309
+ {dmAgentEnabled && (
310
+ <div className="mt-0.5 flex items-center gap-1 text-[10px] leading-3 text-black/55">
311
+ <SparkleIcon className="size-3 shrink-0 text-black/55" />
312
+ <span className="truncate">
313
+ {DM_AGENT_HEADER_HELPER_TEXT}
314
+ </span>
315
+ </div>
260
316
  )}
261
- </h1>
262
- )}
263
- {dmAgentEnabled && (
264
- <div className="mt-0.5 flex items-center gap-1 text-[10px] leading-3 text-black/55">
265
- <SparkleIcon className="size-3 shrink-0 text-black/55" />
266
- <span className="truncate">{DM_AGENT_HEADER_HELPER_TEXT}</span>
267
317
  </div>
268
- )}
269
- </div>
318
+ </div>
319
+ )}
270
320
  </div>
271
321
  <div className="flex items-center gap-2">
272
322
  {showStarButton && (
@@ -346,6 +396,7 @@ const ChannelViewInner: React.FC<{
346
396
  viewerLanguage?: string
347
397
  showChannelInfo?: boolean
348
398
  onParticipantNameClick?: () => void
399
+ composerInput?: ChannelViewProps['composerInput']
349
400
  }> = ({
350
401
  onBack,
351
402
  showBackButton,
@@ -373,6 +424,7 @@ const ChannelViewInner: React.FC<{
373
424
  viewerLanguage,
374
425
  showChannelInfo = true,
375
426
  onParticipantNameClick,
427
+ composerInput,
376
428
  }) => {
377
429
  const { channel } = useChannelStateContext()
378
430
 
@@ -394,10 +446,10 @@ const ChannelViewInner: React.FC<{
394
446
  }, [channel._client?.userID, channel.state?.members])
395
447
 
396
448
  const currentUserIsAccount =
397
- (currentMember?.user as { is_account?: boolean } | undefined)?.is_account ??
449
+ currentMember?.user?.is_account ??
398
450
  (currentMember as { is_account?: boolean } | undefined)?.is_account
399
451
  const participantIsAccount =
400
- (participant?.user as { is_account?: boolean } | undefined)?.is_account ??
452
+ participant?.user?.is_account ??
401
453
  (participant as { is_account?: boolean } | undefined)?.is_account
402
454
 
403
455
  const showDmAgentHeader =
@@ -489,6 +541,7 @@ const ChannelViewInner: React.FC<{
489
541
  renderFooter={() => renderMessageInputFooter?.(channel)}
490
542
  disabled={composerDisabled}
491
543
  disabledReason={composerDisabledReason}
544
+ composerInput={composerInput}
492
545
  />
493
546
  </Window>
494
547
  </WithComponents>
@@ -535,6 +588,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
535
588
  viewerLanguage,
536
589
  showChannelInfo = true,
537
590
  onParticipantNameClick,
591
+ composerInput,
538
592
  }) => {
539
593
  // Custom send message handler that:
540
594
  // 1. Applies messageMetadata if provided
@@ -649,6 +703,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
649
703
  viewerLanguage={viewerLanguage}
650
704
  showChannelInfo={showChannelInfo}
651
705
  onParticipantNameClick={onParticipantNameClick}
706
+ composerInput={composerInput}
652
707
  />
653
708
  </Channel>
654
709
  </DmAgentEnabledContext.Provider>