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

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 (38) hide show
  1. package/dist/{Card-mpVcQ8-W.js → Card-BNzC_bp8.js} +3 -3
  2. package/dist/{Card-mpVcQ8-W.js.map → Card-BNzC_bp8.js.map} +1 -1
  3. package/dist/{Card-BKalDyyu.js → Card-CUHEDe8O.js} +2 -2
  4. package/dist/{Card-BKalDyyu.js.map → Card-CUHEDe8O.js.map} +1 -1
  5. package/dist/{Card-DFy_ZvFH.cjs → Card-DEa50m9p.cjs} +2 -2
  6. package/dist/{Card-DFy_ZvFH.cjs.map → Card-DEa50m9p.cjs.map} +1 -1
  7. package/dist/{Card-V-bNUxcw.cjs → Card-DJtDI03Y.cjs} +2 -2
  8. package/dist/{Card-V-bNUxcw.cjs.map → Card-DJtDI03Y.cjs.map} +1 -1
  9. package/dist/{Card-DxroYNrS.cjs → Card-DuMr5KMP.cjs} +2 -2
  10. package/dist/{Card-DxroYNrS.cjs.map → Card-DuMr5KMP.cjs.map} +1 -1
  11. package/dist/{Card-CphU_jG-.js → Card-iekRe643.js} +2 -2
  12. package/dist/{Card-CphU_jG-.js.map → Card-iekRe643.js.map} +1 -1
  13. package/dist/{LockedThumbnail-BD7qtcso.js → LockedThumbnail-CvpRZFi-.js} +2 -2
  14. package/dist/{LockedThumbnail-BD7qtcso.js.map → LockedThumbnail-CvpRZFi-.js.map} +1 -1
  15. package/dist/{LockedThumbnail-B3W7upHs.cjs → LockedThumbnail-aME0f_ZF.cjs} +2 -2
  16. package/dist/{LockedThumbnail-B3W7upHs.cjs.map → LockedThumbnail-aME0f_ZF.cjs.map} +1 -1
  17. package/dist/index-BIWS3Inh.cjs +2 -0
  18. package/dist/index-BIWS3Inh.cjs.map +1 -0
  19. package/dist/{index-CjwQ2Hj3.js → index-BlpDX5fl.js} +1369 -1367
  20. package/dist/index-BlpDX5fl.js.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.d.ts +19 -25
  23. package/dist/index.js +19 -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.test.tsx +35 -0
  28. package/src/components/ChannelView.tsx +6 -5
  29. package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +0 -34
  30. package/src/components/CustomMessageInput/index.tsx +1 -23
  31. package/src/components/MessagingShell/index.tsx +0 -2
  32. package/src/components/VerifiedBadge/index.tsx +25 -0
  33. package/src/index.ts +2 -1
  34. package/src/stream-custom-data.ts +8 -0
  35. package/src/types.ts +0 -18
  36. package/dist/index-CjwQ2Hj3.js.map +0 -1
  37. package/dist/index-tI-4cxV1.cjs +0 -2
  38. package/dist/index-tI-4cxV1.cjs.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-tI-4cxV1.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;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-BIWS3Inh.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.VerifiedBadge=e.VerifiedBadge;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
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' | 'composerInput'>;
152
+ declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderHeaderTitleBadges' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'showChannelInfo'>;
153
153
 
154
154
  /**
155
155
  * ChannelView component props
@@ -351,22 +351,6 @@ 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;
370
354
  /**
371
355
  * Fired when the participant's name in the channel header is clicked. When
372
356
  * provided, the name renders as a button with a trailing caret (an affordance
@@ -1392,14 +1376,6 @@ export declare interface SentCardProps extends LockedAttachmentBaseProps {
1392
1376
  onFetchSource?: () => Promise<LockedAttachmentSource | void>;
1393
1377
  }
1394
1378
 
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
-
1403
1379
  export declare function useCustomMessage<K extends keyof CustomMessageRegistry>(key: K): CustomMessageRegistry[K];
1404
1380
 
1405
1381
  /**
@@ -1423,6 +1399,17 @@ declare interface UseMessageVoteResult {
1423
1399
  */
1424
1400
  export declare const useMessaging: () => MessagingContextValue;
1425
1401
 
