@linktr.ee/messaging-react 3.1.4-rc-1780636753 → 3.2.0

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 (35) hide show
  1. package/README.md +1 -18
  2. package/dist/{Card-BkgsPkp4.cjs → Card-B5TCecD6.cjs} +2 -2
  3. package/dist/{Card-BkgsPkp4.cjs.map → Card-B5TCecD6.cjs.map} +1 -1
  4. package/dist/{Card-D_XOj1eE.cjs → Card-CO089n1e.cjs} +2 -2
  5. package/dist/{Card-D_XOj1eE.cjs.map → Card-CO089n1e.cjs.map} +1 -1
  6. package/dist/{Card-BwFdJXYm.js → Card-DQYLHbDI.js} +2 -2
  7. package/dist/{Card-BwFdJXYm.js.map → Card-DQYLHbDI.js.map} +1 -1
  8. package/dist/{Card-B9atg4sP.js → Card-DTaHgygz.js} +2 -2
  9. package/dist/{Card-B9atg4sP.js.map → Card-DTaHgygz.js.map} +1 -1
  10. package/dist/{Card-1U2tLPcp.cjs → Card-aO1qZWDU.cjs} +2 -2
  11. package/dist/{Card-1U2tLPcp.cjs.map → Card-aO1qZWDU.cjs.map} +1 -1
  12. package/dist/{Card-jyXjZZ0u.js → Card-bdnjL_4d.js} +3 -3
  13. package/dist/{Card-jyXjZZ0u.js.map → Card-bdnjL_4d.js.map} +1 -1
  14. package/dist/{LockedThumbnail-oxtdpgut.cjs → LockedThumbnail-CWVybsBb.cjs} +2 -2
  15. package/dist/{LockedThumbnail-oxtdpgut.cjs.map → LockedThumbnail-CWVybsBb.cjs.map} +1 -1
  16. package/dist/{LockedThumbnail-Dwt_goCX.js → LockedThumbnail-nsFA3DjA.js} +2 -2
  17. package/dist/{LockedThumbnail-Dwt_goCX.js.map → LockedThumbnail-nsFA3DjA.js.map} +1 -1
  18. package/dist/index-BO2VfA-M.cjs +2 -0
  19. package/dist/index-BO2VfA-M.cjs.map +1 -0
  20. package/dist/{index-CO975B6P.js → index-DJKFVBkP.js} +1108 -1143
  21. package/dist/index-DJKFVBkP.js.map +1 -0
  22. package/dist/index.cjs +1 -1
  23. package/dist/index.d.ts +9 -39
  24. package/dist/index.js +1 -1
  25. package/package.json +4 -3
  26. package/src/components/ChannelList/index.test.tsx +3 -151
  27. package/src/components/ChannelList/index.tsx +4 -72
  28. package/src/components/ChannelView.test.tsx +31 -0
  29. package/src/components/ChannelView.tsx +41 -30
  30. package/src/components/MessagingShell/index.tsx +2 -0
  31. package/src/index.ts +0 -1
  32. package/src/types.ts +10 -38
  33. package/dist/index-CO975B6P.js.map +0 -1
  34. package/dist/index-D4Dse1Lu.cjs +0 -2
  35. package/dist/index-D4Dse1Lu.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-D4Dse1Lu.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-BO2VfA-M.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;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -95,36 +95,6 @@ export declare const ChannelEmptyState: default_2.FC;
95
95
  */
96
96
  export declare const ChannelList: default_2.NamedExoticComponent<ChannelListProps>;
97
97
 
98
- /**
99
- * Derived state reported by the mounted ChannelList instance.
100
- *
101
- * This reflects the currently loaded list data only. It does not guarantee
102
- * complete server-side truth for channels outside the mounted list's current
103
- * query window or cache.
104
- */
105
- export declare interface ChannelListDerivedState {
106
- /**
107
- * Whether the mounted list has completed its first channel-resolution pass.
108
- */
109
- isInitialLoadSettled: boolean;
110
- /**
111
- * Visible channels after `channelRenderFilterFn` is applied.
112
- */
113
- channels: Channel[];
114
- /**
115
- * Raw channels received from Stream before client-side filtering.
116
- */
117
- rawChannels: Channel[];
118
- /**
119
- * Convenience flag for empty-state consumers. Mirrors `channels.length > 0`.
120
- */
121
- hasChannels: boolean;
122
- /**
123
- * Number of visible channels with at least one unread message.
124
- */
125
- unreadCount: number;
126
- }
127
-
128
98
  /**
129
99
  * ChannelList component props
130
100
  */
