@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.
- package/README.md +1 -18
- package/dist/{Card-BkgsPkp4.cjs → Card-B5TCecD6.cjs} +2 -2
- package/dist/{Card-BkgsPkp4.cjs.map → Card-B5TCecD6.cjs.map} +1 -1
- package/dist/{Card-D_XOj1eE.cjs → Card-CO089n1e.cjs} +2 -2
- package/dist/{Card-D_XOj1eE.cjs.map → Card-CO089n1e.cjs.map} +1 -1
- package/dist/{Card-BwFdJXYm.js → Card-DQYLHbDI.js} +2 -2
- package/dist/{Card-BwFdJXYm.js.map → Card-DQYLHbDI.js.map} +1 -1
- package/dist/{Card-B9atg4sP.js → Card-DTaHgygz.js} +2 -2
- package/dist/{Card-B9atg4sP.js.map → Card-DTaHgygz.js.map} +1 -1
- package/dist/{Card-1U2tLPcp.cjs → Card-aO1qZWDU.cjs} +2 -2
- package/dist/{Card-1U2tLPcp.cjs.map → Card-aO1qZWDU.cjs.map} +1 -1
- package/dist/{Card-jyXjZZ0u.js → Card-bdnjL_4d.js} +3 -3
- package/dist/{Card-jyXjZZ0u.js.map → Card-bdnjL_4d.js.map} +1 -1
- package/dist/{LockedThumbnail-oxtdpgut.cjs → LockedThumbnail-CWVybsBb.cjs} +2 -2
- package/dist/{LockedThumbnail-oxtdpgut.cjs.map → LockedThumbnail-CWVybsBb.cjs.map} +1 -1
- package/dist/{LockedThumbnail-Dwt_goCX.js → LockedThumbnail-nsFA3DjA.js} +2 -2
- package/dist/{LockedThumbnail-Dwt_goCX.js.map → LockedThumbnail-nsFA3DjA.js.map} +1 -1
- package/dist/index-BO2VfA-M.cjs +2 -0
- package/dist/index-BO2VfA-M.cjs.map +1 -0
- package/dist/{index-CO975B6P.js → index-DJKFVBkP.js} +1108 -1143
- package/dist/index-DJKFVBkP.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +9 -39
- package/dist/index.js +1 -1
- package/package.json +4 -3
- package/src/components/ChannelList/index.test.tsx +3 -151
- package/src/components/ChannelList/index.tsx +4 -72
- package/src/components/ChannelView.test.tsx +31 -0
- package/src/components/ChannelView.tsx +41 -30
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/index.ts +0 -1
- package/src/types.ts +10 -38
- package/dist/index-CO975B6P.js.map +0 -1
- package/dist/index-D4Dse1Lu.cjs +0 -2
- 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-
|
|
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-
|
|
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.
|
|
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',
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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={
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
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
|
/**
|