1402
+ /**
1403
+ * Linktree verification badge shown next to the name of a verified profile.
1404
+ * Rendered when a participant's Stream user has `is_verified` set to true.
1405
+ */
1406
+ export declare const VerifiedBadge: default_2.FC<VerifiedBadgeProps>;
1407
+
1408
+ export declare interface VerifiedBadgeProps {
1409
+ className?: string;
1410
+ size?: number;
1411
+ }
1412
+
1426
1413
  declare interface VideoAttachmentSharedProps extends MessageAttachmentBaseProps {
1427
1414
  /** Single video — convenience for the most common case. */
1428
1415
  src?: string;
@@ -1495,6 +1482,13 @@ declare module 'stream-chat' {
1495
1482
  */
1496
1483
  chatbot_paused?: boolean;
1497
1484
  }
1485
+ interface CustomUserData {
1486
+ /**
1487
+ * Whether the user is a verified Linktree profile. When true, a
1488
+ * verification badge is shown next to their name in messaging.
1489
+ */
1490
+ is_verified?: boolean;
1491
+ }
1498
1492
  interface CustomMessageData {
1499
1493
  /**
1500
1494
  * When true, hides the date timestamp in system messages.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
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-CjwQ2Hj3.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-BlpDX5fl.js";
2
2
  export {
3
- e as ActionButton,
3
+ s as ActionButton,
4
4
  t as Avatar,
5
5
  i as ChannelEmptyState,
6
- o as ChannelList,
7
- n as ChannelView,
6
+ n as ChannelList,
7
+ o as ChannelView,
8
8
  m as CustomMessageProvider,
9
9
  g as FaqList,
10
10
  r as FaqListItem,
@@ -13,23 +13,23 @@ export {
13
13
  u as MediaMessage,
14
14
  L as MessageAttachment,
15
15
  c as MessageVoteButtons,
16
- h as MessagingProvider,
17
- d as MessagingShell,
18
- p as buildCompactMetaLabel,
19
- C as formatFileSize,
20
- v as formatRelativeTime,
21
- A as getFileExtensionLabel,
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,
22
23
  k as getMessageDisplayText,
23
- F as isLinkAttachment,
24
- b as isUuidLike,
25
- f as messageAttachmentGroupPositionFromStream,
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 useComposerLocked,
31
- z as useCustomMessage,
32
- B as useMessageVote,
33
- D as useMessaging
29
+ B as resolveMediaFromMessage,
30
+ P as resolveParticipantDisplayName,
31
+ S as useCustomMessage,
32
+ q as useMessageVote,
33
+ z as useMessaging
34
34
  };
35
35
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.4.2-rc-1782395074",
3
+ "version": "3.5.0-rc-1782408966",
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-1782395074",
41
+ "@linktr.ee/messaging-core": "2.3.0-rc-1782408966",
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">
@@ -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
  ({
@@ -475,6 +476,40 @@ describe('ChannelView', () => {
475
476
  expect(screen.getAllByText('Unknown member').length).toBeGreaterThan(0)
476
477
  })
477
478
 
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
+
478
513
  it('fires onMessageLinkClick with the href and undefined message when the link is not inside a message wrapper', () => {
479
514
  const handler = vi.fn()
480
515
  const { container } = renderWithProviders(
@@ -32,6 +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
36
 
36
37
  const ICON_BTN_CLASS =
37
38
  'size-10 rounded-full hover:bg-[#E5E4E1] flex items-center justify-center transition-colors duration-150 focus-ring'
@@ -92,6 +93,7 @@ const CustomChannelHeader: React.FC<{
92
93
 
93
94
  const participantName = resolveParticipantDisplayName(participant?.user)
94
95
  const participantImage = participant?.user?.image
96
+ const isParticipantVerified = participant?.user?.is_verified === true
95
97
  const isStarred = useChannelStar(channel)
96
98
  const headerTitleBadges = renderHeaderTitleBadges?.({
97
99
  channel,
@@ -148,6 +150,7 @@ const CustomChannelHeader: React.FC<{
148
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"
149
151
  >
150
152
  <span className="min-w-0 truncate">{participantName}</span>
153
+ {isParticipantVerified && <VerifiedBadge />}
151
154
  {headerTitleBadges && (
152
155
  <span className="shrink-0">{headerTitleBadges}</span>
153
156
  )}
@@ -156,6 +159,7 @@ const CustomChannelHeader: React.FC<{
156
159
  ) : (
157
160
  <p className="flex max-w-full items-center justify-center gap-1 text-center text-xs font-medium text-black/90">
158
161
  <span className="min-w-0 truncate">{participantName}</span>
162
+ {isParticipantVerified && <VerifiedBadge />}
159
163
  {headerTitleBadges && (
160
164
  <span className="shrink-0">{headerTitleBadges}</span>
161
165
  )}
@@ -241,6 +245,7 @@ const CustomChannelHeader: React.FC<{
241
245
  className="flex min-w-0 max-w-full items-center gap-1 rounded font-medium text-black/90 transition-opacity hover:opacity-70"
242
246
  >
243
247
  <span className="min-w-0 truncate">{participantName}</span>
248
+ {isParticipantVerified && <VerifiedBadge />}
244
249
  {headerTitleBadges && (
245
250
  <span className="shrink-0">{headerTitleBadges}</span>
246
251
  )}
@@ -249,6 +254,7 @@ const CustomChannelHeader: React.FC<{
249
254
  ) : (
250
255
  <h1 className="flex min-w-0 items-center gap-1 font-medium text-black/90">
251
256
  <span className="min-w-0 truncate">{participantName}</span>
257
+ {isParticipantVerified && <VerifiedBadge />}
252
258
  {headerTitleBadges && (
253
259
  <span className="shrink-0">{headerTitleBadges}</span>
254
260
  )}
@@ -340,7 +346,6 @@ const ChannelViewInner: React.FC<{
340
346
  viewerLanguage?: string
341
347
  showChannelInfo?: boolean
342
348
  onParticipantNameClick?: () => void
343
- composerInput?: ChannelViewProps['composerInput']
344
349
  }> = ({
345
350
  onBack,
346
351
  showBackButton,
@@ -368,7 +373,6 @@ const ChannelViewInner: React.FC<{
368
373
  viewerLanguage,
369
374
  showChannelInfo = true,
370
375
  onParticipantNameClick,
371
- composerInput,
372
376
  }) => {
373
377
  const { channel } = useChannelStateContext()
374
378
 
@@ -485,7 +489,6 @@ const ChannelViewInner: React.FC<{
485
489
  renderFooter={() => renderMessageInputFooter?.(channel)}
486
490
  disabled={composerDisabled}
487
491
  disabledReason={composerDisabledReason}
488
- composerInput={composerInput}
489
492
  />
490
493
  </Window>
491
494
  </WithComponents>
@@ -532,7 +535,6 @@ export const ChannelView = React.memo<ChannelViewProps>(
532
535
  viewerLanguage,
533
536
  showChannelInfo = true,
534
537
  onParticipantNameClick,
535
- composerInput,
536
538
  }) => {
537
539
  // Custom send message handler that:
538
540
  // 1. Applies messageMetadata if provided
@@ -647,7 +649,6 @@ export const ChannelView = React.memo<ChannelViewProps>(
647
649
  viewerLanguage={viewerLanguage}
648
650
  showChannelInfo={showChannelInfo}
649
651
  onParticipantNameClick={onParticipantNameClick}
650
- composerInput={composerInput}
651
652
  />
652
653
  </Channel>
653
654
  </DmAgentEnabledContext.Provider>
@@ -204,40 +204,6 @@ describe('CustomMessageInput', () => {
204
204
  ).toBeInTheDocument()
205
205
  })
206
206
 
207
- it('renders a custom composerInput in place of the default composer', () => {
208
- mockChannelData = {}
209
-
210
- renderWithProviders(
211
- <CustomMessageInput
212
- composerInput={() => (
213
- <div data-testid="custom-composer">custom composer</div>
214
- )}
215
- />
216
- )
217
-
218
- // The host-owned composer renders instead of the built-in textarea/send.
219
- expect(screen.getByTestId('custom-composer')).toBeInTheDocument()
220
- expect(screen.queryByTestId('textarea-composer')).not.toBeInTheDocument()
221
- // It is still mounted inside Stream's <MessageInput> so it keeps the
222
- // message-input context (send pipeline, attachments, previews).
223
- expect(screen.getByTestId('stream-message-input')).toBeInTheDocument()
224
- })
225
-
226
- it('keeps the frozen wrapper around a custom composerInput', () => {
227
- mockChannelData = { frozen: true }
228
-
229
- const { container } = renderWithProviders(
230
- <CustomMessageInput
231
- composerInput={() => <div data-testid="custom-composer" />}
232
- />
233
- )
234
-
235
- const messageInput = container.querySelector('.message-input')
236
- expect(messageInput).toHaveAttribute('aria-disabled', 'true')
237
- expect(messageInput).toHaveAttribute('inert')
238
- expect(screen.getByTestId('custom-composer')).toBeInTheDocument()
239
- })
240
-
241
207
  it('does not render the locked panel when the composer is not disabled', () => {
242
208
  mockChannelData = {}
243
209
 
@@ -23,15 +23,6 @@ import { CustomLinkPreviewList } from '../CustomLinkPreviewList'
23
23
  */
24
24
  const ComposerLockedContext = React.createContext(false)
25
25
 
26
- /**
27
- * Read the composer's locked (frozen-channel) state from inside a custom
28
- * composer supplied via `composerInput`. Returns true when the channel is
29
- * frozen; mirror the default composer by rendering the textarea read-only and
30
- * disabling the send control.
31
- */
32
- export const useComposerLocked = (): boolean =>
33
- useContext(ComposerLockedContext)
34
-
35
26
  const DefaultSendButton: React.FC<{
36
27
  sendMessage: () => void
37
28
  disabled?: boolean
@@ -110,18 +101,6 @@ export interface CustomMessageInputProps {
110
101
  * `disabled` is true.
111
102
  */
112
103
  disabledReason?: string
113
- /**
114
- * Replace the inner composer — the textarea + send arrangement rendered
115
- * inside Stream's `<MessageInput>` — with a host-owned component. When
116
- * provided it is passed to Stream as the `Input` component, so it runs inside
117
- * the message-input context and can drive sending, attachments, and previews
118
- * via the Stream hooks (`useMessageInputContext`,
119
- * `useMessageComposerHasSendableData`, `TextareaComposer`, …). The outer
120
- * frozen/`renderActions`/`renderFooter` wrapper is unchanged; read
121
- * `useComposerLocked()` to honour the frozen state. Defaults to the built-in
122
- * composer.
123
- */
124
- composerInput?: React.ComponentType
125
104
  }
126
105
 
127
106
  export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
@@ -129,7 +108,6 @@ export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
129
108
  renderFooter,
130
109
  disabled = false,
131
110
  disabledReason,
132
- composerInput,
133
111
  }) => {
134
112
  const { channel } = useChannelStateContext()
135
113
  const isFrozen = channel?.data?.frozen === true
@@ -165,7 +143,7 @@ export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
165
143
  </div>
166
144
  )}
