@linktr.ee/messaging-react 3.4.0-rc-1782373728 → 3.4.0-rc-1782387968

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 (37) hide show
  1. package/dist/{Card-h43tRi_a.js → Card-BOD2Qo_b.js} +3 -3
  2. package/dist/{Card-h43tRi_a.js.map → Card-BOD2Qo_b.js.map} +1 -1
  3. package/dist/{Card-TNeK_nFK.js → Card-DcEBssw0.js} +2 -2
  4. package/dist/{Card-TNeK_nFK.js.map → Card-DcEBssw0.js.map} +1 -1
  5. package/dist/{Card-C3icBnXT.cjs → Card-DkNdLpVE.cjs} +2 -2
  6. package/dist/{Card-C3icBnXT.cjs.map → Card-DkNdLpVE.cjs.map} +1 -1
  7. package/dist/{Card-DVo2e4rF.cjs → Card-UbPYWjnX.cjs} +2 -2
  8. package/dist/{Card-DVo2e4rF.cjs.map → Card-UbPYWjnX.cjs.map} +1 -1
  9. package/dist/{Card-DGkh6_ti.js → Card-beoX9cug.js} +2 -2
  10. package/dist/{Card-DGkh6_ti.js.map → Card-beoX9cug.js.map} +1 -1
  11. package/dist/{Card-B9Z659Y-.cjs → Card-utKbCGom.cjs} +2 -2
  12. package/dist/{Card-B9Z659Y-.cjs.map → Card-utKbCGom.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-DjFuOEgv.cjs → LockedThumbnail-Bjdf-fTw.cjs} +2 -2
  14. package/dist/{LockedThumbnail-DjFuOEgv.cjs.map → LockedThumbnail-Bjdf-fTw.cjs.map} +1 -1
  15. package/dist/{LockedThumbnail-DE3KLtk1.js → LockedThumbnail-bOjjYYyV.js} +2 -2
  16. package/dist/{LockedThumbnail-DE3KLtk1.js.map → LockedThumbnail-bOjjYYyV.js.map} +1 -1
  17. package/dist/index-B3oQq9zK.cjs +2 -0
  18. package/dist/index-B3oQq9zK.cjs.map +1 -0
  19. package/dist/{index-DKq7HA0h.js → index-DG5LOHey.js} +905 -910
  20. package/dist/index-DG5LOHey.js.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.d.ts +28 -11
  23. package/dist/index.js +20 -19
  24. package/package.json +2 -2
  25. package/src/components/ChannelList/CustomChannelPreview.test.tsx +38 -0
  26. package/src/components/ChannelList/CustomChannelPreview.tsx +5 -6
  27. package/src/components/ChannelView.stories.tsx +21 -0
  28. package/src/components/ChannelView.test.tsx +35 -20
  29. package/src/components/ChannelView.tsx +28 -43
  30. package/src/components/MessagingShell/index.tsx +2 -0
  31. package/src/components/VerifiedBadge/index.tsx +25 -0
  32. package/src/index.ts +2 -0
  33. package/src/stream-custom-data.ts +8 -0
  34. package/src/types.ts +11 -11
  35. package/dist/index--bjxhW3D.cjs +0 -2
  36. package/dist/index--bjxhW3D.cjs.map +0 -1
  37. package/dist/index-DKq7HA0h.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index--bjxhW3D.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-B3oQq9zK.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;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Attachment } from 'stream-chat';
2
2
  import { Channel } from 'stream-chat';
3
3
  import { ChannelFilters } from 'stream-chat';
4
+ import { ChannelMemberResponse } from 'stream-chat';
4
5
  import { ChannelSort } from 'stream-chat';
5
6
  import { ComponentType } from 'react';
6
7
  import { default as default_2 } from 'react';
@@ -148,7 +149,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
148
149
  * Props that MessagingShell passes through to ChannelView.
149
150
  * ChannelViewProps is the source of truth for these props.
150
151
  */