@@ -167,14 +137,6 @@ export declare interface ChannelListProps {
167
137
  * Falls back to message.text when no matching translation exists.
168
138
  */
169
139
  viewerLanguage?: string;
170
- /**
171
- * Reports derived state from the mounted Stream ChannelList instance.
172
- *
173
- * This callback is driven by currently loaded list data only. Use it for
174
- * cache/list-derived UI state such as empty-state confirmation or visible
175
- * unread badges; do not assume it represents complete server-side truth.
176
- */
177
- onStateChange?: (state: ChannelListDerivedState) => void;
178
140
  }
179
141
 
180
142
  /**
@@ -186,7 +148,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
186
148
  * Props that MessagingShell passes through to ChannelView.
187
149
  * ChannelViewProps is the source of truth for these props.
188
150
  */
189
- declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderMessage' | 'onMessageLinkClick'>;
151
+ declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderMessage' | 'onMessageLinkClick' | 'showChannelInfo'>;
190
152
 
191
153
  /**
192
154
  * ChannelView component props
@@ -286,6 +248,14 @@ export declare interface ChannelViewProps {
286
248
  * and filter by starred/pinned status.
287
249
  */
288
250
  showStarButton?: boolean;
251
+ /**
252
+ * Show the channel info trigger (kebab) in the header, the clickable
253
+ * participant name on desktop, and mount the channel info dialog. Defaults
254
+ * to true. Set false for surfaces that should not expose the participant
255
+ * profile, block/report/delete actions — e.g. anonymous visitor chat,
256
+ * where the visitor has no authenticated identity to act on.
257
+ */
258
+ showChannelInfo?: boolean;
289
259
  /**
290
260
  * Enable thumbs up/down voting on chatbot messages.
291
261
  * When true, vote buttons render below chatbot (DM Agent) messages.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
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-CO975B6P.js";
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-DJKFVBkP.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.1.4-rc-1780636753",
3
+ "version": "3.2.0",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -28,8 +28,9 @@
28
28
  "test:ci": "vitest run",
29
29
  "test:ui": "vitest --ui",
30
30
  "test:coverage": "vitest --coverage",
31
- "lint": "eslint . --ext .ts,.tsx",
32
- "lint:fix": "eslint . --ext .ts,.tsx --fix",
31
+ "lint": "../../node_modules/.bin/eslint . --ext .ts,.tsx",
32
+ "lint:fix": "../../node_modules/.bin/eslint . --ext .ts,.tsx --fix",
33
+ "verify": "yarn type-check && yarn lint && yarn test:ci && yarn build",
33
34
  "storybook": "storybook dev -p 6006",
34
35
  "storybook:build": "storybook build"
35
36
  },
@@ -1,4 +1,3 @@
1
- import { act, waitFor } from '@testing-library/react'
2
1
  import React from 'react'
3
2
  import type { Channel } from 'stream-chat'
4
3
  import { describe, expect, it, vi, beforeEach } from 'vitest'
@@ -57,7 +56,7 @@ describe('ChannelList', () => {
57
56
  })
58
57
  })
59
58
 
60
- it('wraps channelRenderFilterFn to restore pending messages and delegates to consumer filter', async () => {
59
+ it('wraps channelRenderFilterFn to restore pending messages and delegates to consumer filter', () => {
61
60
  const filterFn = vi.fn((channels: Channel[]) => channels)
62
61
 
63
62
  renderWithProviders(
@@ -78,156 +77,9 @@ describe('ChannelList', () => {
78
77
 
79
78
  // When the wrapper is called, it should delegate to the consumer's filter
80
79
  const mockChannels = [
81
- {
82
- cid: 'ch-1',
83
- countUnread: () => 0,
84
- state: { pending_messages: [], addMessageSorted: vi.fn() },
85
- },
80
+ { cid: 'ch-1', state: { pending_messages: [], addMessageSorted: vi.fn() } },
86
81
  ]
87
- await act(async () => {
88
- streamProps.channelRenderFilterFn!(mockChannels)
89
- await Promise.resolve()
90
- })
82
+ streamProps.channelRenderFilterFn!(mockChannels)
91
83
  expect(filterFn).toHaveBeenCalledWith(mockChannels)
92
84
  })
93
-
94
- it('notifies hosts with derived visible-channel state', async () => {
95
- const onStateChange = vi.fn()
96
- const visibleChannel = {
97
- cid: 'ch-visible',
98
- countUnread: () => 2,
99
- state: { pending_messages: [], addMessageSorted: vi.fn() },
100
- }
101
- const hiddenChannel = {
102
- cid: 'ch-hidden',
103
- countUnread: () => 7,
104
- state: { pending_messages: [], addMessageSorted: vi.fn() },
105
- }
106
- const filterFn = vi.fn((channels: Channel[]) => channels.slice(0, 1))
107
-
108
- renderWithProviders(
109
- React.createElement(ChannelList, {
110
- ...defaultProps,
111
- channelRenderFilterFn: filterFn,
112
- onStateChange,
113
- } as never)
114
- )
115
-
116
- const streamProps = streamChannelListMock.mock.calls[0][0] as {
117
- channelRenderFilterFn?: (channels: Channel[]) => Channel[]
118
- }
119
-
120
- await act(async () => {
121
- streamProps.channelRenderFilterFn!([
122
- visibleChannel as unknown as Channel,
123
- hiddenChannel as unknown as Channel,
124
- ])
125
- await Promise.resolve()
126
- })
127
-
128
- await waitFor(() => {
129
- expect(onStateChange).toHaveBeenLastCalledWith({
130
- isInitialLoadSettled: true,
131
- channels: [visibleChannel],
132
- rawChannels: [visibleChannel, hiddenChannel],
133
- hasChannels: true,
134
- unreadCount: 1,
135
- })
136
- })
137
- })
138
-
139
- it('reports a settled empty state when no visible channels remain', async () => {
140
- const onStateChange = vi.fn()
141
- const filterFn = vi.fn(() => [])
142
- const hiddenChannel = {
143
- cid: 'ch-hidden',
144
- countUnread: () => 3,
145
- state: { pending_messages: [], addMessageSorted: vi.fn() },
146
- }
147
-
148
- renderWithProviders(
149
- React.createElement(ChannelList, {
150
- ...defaultProps,
151
- channelRenderFilterFn: filterFn,
152
- onStateChange,
153
- } as never)
154
- )
155
-
156
- const streamProps = streamChannelListMock.mock.calls[0][0] as {
157
- channelRenderFilterFn?: (channels: Channel[]) => Channel[]
158
- }
159
-
160
- await act(async () => {
161
- streamProps.channelRenderFilterFn!([hiddenChannel as unknown as Channel])
162
- await Promise.resolve()
163
- })
164
-
165
- await waitFor(() => {
166
- expect(onStateChange).toHaveBeenLastCalledWith({
167
- isInitialLoadSettled: true,
168
- channels: [],
169
- rawChannels: [hiddenChannel],
170
- hasChannels: false,
171
- unreadCount: 0,
172
- })
173
- })
174
- })
175
-
176
- it('resets derived state when filters change before the new list resolves', async () => {
177
- const onStateChange = vi.fn()
178
- const firstChannel = {
179
- cid: 'ch-first',
180
- countUnread: () => 1,
181
- state: { pending_messages: [], addMessageSorted: vi.fn() },
182
- }
183
-
184
- const { rerender } = renderWithProviders(
185
- React.createElement(ChannelList, {
186
- ...defaultProps,
187
- onStateChange,
188
- } as never)
189
- )
190
-
191
- let streamProps = streamChannelListMock.mock.calls.at(-1)?.[0] as {
192
- channelRenderFilterFn?: (channels: Channel[]) => Channel[]
193
- }
194
-
195
- await act(async () => {
196
- streamProps.channelRenderFilterFn!([firstChannel as unknown as Channel])
197
- await Promise.resolve()
198
- })
199
-
200
- await waitFor(() => {
201
- expect(onStateChange).toHaveBeenLastCalledWith({
202
- isInitialLoadSettled: true,
203
- channels: [firstChannel],
204
- rawChannels: [firstChannel],
205
- hasChannels: true,
206
- unreadCount: 1,
207
- })
208
- })
209
-
210
- rerender(
211
- React.createElement(ChannelList, {
212
- ...defaultProps,
213
- filters: { type: 'messaging', frozen: true },
214
- onStateChange,
215
- } as never)
216
- )
217
-
218
- streamProps = streamChannelListMock.mock.calls.at(-1)?.[0] as {
219
- channelRenderFilterFn?: (channels: Channel[]) => Channel[]
220
- }
221
- expect(typeof streamProps.channelRenderFilterFn).toBe('function')
222
-
223
- await waitFor(() => {
224
- expect(onStateChange).toHaveBeenLastCalledWith({
225
- isInitialLoadSettled: false,
226
- channels: [],
227
- rawChannels: [],
228
- hasChannels: false,
229
- unreadCount: 0,
230
- })
231
- })
232
- })
233
85
  })
@@ -5,21 +5,13 @@ import { ChannelList as StreamChannelList } from 'stream-chat-react'
5
5
 
6
6
  import { restorePendingMessages } from '../../hooks/useRestorePendingMessages'
7
7
  import { useMessagingContext } from '../../providers/MessagingProvider'
8
- import type { ChannelListDerivedState, ChannelListProps } from '../../types'
8
+ import type { ChannelListProps } from '../../types'
9
9
 
10
10
  import { ChannelListProvider } from './ChannelListContext'
11
11
  import CustomChannelPreview from './CustomChannelPreview'
12
12
 
13
13
  const DEFAULT_SORT = { last_message_at: -1 } as const
14
14
 
15
- const EMPTY_DERIVED_STATE: ChannelListDerivedState = {
16
- isInitialLoadSettled: false,
17
- channels: [],
18
- rawChannels: [],
19
- hasChannels: false,
20
- unreadCount: 0,
21
- }
22
-
23
15
  /**
24
16
  * Channel list component with customizable header and actions
25
17
  */
