@linktr.ee/messaging-react 3.5.0-rc-1782480113 → 3.5.0-rc-1782792958

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 (46) hide show
  1. package/dist/{Card-Dy1Qj3MR.js → Card-BWWBaNKF.js} +2 -2
  2. package/dist/{Card-Dy1Qj3MR.js.map → Card-BWWBaNKF.js.map} +1 -1
  3. package/dist/{Card-DhAhEetQ.cjs → Card-Bj4YaHav.cjs} +2 -2
  4. package/dist/{Card-DhAhEetQ.cjs.map → Card-Bj4YaHav.cjs.map} +1 -1
  5. package/dist/{Card-D34G7gvm.js → Card-BnGeMt4f.js} +2 -2
  6. package/dist/{Card-D34G7gvm.js.map → Card-BnGeMt4f.js.map} +1 -1
  7. package/dist/{Card-DVDoFfkP.js → Card-DkXzpFLA.js} +3 -3
  8. package/dist/{Card-DVDoFfkP.js.map → Card-DkXzpFLA.js.map} +1 -1
  9. package/dist/{Card-DIFgYVKx.cjs → Card-DpCwDviq.cjs} +2 -2
  10. package/dist/{Card-DIFgYVKx.cjs.map → Card-DpCwDviq.cjs.map} +1 -1
  11. package/dist/{Card-yHlBBfOt.cjs → Card-qrDaFHtJ.cjs} +2 -2
  12. package/dist/{Card-yHlBBfOt.cjs.map → Card-qrDaFHtJ.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-1S_Rjelj.js → LockedThumbnail-DJR--PAi.js} +2 -2
  14. package/dist/{LockedThumbnail-1S_Rjelj.js.map → LockedThumbnail-DJR--PAi.js.map} +1 -1
  15. package/dist/{LockedThumbnail-P2BVsLli.cjs → LockedThumbnail-DhXEk-NX.cjs} +2 -2
  16. package/dist/{LockedThumbnail-P2BVsLli.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-CgczmQkU.js → index-ubCRSgQk.js} +1161 -1074
  20. package/dist/index-ubCRSgQk.js.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.d.ts +15 -15
  23. package/dist/index.js +17 -18
  24. package/package.json +2 -2
  25. package/src/components/ChannelList/ChannelList.stories.tsx +42 -3
  26. package/src/components/ChannelList/CustomChannelPreview.stories.tsx +64 -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 +118 -68
  34. package/src/components/CustomMessage/index.tsx +1 -0
  35. package/src/components/CustomMessageInput/index.tsx +4 -1
  36. package/src/components/MediaMessage/index.tsx +13 -5
  37. package/src/components/ParticipantBadges.tsx +42 -0
  38. package/src/hooks/useChannelStar.ts +9 -6
  39. package/src/index.ts +0 -2
  40. package/src/stories/mocks.tsx +30 -5
  41. package/src/stream-custom-data.ts +12 -5
  42. package/src/types.ts +9 -1
  43. package/dist/index-BZjaMDgI.cjs +0 -2
  44. package/dist/index-BZjaMDgI.cjs.map +0 -1
  45. package/dist/index-CgczmQkU.js.map +0 -1
  46. package/src/components/VerifiedBadge/index.tsx +0 -25
@@ -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 && (
@@ -396,10 +446,10 @@ const ChannelViewInner: React.FC<{
396
446
  }, [channel._client?.userID, channel.state?.members])
397
447
 
398
448
  const currentUserIsAccount =
399
- (currentMember?.user as { is_account?: boolean } | undefined)?.is_account ??
449
+ currentMember?.user?.is_account ??
400
450
  (currentMember as { is_account?: boolean } | undefined)?.is_account
401
451
  const participantIsAccount =
402
- (participant?.user as { is_account?: boolean } | undefined)?.is_account ??
452
+ participant?.user?.is_account ??
403
453
  (participant as { is_account?: boolean } | undefined)?.is_account
404
454
 
405
455
  const showDmAgentHeader =
@@ -198,6 +198,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
198
198
  className={rootClassName}
199
199
  key={message.id}
200
200
  data-message-id={message.id}
201
+ data-dd-privacy="mask"
201
202
  >
202
203
  {PinIndicator && <PinIndicator />}
203
204
  {!!reminder && <ReminderNotification reminder={reminder} />}
@@ -61,7 +61,10 @@ const CustomMessageInputInner: React.FC = () => {
61
61
  } = useComponentContext('CustomMessageInput')
62
62
 
63
63
  return (
64
- <div 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)]">
64
+ <div
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)]"
66
+ data-dd-privacy="mask"
67
+ >
65
68
  <QuotedMessagePreview />
66
69
  <CustomLinkPreviewList />
67
70
  <AttachmentPreviewList />
@@ -302,7 +302,7 @@ const MediaMessageRoot: React.FC<MediaMessageProps> = ({
302
302
  : 'str-chat__message str-chat__message-simple str-chat__message--other'
303
303
 
304
304
  return (
305
- <div className={messageClass}>
305
+ <div className={messageClass} data-dd-privacy="mask">
306
306
  {!isMyMessage && message.user && (
307
307
  <Avatar
308
308
  className="str-chat__avatar str-chat__message-sender-avatar"
@@ -342,14 +342,18 @@ const MediaMessageCreatorEntry: React.FC<{ message: LocalMessage }> = ({
342
342
  const linkAttachment = resolveLinkAttachment(message)
343
343
  if (linkAttachment) {
344
344
  return (
345
- <div className={linkCardShellClass(true)}>
345
+ <div className={linkCardShellClass(true)} data-dd-privacy="mask">
346
346
  <LinkCard attachment={linkAttachment} isMyMessage={true} />
347
347
  </div>
348
348
  )
349
349
  }
350
350
  const resolved = resolveMediaFromMessage(message)
351
351
  if (!resolved) return null
352
- return <Creator {...resolved} />
352
+ return (
353
+ <div style={{ display: 'contents' }} data-dd-privacy="mask">
354
+ <Creator {...resolved} />
355
+ </div>
356
+ )
353
357
  }
354
358
 
355
359
  const MediaMessageVisitorEntry: React.FC<{ message: LocalMessage }> = ({
@@ -358,14 +362,18 @@ const MediaMessageVisitorEntry: React.FC<{ message: LocalMessage }> = ({
358
362
  const linkAttachment = resolveLinkAttachment(message)
359
363
  if (linkAttachment) {
360
364
  return (
361
- <div className={linkCardShellClass(false)}>
365
+ <div className={linkCardShellClass(false)} data-dd-privacy="mask">
362
366
  <LinkCard attachment={linkAttachment} isMyMessage={false} />
363
367
  </div>
364
368
  )
365
369
  }
366
370
  const resolved = resolveMediaFromMessage(message)
367
371
  if (!resolved) return null
368
- return <Visitor {...resolved} />
372
+ return (
373
+ <div style={{ display: 'contents' }} data-dd-privacy="mask">
374
+ <Visitor {...resolved} />
375
+ </div>
376
+ )
369
377
  }
370
378
 
371
379
  export const MediaMessage = Object.assign(MediaMessageRoot, {