@linktr.ee/messaging-react 2.6.2-rc-1780478292 → 3.0.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 +36 -88
- package/dist/{Card-DtoXBrhV.cjs → Card-B7ePjYQ6.cjs} +2 -2
- package/dist/{Card-DtoXBrhV.cjs.map → Card-B7ePjYQ6.cjs.map} +1 -1
- package/dist/{Card-DmgAq0-y.js → Card-C-ZIQW_q.js} +2 -2
- package/dist/{Card-DmgAq0-y.js.map → Card-C-ZIQW_q.js.map} +1 -1
- package/dist/{Card-BIb2ouTi.js → Card-C46z9zz4.js} +2 -2
- package/dist/{Card-BIb2ouTi.js.map → Card-C46z9zz4.js.map} +1 -1
- package/dist/{Card-Dr3LuTAS.cjs → Card-Cq0x0bbb.cjs} +2 -2
- package/dist/{Card-Dr3LuTAS.cjs.map → Card-Cq0x0bbb.cjs.map} +1 -1
- package/dist/{Card-CafojjYc.js → Card-Cqld0-Ws.js} +3 -3
- package/dist/{Card-CafojjYc.js.map → Card-Cqld0-Ws.js.map} +1 -1
- package/dist/{Card-DrIyNSwR.cjs → Card-Drz28Q-Y.cjs} +2 -2
- package/dist/{Card-DrIyNSwR.cjs.map → Card-Drz28Q-Y.cjs.map} +1 -1
- package/dist/{LockedThumbnail-CPAHQ9jA.cjs → LockedThumbnail--h4GTH41.cjs} +2 -2
- package/dist/{LockedThumbnail-CPAHQ9jA.cjs.map → LockedThumbnail--h4GTH41.cjs.map} +1 -1
- package/dist/{LockedThumbnail-B4LMWHDV.js → LockedThumbnail-D5NHhET2.js} +2 -2
- package/dist/{LockedThumbnail-B4LMWHDV.js.map → LockedThumbnail-D5NHhET2.js.map} +1 -1
- package/dist/{index-LiNmL1ax.js → index-BUT2yBvJ.js} +1439 -1737
- package/dist/index-BUT2yBvJ.js.map +1 -0
- package/dist/index-DqNobxVj.cjs +2 -0
- package/dist/index-DqNobxVj.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +29 -63
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/ChannelList/index.test.tsx +0 -22
- package/src/components/ChannelList/index.tsx +0 -4
- package/src/components/ChannelView.test.tsx +0 -11
- package/src/components/ChannelView.tsx +3 -21
- package/src/components/MessagingShell/MessagingShell.test.tsx +171 -0
- package/src/components/MessagingShell/index.tsx +104 -329
- package/src/types.ts +23 -81
- package/src/utils/getMessageDisplayText.test.ts +0 -32
- package/src/utils/getMessageDisplayText.ts +1 -6
- package/dist/index-Degc6G3J.cjs +0 -2
- package/dist/index-Degc6G3J.cjs.map +0 -1
- package/dist/index-LiNmL1ax.js.map +0 -1
- package/src/components/CustomMessage/CustomMessage.translation.test.tsx +0 -191
- package/src/components/MessagingShell/EmptyState.stories.tsx +0 -35
- package/src/components/MessagingShell/EmptyState.tsx +0 -117
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-DqNobxVj.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
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Attachment } from 'stream-chat';
|
|
2
2
|
import { Channel } from 'stream-chat';
|
|
3
3
|
import { ChannelFilters } from 'stream-chat';
|
|
4
|
-
import { ChannelListProps as ChannelListProps_2 } from 'stream-chat-react';
|
|
5
|
-
import { ChannelMemberResponse } from 'stream-chat';
|
|
6
4
|
import { ChannelSort } from 'stream-chat';
|
|
7
5
|
import { ComponentType } from 'react';
|
|
8
6
|
import { default as default_2 } from 'react';
|
|
@@ -113,18 +111,6 @@ export declare interface ChannelListProps {
|
|
|
113
111
|
* ChannelList consumers.
|
|
114
112
|
*/
|
|
115
113
|
allowNewMessagesFromUnfilteredChannels?: boolean;
|
|
116
|
-
/**
|
|
117
|
-
* Advanced override for handling `notification.message_new` events.
|
|
118
|
-
* Use for filtered/product-defined views that need to verify whether a
|
|
119
|
-
* channel belongs in the current list before inserting it.
|
|
120
|
-
*/
|
|
121
|
-
onMessageNew?: ChannelListProps_2['onMessageNew'];
|
|
122
|
-
/**
|
|
123
|
-
* Advanced override for handling `notification.added_to_channel` events.
|
|
124
|
-
* Use for filtered/product-defined views that need to verify whether a
|
|
125
|
-
* channel belongs in the current list before inserting it.
|
|
126
|
-
*/
|
|
127
|
-
onAddedToChannel?: ChannelListProps_2['onAddedToChannel'];
|
|
128
114
|
/**
|
|
129
115
|
* Client-side filter applied before rendering the channel list.
|
|
130
116
|
* Websocket events can add channels to the list that bypass server-side
|
|
@@ -162,7 +148,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
|
|
|
162
148
|
* Props that MessagingShell passes through to ChannelView.
|
|
163
149
|
* ChannelViewProps is the source of truth for these props.
|
|
164
150
|
*/
|
|
165
|
-
declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | '
|
|
151
|
+
declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderMessage' | 'onMessageLinkClick'>;
|
|
166
152
|
|
|
167
153
|
/**
|
|
168
154
|
* ChannelView component props
|
|
@@ -241,11 +227,6 @@ export declare interface ChannelViewProps {
|
|
|
241
227
|
* Falls back to message.text when no matching translation exists.
|
|
242
228
|
*/
|
|
243
229
|
viewerLanguage?: string;
|
|
244
|
-
/**
|
|
245
|
-
* Resolves the display label for the other channel participant.
|
|
246
|
-
* Defaults to name, then username, then "Unknown member" (never user.id).
|
|
247
|
-
*/
|
|
248
|
-
getParticipantDisplayName?: (participant: ChannelMemberResponse | undefined) => string;
|
|
249
230
|
/**
|
|
250
231
|
* Custom render function for a banner/card component that renders
|
|
251
232
|
* between the channel header and message list.
|
|
@@ -1147,65 +1128,50 @@ export declare interface MessagingProviderProps {
|
|
|
1147
1128
|
}
|
|
1148
1129
|
|
|
1149
1130
|
/**
|
|
1150
|
-
*
|
|
1131
|
+
* Direct-conversation surface for one specific participant.
|
|
1132
|
+
*
|
|
1133
|
+
* Renders a single ChannelView for the channel between the connected user and
|
|
1134
|
+
* `initialParticipantFilter`. If no channel exists yet and
|
|
1135
|
+
* `initialParticipantData` is supplied, the configured StreamChatService
|
|
1136
|
+
* channel creator is invoked to create one.
|
|
1151
1137
|
*/
|
|
1152
1138
|
export declare const MessagingShell: default_2.FC<MessagingShellProps>;
|
|
1153
1139
|
|
|
1154
1140
|
/**
|
|
1155
|
-
* Main MessagingShell component props
|
|
1141
|
+
* Main MessagingShell component props.
|
|
1142
|
+
*
|
|
1143
|
+
* MessagingShell renders a single direct conversation between the connected
|
|
1144
|
+
* user and `initialParticipantFilter`. It does not show a channel list — for
|
|
1145
|
+
* inbox-style surfaces, compose `<ChannelList>` and `<ChannelView>` directly.
|
|
1156
1146
|
*/
|
|
1157
1147
|
export declare interface MessagingShellProps extends ChannelViewPassthroughProps {
|
|
1158
1148
|
capabilities?: MessagingCapabilities;
|
|
1159
|
-
className?: string;
|
|
1160
1149
|
onChannelSelect?: (channel: Channel) => void;
|
|
1161
1150
|
/**
|
|
1162
|
-
*
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
* create the channel using the existing ChannelCreator.
|
|
1151
|
+
* Fired when the user leaves the conversation or blocks the participant.
|
|
1152
|
+
* The consumer is responsible for what happens next — typically
|
|
1153
|
+
* unmounting MessagingShell, navigating away, or re-rendering with new
|
|
1154
|
+
* participant data. When omitted, the shell renders a "Conversation
|
|
1155
|
+
* ended" state rather than an indefinite spinner.
|
|
1168
1156
|
*/
|
|
1169
|
-
|
|
1157
|
+
onExitConversation?: () => void;
|
|
1170
1158
|
/**
|
|
1171
|
-
*
|
|
1172
|
-
* is provided but no channel exists. If this is provided, MessagingShell will
|
|
1173
|
-
* automatically call service.startChannelWithParticipant() to create the channel.
|
|
1159
|
+
* UUID of the other participant for the direct conversation to render.
|
|
1174
1160
|
*
|
|
1175
|
-
*
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
/**
|
|
1179
|
-
* Controls whether the channel list is shown. When false, the channel list
|
|
1180
|
-
* is immediately hidden. Useful for direct conversation mode where you want
|
|
1181
|
-
* to show only the channel view without the list.
|
|
1182
|
-
*/
|
|
1183
|
-
showChannelList?: boolean;
|
|
1184
|
-
/**
|
|
1185
|
-
* Filters for channel list. Passed through to StreamChat ChannelList.
|
|
1161
|
+
* If a channel with this participant already exists, it is auto-selected.
|
|
1162
|
+
* If no channel exists, supply `initialParticipantData` to auto-create one
|
|
1163
|
+
* via the configured StreamChatService channel creator.
|
|
1186
1164
|
*/
|
|
1187
|
-
|
|
1165
|
+
initialParticipantFilter: string;
|
|
1188
1166
|
/**
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1167
|
+
* Participant data for auto-creating a channel when no channel exists yet
|
|
1168
|
+
* for `initialParticipantFilter`. When omitted, the shell renders an error
|
|
1169
|
+
* state ("No conversation found with this account") if the channel is
|
|
1170
|
+
* missing.
|
|
1192
1171
|
*
|
|
1193
|
-
*
|
|
1194
|
-
* channelRenderFilterFn={(channels) =>
|
|
1195
|
-
* channels.filter(ch => ch.data?.has_visitor_message !== false)
|
|
1196
|
-
* }
|
|
1197
|
-
*/
|
|
1198
|
-
channelRenderFilterFn?: (channels: Channel[]) => Channel[];
|
|
1199
|
-
/**
|
|
1200
|
-
* Custom empty state indicator component to render when the channel list is empty.
|
|
1201
|
-
* Useful for showing a custom empty state indicator when the channel list is empty.
|
|
1202
|
-
*/
|
|
1203
|
-
channelListCustomEmptyStateIndicator?: React.ComponentType<EmptyStateIndicatorProps>;
|
|
1204
|
-
/**
|
|
1205
|
-
* Custom render function for message preview text in channel list.
|
|
1206
|
-
* Receives the message and a default preview string.
|
|
1172
|
+
* This reuses the existing ChannelCreator from StreamChatServiceConfig.
|
|
1207
1173
|
*/
|
|
1208
|
-
|
|
1174
|
+
initialParticipantData?: Participant;
|
|
1209
1175
|
}
|
|
1210
1176
|
|
|
1211
1177
|
export declare function normalizeLanguageCode(language?: string): string | undefined;
|
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-BUT2yBvJ.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
package/package.json
CHANGED
|
@@ -56,28 +56,6 @@ describe('ChannelList', () => {
|
|
|
56
56
|
})
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
it('passes new-channel event handlers through to Stream ChannelList', () => {
|
|
60
|
-
const onMessageNew = vi.fn()
|
|
61
|
-
const onAddedToChannel = vi.fn()
|
|
62
|
-
|
|
63
|
-
renderWithProviders(
|
|
64
|
-
React.createElement(ChannelList, {
|
|
65
|
-
...defaultProps,
|
|
66
|
-
onMessageNew,
|
|
67
|
-
onAddedToChannel,
|
|
68
|
-
})
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
expect(streamChannelListMock).toHaveBeenCalledOnce()
|
|
72
|
-
const streamProps = streamChannelListMock.mock.calls[0][0] as {
|
|
73
|
-
onMessageNew?: unknown
|
|
74
|
-
onAddedToChannel?: unknown
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
expect(streamProps.onMessageNew).toBe(onMessageNew)
|
|
78
|
-
expect(streamProps.onAddedToChannel).toBe(onAddedToChannel)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
59
|
it('wraps channelRenderFilterFn to restore pending messages and delegates to consumer filter', () => {
|
|
82
60
|
const filterFn = vi.fn((channels: Channel[]) => channels)
|
|
83
61
|
|
|
@@ -21,8 +21,6 @@ export const ChannelList = React.memo<ChannelListProps>(
|
|
|
21
21
|
selectedChannel,
|
|
22
22
|
filters,
|
|
23
23
|
allowNewMessagesFromUnfilteredChannels = false,
|
|
24
|
-
onMessageNew,
|
|
25
|
-
onAddedToChannel,
|
|
26
24
|
channelRenderFilterFn,
|
|
27
25
|
sort = DEFAULT_SORT,
|
|
28
26
|
className,
|
|
@@ -94,8 +92,6 @@ export const ChannelList = React.memo<ChannelListProps>(
|
|
|
94
92
|
allowNewMessagesFromUnfilteredChannels={
|
|
95
93
|
allowNewMessagesFromUnfilteredChannels
|
|
96
94
|
}
|
|
97
|
-
onMessageNew={onMessageNew}
|
|
98
|
-
onAddedToChannel={onAddedToChannel}
|
|
99
95
|
channelRenderFilterFn={wrappedChannelRenderFilterFn}
|
|
100
96
|
Preview={CustomChannelPreview}
|
|
101
97
|
EmptyStateIndicator={customEmptyStateIndicator}
|
|
@@ -372,17 +372,6 @@ describe('ChannelView', () => {
|
|
|
372
372
|
expect(screen.getAllByText('Unknown member').length).toBeGreaterThan(0)
|
|
373
373
|
})
|
|
374
374
|
|
|
375
|
-
it('uses getParticipantDisplayName override when provided', () => {
|
|
376
|
-
renderWithProviders(
|
|
377
|
-
<ChannelView
|
|
378
|
-
channel={createChannel()}
|
|
379
|
-
getParticipantDisplayName={() => 'Custom Label'}
|
|
380
|
-
/>
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
expect(screen.getAllByText('Custom Label').length).toBeGreaterThan(0)
|
|
384
|
-
})
|
|
385
|
-
|
|
386
375
|
it('fires onMessageLinkClick with the href and undefined message when the link is not inside a message wrapper', () => {
|
|
387
376
|
const handler = vi.fn()
|
|
388
377
|
const { container } = renderWithProviders(
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from '@phosphor-icons/react'
|
|
8
8
|
import classNames from 'classnames'
|
|
9
9
|
import React, { useCallback, useEffect, useRef } from 'react'
|
|
10
|
-
import { Channel as ChannelType
|
|
10
|
+
import { Channel as ChannelType } from 'stream-chat'
|
|
11
11
|
import {
|
|
12
12
|
Channel,
|
|
13
13
|
Window,
|
|
@@ -49,9 +49,6 @@ const CustomChannelHeader: React.FC<{
|
|
|
49
49
|
canShowInfo: boolean
|
|
50
50
|
showStarButton?: boolean
|
|
51
51
|
dmAgentEnabled?: boolean
|
|
52
|
-
getParticipantDisplayName: (
|
|
53
|
-
participant: ChannelMemberResponse | undefined
|
|
54
|
-
) => string
|
|
55
52
|
}> = ({
|
|
56
53
|
onBack,
|
|
57
54
|
showBackButton,
|
|
@@ -59,7 +56,6 @@ const CustomChannelHeader: React.FC<{
|
|
|
59
56
|
canShowInfo,
|
|
60
57
|
showStarButton = false,
|
|
61
58
|
dmAgentEnabled = false,
|
|
62
|
-
getParticipantDisplayName,
|
|
63
59
|
}) => {
|
|
64
60
|
const { channel } = useChannelStateContext()
|
|
65
61
|
|
|
@@ -73,7 +69,7 @@ const CustomChannelHeader: React.FC<{
|
|
|
73
69
|
)
|
|
74
70
|
}, [channel._client?.userID, channel.state?.members])
|
|
75
71
|
|
|
76
|
-
const participantName =
|
|
72
|
+
const participantName = resolveParticipantDisplayName(participant?.user)
|
|
77
73
|
const participantImage = participant?.user?.image
|
|
78
74
|
const isStarred = useChannelStar(channel)
|
|
79
75
|
|
|
@@ -274,9 +270,6 @@ const ChannelViewInner: React.FC<{
|
|
|
274
270
|
) => React.ReactNode
|
|
275
271
|
dmAgentEnabled?: boolean
|
|
276
272
|
viewerLanguage?: string
|
|
277
|
-
getParticipantDisplayName: (
|
|
278
|
-
participant: ChannelMemberResponse | undefined
|
|
279
|
-
) => string
|
|
280
273
|
}> = ({
|
|
281
274
|
onBack,
|
|
282
275
|
showBackButton,
|
|
@@ -297,7 +290,6 @@ const ChannelViewInner: React.FC<{
|
|
|
297
290
|
renderMessage,
|
|
298
291
|
dmAgentEnabled = false,
|
|
299
292
|
viewerLanguage,
|
|
300
|
-
getParticipantDisplayName,
|
|
301
293
|
}) => {
|
|
302
294
|
const { channel } = useChannelStateContext()
|
|
303
295
|
const infoDialogRef = useRef<HTMLDialogElement>(null)
|
|
@@ -400,7 +392,6 @@ const ChannelViewInner: React.FC<{
|
|
|
400
392
|
canShowInfo={Boolean(participant)}
|
|
401
393
|
showStarButton={showStarButton}
|
|
402
394
|
dmAgentEnabled={showDmAgentHeader}
|
|
403
|
-
getParticipantDisplayName={getParticipantDisplayName}
|
|
404
395
|
/>
|
|
405
396
|
</div>
|
|
406
397
|
|
|
@@ -442,7 +433,7 @@ const ChannelViewInner: React.FC<{
|
|
|
442
433
|
dialogRef={infoDialogRef}
|
|
443
434
|
onClose={handleCloseInfo}
|
|
444
435
|
participant={participant}
|
|
445
|
-
participantDisplayName={
|
|
436
|
+
participantDisplayName={resolveParticipantDisplayName(participant?.user)}
|
|
446
437
|
channel={channel}
|
|
447
438
|
followerStatusLabel={followerStatusLabel}
|
|
448
439
|
onLeaveConversation={onLeaveConversation}
|
|
@@ -490,15 +481,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
490
481
|
sendButton,
|
|
491
482
|
attachmentPreviewList,
|
|
492
483
|
viewerLanguage,
|
|
493
|
-
getParticipantDisplayName: getParticipantDisplayNameProp,
|
|
494
484
|
}) => {
|
|
495
|
-
const getParticipantDisplayName = useCallback(
|
|
496
|
-
(participant: ChannelMemberResponse | undefined) =>
|
|
497
|
-
getParticipantDisplayNameProp?.(participant) ??
|
|
498
|
-
resolveParticipantDisplayName(participant?.user),
|
|
499
|
-
[getParticipantDisplayNameProp]
|
|
500
|
-
)
|
|
501
|
-
|
|
502
485
|
// Custom send message handler that:
|
|
503
486
|
// 1. Applies messageMetadata if provided
|
|
504
487
|
// 2. Adds skip_push and silent when DM agent is active
|
|
@@ -605,7 +588,6 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
605
588
|
customChannelActions={customChannelActions}
|
|
606
589
|
renderMessage={renderMessage}
|
|
607
590
|
viewerLanguage={viewerLanguage}
|
|
608
|
-
getParticipantDisplayName={getParticipantDisplayName}
|
|
609
591
|
/>
|
|
610
592
|
</Channel>
|
|
611
593
|
</DmAgentEnabledContext.Provider>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React, { act } from 'react'
|
|
2
|
+
import type { Channel } from 'stream-chat'
|
|
3
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { renderWithProviders, screen, waitFor } from '../../test/utils'
|
|
6
|
+
|
|
7
|
+
import { MessagingShell } from './index'
|
|
8
|
+
|
|
9
|
+
const queryChannelsMock = vi.fn()
|
|
10
|
+
const startChannelWithParticipantMock = vi.fn()
|
|
11
|
+
|
|
12
|
+
const baseClient = {
|
|
13
|
+
userID: 'me-1',
|
|
14
|
+
queryChannels: queryChannelsMock,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const baseService = {
|
|
18
|
+
startChannelWithParticipant: startChannelWithParticipantMock,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let useMessagingReturn: Record<string, unknown> = {
|
|
22
|
+
client: baseClient,
|
|
23
|
+
isConnected: true,
|
|
24
|
+
isLoading: false,
|
|
25
|
+
error: null,
|
|
26
|
+
refreshConnection: vi.fn(),
|
|
27
|
+
service: baseService,
|
|
28
|
+
debug: false,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
vi.mock('../../hooks/useMessaging', () => ({
|
|
32
|
+
useMessaging: () => useMessagingReturn,
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
let capturedChannelViewProps: Record<string, unknown> = {}
|
|
36
|
+
vi.mock('../ChannelView', () => ({
|
|
37
|
+
ChannelView: (props: Record<string, unknown>) => {
|
|
38
|
+
capturedChannelViewProps = props
|
|
39
|
+
return <div data-testid="channel-view" />
|
|
40
|
+
},
|
|
41
|
+
}))
|
|
42
|
+
|
|
43
|
+
vi.mock('./LoadingState', () => ({
|
|
44
|
+
LoadingState: () => <div data-testid="loading-state" />,
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
vi.mock('./ErrorState', () => ({
|
|
48
|
+
ErrorState: ({ message }: { message: string }) => (
|
|
49
|
+
<div data-testid="error-state">{message}</div>
|
|
50
|
+
),
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
const makeChannel = (id: string): Channel =>
|
|
54
|
+
({ id, cid: `messaging:${id}` }) as unknown as Channel
|
|
55
|
+
|
|
56
|
+
describe('MessagingShell', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
queryChannelsMock.mockReset()
|
|
59
|
+
startChannelWithParticipantMock.mockReset()
|
|
60
|
+
capturedChannelViewProps = {}
|
|
61
|
+
useMessagingReturn = {
|
|
62
|
+
client: baseClient,
|
|
63
|
+
isConnected: true,
|
|
64
|
+
isLoading: false,
|
|
65
|
+
error: null,
|
|
66
|
+
refreshConnection: vi.fn(),
|
|
67
|
+
service: baseService,
|
|
68
|
+
debug: false,
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('does not refire queryChannels when initialParticipantData and onChannelSelect get fresh references on re-render', async () => {
|
|
73
|
+
queryChannelsMock.mockResolvedValue([makeChannel('existing-1')])
|
|
74
|
+
|
|
75
|
+
const { rerender } = renderWithProviders(
|
|
76
|
+
<MessagingShell
|
|
77
|
+
initialParticipantFilter="other-1"
|
|
78
|
+
initialParticipantData={{ id: 'other-1', name: 'Other Person' }}
|
|
79
|
+
onChannelSelect={(channel) => void channel}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
await waitFor(() => {
|
|
84
|
+
expect(screen.getByTestId('channel-view')).toBeInTheDocument()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
expect(queryChannelsMock).toHaveBeenCalledTimes(1)
|
|
88
|
+
|
|
89
|
+
// Re-render with new inline references for the same logical values — this
|
|
90
|
+
// is the README-documented usage pattern that previously re-fired the
|
|
91
|
+
// effect on every render.
|
|
92
|
+
rerender(
|
|
93
|
+
<MessagingShell
|
|
94
|
+
initialParticipantFilter="other-1"
|
|
95
|
+
initialParticipantData={{ id: 'other-1', name: 'Other Person' }}
|
|
96
|
+
onChannelSelect={(channel) => void channel}
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
99
|
+
rerender(
|
|
100
|
+
<MessagingShell
|
|
101
|
+
initialParticipantFilter="other-1"
|
|
102
|
+
initialParticipantData={{ id: 'other-1', name: 'Other Person' }}
|
|
103
|
+
onChannelSelect={(channel) => void channel}
|
|
104
|
+
/>
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
expect(queryChannelsMock).toHaveBeenCalledTimes(1)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('refires queryChannels when initialParticipantFilter actually changes', async () => {
|
|
111
|
+
queryChannelsMock.mockResolvedValue([makeChannel('existing-1')])
|
|
112
|
+
|
|
113
|
+
const { rerender } = renderWithProviders(
|
|
114
|
+
<MessagingShell initialParticipantFilter="other-1" />
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(queryChannelsMock).toHaveBeenCalledTimes(1)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
rerender(<MessagingShell initialParticipantFilter="other-2" />)
|
|
122
|
+
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
expect(queryChannelsMock).toHaveBeenCalledTimes(2)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('calls onExitConversation and renders "Conversation ended" when the user leaves the conversation', async () => {
|
|
129
|
+
queryChannelsMock.mockResolvedValue([makeChannel('existing-1')])
|
|
130
|
+
const onExit = vi.fn()
|
|
131
|
+
|
|
132
|
+
renderWithProviders(
|
|
133
|
+
<MessagingShell
|
|
134
|
+
initialParticipantFilter="other-1"
|
|
135
|
+
onExitConversation={onExit}
|
|
136
|
+
/>
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
expect(screen.getByTestId('channel-view')).toBeInTheDocument()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const onLeave = capturedChannelViewProps.onLeaveConversation as () => void
|
|
144
|
+
act(() => onLeave())
|
|
145
|
+
|
|
146
|
+
expect(onExit).toHaveBeenCalledTimes(1)
|
|
147
|
+
expect(screen.getByTestId('error-state')).toHaveTextContent(
|
|
148
|
+
'Conversation ended'
|
|
149
|
+
)
|
|
150
|
+
expect(screen.queryByTestId('loading-state')).not.toBeInTheDocument()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('renders the "Conversation ended" state even when no onExitConversation callback is provided (no permanent spinner)', async () => {
|
|
154
|
+
queryChannelsMock.mockResolvedValue([makeChannel('existing-1')])
|
|
155
|
+
|
|
156
|
+
renderWithProviders(<MessagingShell initialParticipantFilter="other-1" />)
|
|
157
|
+
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(screen.getByTestId('channel-view')).toBeInTheDocument()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const onBlock = capturedChannelViewProps.onBlockParticipant as (
|
|
163
|
+
participantId?: string
|
|
164
|
+
) => void
|
|
165
|
+
act(() => onBlock('other-1'))
|
|
166
|
+
|
|
167
|
+
expect(screen.getByTestId('error-state')).toHaveTextContent(
|
|
168
|
+
'Conversation ended'
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
})
|