151
- declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'renderMessage' | 'onMessageLinkClick' | 'showChannelInfo'>;
152
+ declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderHeaderTitleBadges' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'renderMessage' | 'onMessageLinkClick' | 'showChannelInfo'>;
152
153
 
153
154
  /**
154
155
  * ChannelView component props
@@ -250,6 +251,14 @@ export declare interface ChannelViewProps {
250
251
  * and filter by starred/pinned status.
251
252
  */
252
253
  showStarButton?: boolean;
254
+ /**
255
+ * Custom badges rendered inline after the participant name in the channel
256
+ * header title.
257
+ */
258
+ renderHeaderTitleBadges?: (context: {
259
+ channel: Channel;
260
+ participant?: ChannelMemberResponse;
261
+ }) => React.ReactNode;
253
262
  /**
254
263
  * Show the channel actions menu (the `...` popover) in the header, which
255
264
  * exposes the block/report/delete moderation actions. Defaults to true.
@@ -342,16 +351,6 @@ export declare interface ChannelViewProps {
342
351
  * Use this to render staged paid or media attachments inside the input.
343
352
  */
344
353
  attachmentPreviewList?: ComponentType;
345
- /**
346
- * Fired when the participant's name in the channel header is clicked. When
347
- * provided, the name renders as a button with a trailing caret (an affordance
348
- * for opening participant details / thread content); when omitted, the name
349
- * renders as plain text.
350
- *
351
- * @example
352
- * onParticipantNameClick={() => openThreadContentDialog()}
353
- */
354
- onParticipantNameClick?: () => void;
355
354
  }
356
355
 