@@ -35,7 +27,6 @@ export const ChannelList = React.memo<ChannelListProps>(
35
27
  customEmptyStateIndicator,
36
28
  renderMessagePreview,
37
29
  viewerLanguage,
38
- onStateChange,
39
30
  }) => {
40
31
  // Track renders
41
32
  const renderCountRef = React.useRef(0)
@@ -43,42 +34,6 @@ export const ChannelList = React.memo<ChannelListProps>(
43
34
 
44
35
  // Get debug flag from context
45
36
  const { debug = false } = useMessagingContext()
46
- const [derivedState, setDerivedState] = React.useState<ChannelListDerivedState>(
47
- EMPTY_DERIVED_STATE
48
- )
49
- const listIdentityKey = React.useMemo(
50
- () => `${JSON.stringify(filters)}:${JSON.stringify(sort)}`,
51
- [filters, sort]
52
- )
53
- const pendingDerivedStateRef = React.useRef<ChannelListDerivedState>(
54
- EMPTY_DERIVED_STATE
55
- )
56
- const publishScheduledRef = React.useRef(false)
57
- const listVersionRef = React.useRef(0)
58
-
59
- const publishDerivedState = React.useCallback(
60
- (nextState: ChannelListDerivedState) => {
61
- const publishVersion = listVersionRef.current
62
- pendingDerivedStateRef.current = nextState
63
- if (publishScheduledRef.current) {
64
- return
65
- }
66
-
67
- publishScheduledRef.current = true
68
- queueMicrotask(() => {
69
- if (publishVersion !== listVersionRef.current) {
70
- publishScheduledRef.current = false
71
- return
72
- }
73
- publishScheduledRef.current = false
74
- setDerivedState((currentState) => {
75
- const pendingState = pendingDerivedStateRef.current
76
- return currentState === pendingState ? currentState : pendingState
77
- })
78
- })
79
- },
80
- []
81
- )
82
37
 
83
38
  // Wrap channelRenderFilterFn to restore pending messages for all channels
84
39
  // as soon as they are loaded, without waiting for the user to click into each one.
@@ -87,23 +42,11 @@ export const ChannelList = React.memo<ChannelListProps>(
87
42
  for (const channel of channels) {
88
43
  restorePendingMessages(channel)
89
44
  }
90
-
91
- const visibleChannels = channelRenderFilterFn
45
+ return channelRenderFilterFn
92
46
  ? channelRenderFilterFn(channels)
93
47
  : channels
94
-
95
- publishDerivedState({
96
- isInitialLoadSettled: true,
97
- channels: visibleChannels,
98
- rawChannels: channels,
99
- hasChannels: visibleChannels.length > 0,
100
- unreadCount: visibleChannels.filter((channel) => channel.countUnread() > 0)
101
- .length,
102
- })
103
-
104
- return visibleChannels
105
48
  },
106
- [channelRenderFilterFn, publishDerivedState]
49
+ [channelRenderFilterFn]
107
50
  )
108
51
 
109
52
  if (debug) {
@@ -131,17 +74,6 @@ export const ChannelList = React.memo<ChannelListProps>(
131
74
  ]
132
75
  )
133
76
 
134
- React.useEffect(() => {
135
- listVersionRef.current += 1
136
- pendingDerivedStateRef.current = EMPTY_DERIVED_STATE
137
- publishScheduledRef.current = false
138
- setDerivedState(EMPTY_DERIVED_STATE)
139
- }, [listIdentityKey])
140
-
141
- React.useEffect(() => {
142
- onStateChange?.(derivedState)
143
- }, [derivedState, onStateChange])
144
-
145
77
  return (
146
78
  <div
147
79
  className={classNames(
@@ -153,7 +85,7 @@ export const ChannelList = React.memo<ChannelListProps>(
153
85
  <div className="flex-1 overflow-hidden min-w-0">
154
86
  <ChannelListProvider value={contextValue}>
155
87
  <StreamChannelList
156
- key={listIdentityKey}
88
+ key={`${JSON.stringify(filters)}:${JSON.stringify(sort)}`}
157
89
  filters={filters}
158
90
  sort={sort}
159
91
  options={{ limit: 30 }}
@@ -205,6 +205,37 @@ describe('ChannelView', () => {
205
205
  expect(lastDialogProps().followerStatusLabel).toBeUndefined()
206
206
  })
207
207
 
208
+ it('renders the channel info trigger by default', () => {
209
+ const { container } = renderWithProviders(
210
+ <ChannelView channel={createChannel()} />
211
+ )
212
+
213
+ const infoTriggers = container.querySelectorAll(
214
+ 'button[aria-label="Show info"]'
215
+ )
216
+ expect(infoTriggers.length).toBeGreaterThan(0)
217
+ expect(channelInfoDialogProps.length).toBeGreaterThan(0)
218
+ })
219
+
220
+ it('hides the channel info trigger (both desktop and mobile) when showChannelInfo is false', () => {
221
+ const { container } = renderWithProviders(
222
+ <ChannelView channel={createChannel()} showChannelInfo={false} />
223
+ )
224
+
225
+ const infoTriggers = container.querySelectorAll(
226
+ 'button[aria-label="Show info"]'
227
+ )
228
+ expect(infoTriggers.length).toBe(0)
229
+ })
230
+
231
+ it('does not mount ChannelInfoDialog when showChannelInfo is false', () => {
232
+ renderWithProviders(
233
+ <ChannelView channel={createChannel()} showChannelInfo={false} />
234
+ )
235
+
236
+ expect(channelInfoDialogProps.length).toBe(0)
237
+ })
238
+
208
239
  it('passes composer disabled state and reason to the message input', () => {
209
240
  renderWithProviders(
210
241
  <ChannelView
@@ -143,14 +143,16 @@ const CustomChannelHeader: React.FC<{
143
143
  />
144
144
  </button>
145
145
  )}
146
- <button
147
- className={classNames(ICON_BTN_CLASS, 'bg-[#F1F0EE]')}
148
- onClick={onShowInfo}
149
- type="button"
150
- aria-label="Show info"
151
- >
152
- <DotsThreeIcon className="size-5 text-black/90" />
153
- </button>
146
+ {canShowInfo && (
147
+ <button
148
+ className={classNames(ICON_BTN_CLASS, 'bg-[#F1F0EE]')}
149
+ onClick={onShowInfo}
150
+ type="button"
151
+ aria-label="Show info"
152
+ >
153
+ <DotsThreeIcon className="size-5 text-black/90" />
154
+ </button>
155
+ )}
154
156
  </div>
155
157
  </div>
156
158
  <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]">
@@ -267,6 +269,7 @@ const ChannelViewInner: React.FC<{
267
269
  ) => React.ReactNode
268
270
  dmAgentEnabled?: boolean
269
271
  viewerLanguage?: string
272
+ showChannelInfo?: boolean
270
273
  }> = ({
271
274
  onBack,
272
275
  showBackButton,
@@ -292,6 +295,7 @@ const ChannelViewInner: React.FC<{
292
295
  renderMessage,
293
296
  dmAgentEnabled = false,
294
297
  viewerLanguage,
298
+ showChannelInfo = true,
295
299
  }) => {
296
300
  const { channel } = useChannelStateContext()
297
301
  const infoDialogRef = useRef<HTMLDialogElement>(null)
@@ -395,7 +399,7 @@ const ChannelViewInner: React.FC<{
395
399
  onBack={onBack}
396
400
  showBackButton={showBackButton}
397
401
  onShowInfo={handleShowInfo}
398
- canShowInfo={Boolean(participant)}
402
+ canShowInfo={showChannelInfo && Boolean(participant)}
399
403
  showStarButton={showStarButton}
400
404
  dmAgentEnabled={showDmAgentHeader}
401
405
  />
@@ -435,27 +439,32 @@ const ChannelViewInner: React.FC<{
435
439
  </Window>
436
440
  </WithComponents>
437
441
 
438
- {/* Channel Info Dialog */}
439
- <ChannelInfoDialog
440
- dialogRef={infoDialogRef}
441
- onClose={handleCloseInfo}
442
- participant={participant}
443
- participantDisplayName={resolveParticipantDisplayName(
444
- participant?.user
445
- )}
446
- channel={channel}
447
- followerStatusLabel={followerStatusLabel}
448
- onLeaveConversation={onLeaveConversation}
449
- onBlockParticipant={onBlockParticipant}
450
- showDeleteConversation={showDeleteConversation}
451
- showBlockParticipant={showBlockParticipant}
452
- showReportParticipant={showReportParticipant}
453
- onDeleteConversationClick={onDeleteConversationClick}
454
- onBlockParticipantClick={onBlockParticipantClick}
455
- onReportParticipantClick={onReportParticipantClick}
456
- customProfileContent={customProfileContent}
457
- customChannelActions={customChannelActions}
458
- />
442
+ {/* Channel Info Dialog — suppressed entirely when showChannelInfo is
443
+ false so restricted surfaces (e.g. anonymous visitor chat) cannot
444
+ surface the participant profile or block/report/delete actions even
445
+ if the dialog were opened programmatically. */}
446
+ {showChannelInfo && (
447
+ <ChannelInfoDialog
448
+ dialogRef={infoDialogRef}
449
+ onClose={handleCloseInfo}
450
+ participant={participant}
451
+ participantDisplayName={resolveParticipantDisplayName(
452
+ participant?.user
453
+ )}
454
+ channel={channel}
455
+ followerStatusLabel={followerStatusLabel}
456
+ onLeaveConversation={onLeaveConversation}
457
+ onBlockParticipant={onBlockParticipant}
458
+ showDeleteConversation={showDeleteConversation}
459
+ showBlockParticipant={showBlockParticipant}
460
+ showReportParticipant={showReportParticipant}
461
+ onDeleteConversationClick={onDeleteConversationClick}
462
+ onBlockParticipantClick={onBlockParticipantClick}
463
+ onReportParticipantClick={onReportParticipantClick}
464
+ customProfileContent={customProfileContent}
465
+ customChannelActions={customChannelActions}
466
+ />
467
+ )}
459
468
  </>
460
469
  )
461
470
  }
@@ -497,6 +506,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
497
506
  sendButton,
498
507
  attachmentPreviewList,
499
508
  viewerLanguage,
509
+ showChannelInfo = true,
500
510
  }) => {
501
511
  // Custom send message handler that:
502
512
  // 1. Applies messageMetadata if provided
@@ -611,6 +621,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
611
621
  customChannelActions={customChannelActions}
612
622
  renderMessage={renderMessage}
613
623
  viewerLanguage={viewerLanguage}
624
+ showChannelInfo={showChannelInfo}
614
625
  />
615
626
  </Channel>
616
627
  </DmAgentEnabledContext.Provider>
@@ -35,6 +35,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
35
35
  customChannelActions,
36
36
  renderMessage,
37
37
  onMessageLinkClick,
38
+ showChannelInfo,
38
39
  }) => {
39
40
  const {
40
41
  client,
@@ -256,6 +257,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
256
257
  customChannelActions={customChannelActions}
257
258
  renderMessage={renderMessage}
258
259
  onMessageLinkClick={onMessageLinkClick}
260
+ showChannelInfo={showChannelInfo}
259
261
  />
260
262
  </div>
261
263
  </div>
package/src/index.ts CHANGED
@@ -48,7 +48,6 @@ export type { ParticipantDisplayUser } from './utils/resolveParticipantDisplayNa
48
48
 
49
49
  // Types
50
50
  export type {
51
- ChannelListDerivedState,
52
51
  MessagingShellProps,
53
52
  ChannelListProps,
54
53
  ChannelViewProps,
package/src/types.ts CHANGED
@@ -52,36 +52,6 @@ export interface MessagingCapabilities {
52
52
  showDeleteConversation?: boolean
53
53
  }
54
54
 
55
- /**
56
- * Derived state reported by the mounted ChannelList instance.
57
- *
58
- * This reflects the currently loaded list data only. It does not guarantee
59
- * complete server-side truth for channels outside the mounted list's current
60
- * query window or cache.
61
- */
62
- export interface ChannelListDerivedState {
63
- /**
64
- * Whether the mounted list has completed its first channel-resolution pass.
65
- */
66
- isInitialLoadSettled: boolean
67
- /**
68
- * Visible channels after `channelRenderFilterFn` is applied.
69
- */
70
- channels: Channel[]
71
- /**
72
- * Raw channels received from Stream before client-side filtering.
73
- */
74
- rawChannels: Channel[]
75
- /**
76
- * Convenience flag for empty-state consumers. Mirrors `channels.length > 0`.
77
- */
78
- hasChannels: boolean
79
- /**
80
- * Number of visible channels with at least one unread message.
81
- */
82
- unreadCount: number
83
- }
84
-
85
55
  /**
86
56
  * ChannelList component props
87
57
  */
@@ -127,14 +97,6 @@ export interface ChannelListProps {
127
97
  * Falls back to message.text when no matching translation exists.
128
98
  */
129
99
  viewerLanguage?: string
130
- /**
131
- * Reports derived state from the mounted Stream ChannelList instance.
132
- *
133
- * This callback is driven by currently loaded list data only. Use it for
134
- * cache/list-derived UI state such as empty-state confirmation or visible
135
- * unread badges; do not assume it represents complete server-side truth.
136
- */
137
- onStateChange?: (state: ChannelListDerivedState) => void
138
100
  }
139
101
 
140
102
  /**
@@ -245,6 +207,15 @@ export interface ChannelViewProps {
245
207
  */
246
208
  showStarButton?: boolean
247
209
 
210
+ /**
211
+ * Show the channel info trigger (kebab) in the header, the clickable
212
+ * participant name on desktop, and mount the channel info dialog. Defaults
213
+ * to true. Set false for surfaces that should not expose the participant
214
+ * profile, block/report/delete actions — e.g. anonymous visitor chat,
215
+ * where the visitor has no authenticated identity to act on.
216
+ */
217
+ showChannelInfo?: boolean
218
+
248
219
  /**
249
220
  * Enable thumbs up/down voting on chatbot messages.
250
221
  * When true, vote buttons render below chatbot (DM Agent) messages.
@@ -347,6 +318,7 @@ export type ChannelViewPassthroughProps = Pick<
347
318
  | 'customChannelActions'
348
319
  | 'renderMessage'
349
320
  | 'onMessageLinkClick'
321
+ | 'showChannelInfo'
350
322
  >
351
323
 
352
324
  /**