167
145
  <ComposerLockedContext.Provider value={isFrozen}>
168
- <MessageInput Input={composerInput ?? CustomMessageInputInner} />
146
+ <MessageInput Input={CustomMessageInputInner} />
169
147
  </ComposerLockedContext.Provider>
170
148
  </div>
171
149
  {renderFooter?.()}
@@ -39,7 +39,6 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
39
39
  renderMessage,
40
40
  onMessageLinkClick,
41
41
  showChannelInfo,
42
- composerInput,
43
42
  }) => {
44
43
  const {
45
44
  client,
@@ -265,7 +264,6 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
265
264
  renderMessage={renderMessage}
266
265
  onMessageLinkClick={onMessageLinkClick}
267
266
  showChannelInfo={showChannelInfo}
268
- composerInput={composerInput}
269
267
  />
270
268
  </div>
271
269
  </div>
@@ -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'
@@ -30,7 +32,6 @@ export { MessagingProvider } from './providers/MessagingProvider'
30
32
  export { CustomMessageProvider } from './components/CustomMessage/context'
31
33
 
32
34
  // Hooks
33
- export { useComposerLocked } from './components/CustomMessageInput'
34
35
  export { useMessaging } from './hooks/useMessaging'
35
36
  export { useMessageVote } from './hooks/useMessageVote'
36
37
  export { useCustomMessage } from './components/CustomMessage/context'
@@ -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
@@ -324,23 +324,6 @@ export interface ChannelViewProps {
324
324
  */
325
325
  attachmentPreviewList?: ComponentType
326
326
 
327
- /**
328
- * Replace the entire inner composer — the textarea + send arrangement — with
329
- * a host-owned component. Passed to Stream's `<MessageInput>` as the `Input`
330
- * component, so it runs inside the message-input context and reuses the
331
- * library's send pipeline (`doSendMessageRequest`, message metadata, DM-agent
332
- * suppression), attachments, and previews via the Stream hooks
333
- * (`useMessageInputContext`, `useMessageComposerHasSendableData`,
334
- * `TextareaComposer`, …). Use it when a surface needs a different composer
335
- * layout (e.g. a full-width textarea above a toolbar row) than the default.
336
- *
337
- * The outer wrapper — frozen handling and the `renderMessageInputActions` /
338
- * `renderMessageInputFooter` slots — is unchanged. Read the exported
339
- * `useComposerLocked()` hook to honour the frozen state. Defaults to the
340
- * built-in composer when omitted.
341
- */
342
- composerInput?: ComponentType
343
-
344
327
  /**
345
328
  * Fired when the participant's name in the channel header is clicked. When
346
329
  * provided, the name renders as a button with a trailing caret (an affordance
@@ -376,7 +359,6 @@ export type ChannelViewPassthroughProps = Pick<
376
359
  | 'renderMessage'
377
360
  | 'onMessageLinkClick'
378
361
  | 'showChannelInfo'
379
- | 'composerInput'
380
362
  >
381
363
 
382
364
  /**