357
356
  export declare interface ComposerCardProps extends LockedAttachmentBaseProps {
@@ -1390,6 +1389,17 @@ declare interface UseMessageVoteResult {
1390
1389
  */
1391
1390
  export declare const useMessaging: () => MessagingContextValue;
1392
1391
 
1392
+ /**
1393
+ * Linktree verification badge shown next to the name of a verified profile.
1394
+ * Rendered when a participant's Stream user has `is_verified` set to true.
1395
+ */
1396
+ export declare const VerifiedBadge: default_2.FC<VerifiedBadgeProps>;
1397
+
1398
+ export declare interface VerifiedBadgeProps {
1399
+ className?: string;
1400
+ size?: number;
1401
+ }
1402
+
1393
1403
  declare interface VideoAttachmentSharedProps extends MessageAttachmentBaseProps {
1394
1404
  /** Single video — convenience for the most common case. */
1395
1405
  src?: string;
@@ -1462,6 +1472,13 @@ declare module 'stream-chat' {
1462
1472
  */
1463
1473
  chatbot_paused?: boolean;
1464
1474
  }
1475
+ interface CustomUserData {
1476
+ /**
1477
+ * Whether the user is a verified Linktree profile. When true, a
1478
+ * verification badge is shown next to their name in messaging.
1479
+ */
1480
+ is_verified?: boolean;
1481
+ }
1465
1482
  interface CustomMessageData {
1466
1483
  /**
1467
1484
  * When true, hides the date timestamp in system messages.
package/dist/index.js CHANGED
@@ -1,34 +1,35 @@
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-DKq7HA0h.js";
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-DG5LOHey.js";
2
2
  export {
3
- e as ActionButton,
3
+ s as ActionButton,
4
4
  t as Avatar,
5
5
  i as ChannelEmptyState,
6
6
  n as ChannelList,
7
7
  o 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,
15
15
  c as MessageVoteButtons,
16
- h as MessagingProvider,
17
- d as MessagingShell,
18
- p as buildCompactMetaLabel,
19
- v as formatFileSize,
20
- A as formatRelativeTime,
21
- C as getFileExtensionLabel,
22
- F as getMessageDisplayText,
23
- k as isLinkAttachment,
24
- b as isUuidLike,
25
- f as messageAttachmentGroupPositionFromStream,
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,
23
+ k as getMessageDisplayText,
24
+ f as isLinkAttachment,
25
+ V as isUuidLike,
26
+ b as messageAttachmentGroupPositionFromStream,
26
27
  x as normalizeLanguageCode,
27
28
  y as resolveLinkAttachment,
28
- P as resolveMediaFromMessage,
29
- S as resolveParticipantDisplayName,
30
- q as useCustomMessage,
31
- z as useMessageVote,
32
- B as useMessaging
29
+ B as resolveMediaFromMessage,
30
+ P as resolveParticipantDisplayName,
31
+ S as useCustomMessage,
32
+ q as useMessageVote,
33
+ z 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.0-rc-1782373728",
3
+ "version": "3.4.0-rc-1782387968",
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-1782373728",
41
+ "@linktr.ee/messaging-core": "2.3.0-rc-1782387968",
42
42
  "@phosphor-icons/react": "^2.1.10"
43
43
  },
44
44
  "devDependencies": {
@@ -101,6 +101,44 @@ describe('CustomChannelPreview', () => {
101
101
  expect(screen.getByText('Unknown member')).toBeInTheDocument()
102
102
  })
103
103
 
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
+
104
142
  it('shows the latest non-system message when the last message is a system message', () => {
105
143
  const channel = createMockChannel([
106
144
  {
@@ -8,6 +8,7 @@ import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
8
8
  import { resolveParticipantDisplayName } from '../../utils/resolveParticipantDisplayName'
9
9
  import { Avatar } from '../Avatar'
10
10
  import { isChatbotMessage } from '../CustomMessage/MessageTag'
11
+ import { VerifiedBadge } from '../VerifiedBadge'
11
12
 
12
13
  import { useChannelListContext } from './ChannelListContext'
13
14
 
@@ -48,6 +49,7 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
48
49
  )
49
50
  const participantName = resolveParticipantDisplayName(participant?.user)
50
51
  const participantImage = participant?.user?.image
52
+ const isParticipantVerified = participant?.user?.is_verified === true
51
53
 
52
54
  // Get last non-system message for preview
53
55
  const lastMessage = (() => {
@@ -146,15 +148,12 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
146
148
  <div className="flex-1 min-w-0 flex flex-col gap-1">
147
149
  {/* Name and timestamp row */}
148
150
  <div className="flex items-center justify-between gap-2">
149
- <h3
150
- className={classNames(
151
- 'text-sm font-medium truncate text-[#191918]'
152
- )}
153
- >
151
+ <h3 className="flex min-w-0 items-center gap-1 text-sm font-medium text-[#191918]">
154
152
  {isChannelStarred && (
155
153
  <span className="sr-only">Starred conversation. </span>
156
154
  )}
157
- {participantName}
155
+ <span className="min-w-0 truncate">{participantName}</span>
156
+ {isParticipantVerified && <VerifiedBadge />}
158
157
  </h3>
159
158
  {lastMessageTime && (
160
159
  <span className="text-xs text-[#717070] flex-shrink-0">
@@ -270,6 +270,27 @@ DmAgentHeader.parameters = {
270
270
  },
271
271
  }
272
272
 
273
+ export const WithHeaderTitleBadges: StoryFn<TemplateProps> = Template.bind({})
274
+ WithHeaderTitleBadges.args = {
275
+ showBackButton: false,
276
+ renderHeaderTitleBadges: ({ participant }) => (
277
+ <span
278
+ aria-label={`${participant?.user?.name ?? 'Participant'} is a customer`}
279
+ className="flex size-4 items-center justify-center rounded-full bg-[#E8F8EF] text-[10px] font-semibold text-[#136C3A]"
280
+ >
281
+ $
282
+ </span>
283
+ ),
284
+ }
285
+ WithHeaderTitleBadges.parameters = {
286
+ docs: {
287
+ description: {
288
+ story:
289
+ 'Renders custom badges inline after the participant name in the channel header via renderHeaderTitleBadges.',
290
+ },
291
+ },
292
+ }
293
+
273
294
  export const RestrictedOfficialChannel: StoryFn<TemplateProps> = Template.bind(
274
295
  {}
275
296
  )
@@ -106,6 +106,7 @@ const createChannel = ({
106
106
  name?: string
107
107
  username?: string
108
108
  is_account?: boolean
109
+ is_verified?: boolean
109
110
  }
110
111
  } = {}) =>
111
112
  ({
@@ -193,26 +194,6 @@ describe('ChannelView', () => {
193
194
  expect(screen.getAllByText('Linker').length).toBeGreaterThanOrEqual(1)
194
195
  })
195
196
 
196
- it('renders the participant name as a button that fires onParticipantNameClick', () => {
197
- const onParticipantNameClick = vi.fn()
198
- renderWithProviders(
199
- <ChannelView
200
- channel={createChannel()}
201
- onParticipantNameClick={onParticipantNameClick}
202
- />
203
- )
204
-
205
- // Both the mobile and desktop header layouts render the button (one is
206
- // hidden via CSS), so there is at least one match.
207
- const buttons = screen.getAllByRole('button', {
208
- name: /view details for linker/i,
209
- })
210
- expect(buttons.length).toBeGreaterThanOrEqual(1)
211
-
212
- buttons[0].click()
213
- expect(onParticipantNameClick).toHaveBeenCalledTimes(1)
214
- })
215
-
216
197
  it('renders the actions menu by default', () => {
217
198
  renderWithProviders(<ChannelView channel={createChannel()} />)
218
199
 
@@ -475,6 +456,40 @@ describe('ChannelView', () => {
475
456
  expect(screen.getAllByText('Unknown member').length).toBeGreaterThan(0)
476
457
  })
477
458
 
459
+ it('shows the verification badge in the header when the participant is verified', () => {
460
+ renderWithProviders(
461
+ <ChannelView
462
+ channel={createChannel({
463
+ otherUser: {
464
+ id: 'linker-1',
465
+ name: 'Linker',
466
+ is_account: true,
467
+ is_verified: true,
468
+ },
469
+ })}
470
+ />
471
+ )
472
+
473
+ expect(screen.getAllByLabelText('Verified').length).toBeGreaterThan(0)
474
+ })
475
+
476
+ it('does not show the verification badge in the header when the participant is not verified', () => {
477
+ renderWithProviders(
478
+ <ChannelView
479
+ channel={createChannel({
480
+ otherUser: {
481
+ id: 'linker-1',
482
+ name: 'Linker',
483
+ is_account: true,
484
+ is_verified: false,
485
+ },
486
+ })}
487
+ />
488
+ )
489
+
490
+ expect(screen.queryByLabelText('Verified')).not.toBeInTheDocument()
491
+ })
492
+
478
493
  it('fires onMessageLinkClick with the href and undefined message when the link is not inside a message wrapper', () => {
479
494
  const handler = vi.fn()
480
495
  const { container } = renderWithProviders(
@@ -1,9 +1,4 @@
1
- import {
2
- ArrowLeftIcon,
3
- CaretRightIcon,
4
- SparkleIcon,
5
- StarIcon,
6
- } from '@phosphor-icons/react'
1
+ import { ArrowLeftIcon, SparkleIcon, StarIcon } from '@phosphor-icons/react'
7
2
  import classNames from 'classnames'
8
3
  import React, { useCallback, useEffect, useRef } from 'react'
9
4
  import { Channel as ChannelType } from 'stream-chat'
@@ -32,6 +27,7 @@ import CustomTypingIndicator from './CustomTypingIndicator'
32
27
  import { DmAgentEnabledContext } from './CustomTypingIndicator/DmAgentContext'
33
28
  import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
34
29
  import { LoadingState } from './MessagingShell/LoadingState'
30
+ import { VerifiedBadge } from './VerifiedBadge'
35
31
 
36
32
  const ICON_BTN_CLASS =
37
33
  'size-10 rounded-full hover:bg-[#E5E4E1] flex items-center justify-center transition-colors duration-150 focus-ring'
@@ -57,7 +53,7 @@ const CustomChannelHeader: React.FC<{
57
53
  customChannelActions?: React.ReactNode
58
54
  renderChannelActions?: React.ReactNode
59
55
  showChannelInfo?: boolean
60
- onParticipantNameClick?: () => void
56
+ renderHeaderTitleBadges?: ChannelViewProps['renderHeaderTitleBadges']
61
57
  }> = ({
62
58
  onBack,
63
59
  showBackButton,
@@ -74,7 +70,7 @@ const CustomChannelHeader: React.FC<{
74
70
  customChannelActions,
75
71
  renderChannelActions,
76
72
  showChannelInfo = true,
77
- onParticipantNameClick,
73
+ renderHeaderTitleBadges,
78
74
  }) => {
79
75
  const { channel } = useChannelStateContext()
80
76
 
@@ -90,7 +86,12 @@ const CustomChannelHeader: React.FC<{
90
86
 
91
87
  const participantName = resolveParticipantDisplayName(participant?.user)
92
88
  const participantImage = participant?.user?.image
89
+ const isParticipantVerified = participant?.user?.is_verified === true
93
90
  const isStarred = useChannelStar(channel)
91
+ const headerTitleBadges = renderHeaderTitleBadges?.({
92
+ channel,
93
+ participant,
94
+ })
94
95
 
95
96
  const handleStarClick = async () => {
96
97
  try {
@@ -134,21 +135,13 @@ const CustomChannelHeader: React.FC<{
134
135
  dmAgentEnabled={dmAgentEnabled}
135
136
  size={48}
136
137
  />
137
- {onParticipantNameClick ? (
138
- <button
139
- type="button"
140
- onClick={onParticipantNameClick}
141
- aria-label={`View details for ${participantName}`}
142
- 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"
143
- >
144
- <span className="truncate">{participantName}</span>
145
- <CaretRightIcon className="size-3 shrink-0" weight="bold" />
146
- </button>
147
- ) : (
148
- <p className="text-center text-xs font-medium text-black/90">
149
- {participantName}
150
- </p>
151
- )}
138
+ <p className="flex max-w-full items-center justify-center gap-1 text-center text-xs font-medium text-black/90">
139
+ <span className="min-w-0 truncate">{participantName}</span>
140
+ {isParticipantVerified && <VerifiedBadge />}
141
+ {headerTitleBadges && (
142
+ <span className="shrink-0">{headerTitleBadges}</span>
143
+ )}
144
+ </p>
152
145
  {dmAgentEnabled && (
153
146
  <div className="flex items-center gap-1 text-[10px] leading-3 text-black/55">
154
147
  <SparkleIcon className="size-3 shrink-0 text-black/55" />
@@ -221,21 +214,13 @@ const CustomChannelHeader: React.FC<{
221
214
  size={48}
222
215
  />
223
216
  <div className="min-w-0">
224
- {onParticipantNameClick ? (
225
- <button
226
- type="button"
227
- onClick={onParticipantNameClick}
228
- aria-label={`View details for ${participantName}`}
229
- className="flex min-w-0 max-w-full items-center gap-1 rounded font-medium text-black/90 transition-opacity hover:opacity-70"
230
- >
231
- <span className="truncate">{participantName}</span>
232
- <CaretRightIcon className="size-4 shrink-0" weight="bold" />
233
- </button>
234
- ) : (
235
- <h1 className="font-medium text-black/90 truncate">
236
- {participantName}
237
- </h1>
238
- )}
217
+ <h1 className="flex min-w-0 items-center gap-1 font-medium text-black/90">
218
+ <span className="min-w-0 truncate">{participantName}</span>
219
+ {isParticipantVerified && <VerifiedBadge />}
220
+ {headerTitleBadges && (
221
+ <span className="shrink-0">{headerTitleBadges}</span>
222
+ )}
223
+ </h1>
239
224
  {dmAgentEnabled && (
240
225
  <div className="mt-0.5 flex items-center gap-1 text-[10px] leading-3 text-black/55">
241
226
  <SparkleIcon className="size-3 shrink-0 text-black/55" />
@@ -311,6 +296,7 @@ const ChannelViewInner: React.FC<{
311
296
  showStarButton?: boolean
312
297
  chatbotVotingEnabled?: boolean
313
298
  renderChannelBanner?: () => React.ReactNode
299
+ renderHeaderTitleBadges?: ChannelViewProps['renderHeaderTitleBadges']
314
300
  customChannelActions?: React.ReactNode
315
301
  renderChannelActions?: React.ReactNode
316
302
  renderMessage?: (
@@ -320,7 +306,6 @@ const ChannelViewInner: React.FC<{
320
306
  dmAgentEnabled?: boolean
321
307
  viewerLanguage?: string
322
308
  showChannelInfo?: boolean
323
- onParticipantNameClick?: () => void
324
309
  }> = ({
325
310
  onBack,
326
311
  showBackButton,
@@ -340,13 +325,13 @@ const ChannelViewInner: React.FC<{
340
325
  showStarButton = false,
341
326
  chatbotVotingEnabled = false,
342
327
  renderChannelBanner,
328
+ renderHeaderTitleBadges,
343
329
  customChannelActions,
344
330
  renderChannelActions,
345
331
  renderMessage,
346
332
  dmAgentEnabled = false,
347
333
  viewerLanguage,
348
334
  showChannelInfo = true,
349
- onParticipantNameClick,
350
335
  }) => {
351
336
  const { channel } = useChannelStateContext()
352
337
 
@@ -428,7 +413,7 @@ const ChannelViewInner: React.FC<{
428
413
  onReportParticipantClick={onReportParticipantClick}
429
414
  customChannelActions={customChannelActions}
430
415
  renderChannelActions={renderChannelActions}
431
- onParticipantNameClick={onParticipantNameClick}
416
+ renderHeaderTitleBadges={renderHeaderTitleBadges}
432
417
  />
433
418
  </div>
434
419
 
@@ -498,6 +483,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
498
483
  showStarButton = false,
499
484
  chatbotVotingEnabled = false,
500
485
  renderChannelBanner,
486
+ renderHeaderTitleBadges,
501
487
  customChannelActions,
502
488
  renderChannelActions,
503
489
  renderMessage,
@@ -506,7 +492,6 @@ export const ChannelView = React.memo<ChannelViewProps>(
506
492
  attachmentPreviewList,
507
493
  viewerLanguage,
508
494
  showChannelInfo = true,
509
- onParticipantNameClick,
510
495
  }) => {
511
496
  // Custom send message handler that:
512
497
  // 1. Applies messageMetadata if provided
@@ -614,12 +599,12 @@ export const ChannelView = React.memo<ChannelViewProps>(
614
599
  dmAgentEnabled={dmAgentEnabled}
615
600
  chatbotVotingEnabled={chatbotVotingEnabled}
616
601
  renderChannelBanner={renderChannelBanner}
602
+ renderHeaderTitleBadges={renderHeaderTitleBadges}
617
603
  customChannelActions={customChannelActions}
618
604
  renderChannelActions={renderChannelActions}
619
605
  renderMessage={renderMessage}
620
606
  viewerLanguage={viewerLanguage}
621
607
  showChannelInfo={showChannelInfo}
622
- onParticipantNameClick={onParticipantNameClick}
623
608
  />
624
609
  </Channel>
625
610
  </DmAgentEnabledContext.Provider>
@@ -31,6 +31,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
31
31
  onMessageSent,
32
32
  chatbotVotingEnabled = false,
33
33
  viewerLanguage,
34
+ renderHeaderTitleBadges,
34
35
  renderChannelBanner,
35
36
  customChannelActions,
36
37
  renderChannelActions,
@@ -255,6 +256,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
255
256
  onMessageSent={onMessageSent}
256
257
  chatbotVotingEnabled={chatbotVotingEnabled}
257
258
  viewerLanguage={viewerLanguage}
259
+ renderHeaderTitleBadges={renderHeaderTitleBadges}
258
260
  customChannelActions={customChannelActions}
259
261
  renderChannelActions={renderChannelActions}
260
262
  renderMessage={renderMessage}
@@ -0,0 +1,25 @@
1
+ import { SealCheckIcon } from '@phosphor-icons/react'
2
+ import classNames from 'classnames'
3
+ import React from 'react'
4
+
5
+ export interface VerifiedBadgeProps {
6
+ className?: string
7
+ size?: number
8
+ }
9
+
10
+ /**
11
+ * Linktree verification badge shown next to the name of a verified profile.
12
+ * Rendered when a participant's Stream user has `is_verified` set to true.
13
+ */
14
+ export const VerifiedBadge: React.FC<VerifiedBadgeProps> = ({
15
+ className,
16
+ size = 16,
17
+ }) => (
18
+ <SealCheckIcon
19
+ weight="fill"
20
+ size={size}
21
+ className={classNames('shrink-0 text-[#1D9BF0]', className)}
22
+ aria-label="Verified"
23
+ role="img"
24
+ />
25
+ )
package/src/index.ts CHANGED
@@ -10,6 +10,8 @@ export { default as LockedAttachment } from './components/LockedAttachment'
10
10
  export { default as LinkAttachment } from './components/LinkAttachment'
11
11
  export { default as MessageAttachment } from './components/MessageAttachment'
12
12
  export { Avatar } from './components/Avatar'
13
+ export { VerifiedBadge } from './components/VerifiedBadge'
14
+ export type { VerifiedBadgeProps } from './components/VerifiedBadge'
13
15
  export { FaqList } from './components/FaqList'
14
16
  export { FaqListItem } from './components/FaqList/FaqListItem'
15
17
  export { ChannelEmptyState } from './components/MessagingShell/ChannelEmptyState'
@@ -98,6 +98,14 @@ declare module 'stream-chat' {
98
98
  chatbot_paused?: boolean
99
99
  }
100
100
 
101
+ interface CustomUserData {
102
+ /**
103
+ * Whether the user is a verified Linktree profile. When true, a
104
+ * verification badge is shown next to their name in messaging.
105
+ */
106
+ is_verified?: boolean
107
+ }
108
+
101
109
  interface CustomMessageData {
102
110
  /**
103
111
  * When true, hides the date timestamp in system messages.
package/src/types.ts CHANGED
@@ -6,6 +6,7 @@ import type { ComponentType } from 'react'
6
6
  import type {
7
7
  Channel,
8
8
  ChannelFilters,
9
+ ChannelMemberResponse,
9
10
  ChannelSort,
10
11
  LocalMessage,
11
12
  SendMessageAPIResponse,
@@ -209,6 +210,15 @@ export interface ChannelViewProps {
209
210
  */
210
211
  showStarButton?: boolean
211
212
 
213
+ /**
214
+ * Custom badges rendered inline after the participant name in the channel
215
+ * header title.
216
+ */
217
+ renderHeaderTitleBadges?: (context: {
218
+ channel: Channel
219
+ participant?: ChannelMemberResponse
220
+ }) => React.ReactNode
221
+
212
222
  /**
213
223
  * Show the channel actions menu (the `...` popover) in the header, which
214
224
  * exposes the block/report/delete moderation actions. Defaults to true.
@@ -313,17 +323,6 @@ export interface ChannelViewProps {
313
323
  * Use this to render staged paid or media attachments inside the input.
314
324
  */
315
325
  attachmentPreviewList?: ComponentType
316
-
317
- /**
318
- * Fired when the participant's name in the channel header is clicked. When
319
- * provided, the name renders as a button with a trailing caret (an affordance
320
- * for opening participant details / thread content); when omitted, the name
321
- * renders as plain text.
322
- *
323
- * @example
324
- * onParticipantNameClick={() => openThreadContentDialog()}
325
- */
326
- onParticipantNameClick?: () => void
327
326
  }
328
327
 
329
328
  /**
@@ -341,6 +340,7 @@ export type ChannelViewPassthroughProps = Pick<
341
340
  | 'onMessageSent'
342
341
  | 'chatbotVotingEnabled'
343
342
  | 'viewerLanguage'
343
+ | 'renderHeaderTitleBadges'
344
344
  | 'renderChannelBanner'
345
345
  | 'customChannelActions'
346
346
  | 'renderChannelActions'