@ermis-network/ermis-chat-react 1.0.8 → 2.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/dist/index.cjs +15295 -4209
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +701 -195
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +862 -94
- package/dist/index.d.ts +862 -94
- package/dist/index.mjs +15246 -4186
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -4
- package/src/channelTypeUtils.ts +1 -1
- package/src/components/Avatar.tsx +2 -1
- package/src/components/Channel.tsx +6 -2
- package/src/components/ChannelActions.tsx +61 -2
- package/src/components/ChannelHeader.tsx +19 -5
- package/src/components/ChannelInfo/AddMemberModal.tsx +5 -1
- package/src/components/ChannelInfo/ChannelInfo.tsx +330 -187
- package/src/components/ChannelInfo/ChannelInfoTabs.tsx +59 -297
- package/src/components/ChannelInfo/ChannelSettingsPanel.tsx +30 -174
- package/src/components/ChannelInfo/EditChannelModal.tsx +4 -1
- package/src/components/ChannelInfo/MediaGridItem.tsx +12 -2
- package/src/components/ChannelInfo/MemberListItem.tsx +2 -3
- package/src/components/ChannelInfo/MessageSearchPanel.tsx +27 -126
- package/src/components/ChannelInfo/States.tsx +1 -1
- package/src/components/ChannelInfo/index.ts +3 -0
- package/src/components/ChannelInfo/useChannelInfoTabs.tsx +386 -0
- package/src/components/ChannelInfo/useChannelSettings.ts +212 -0
- package/src/components/ChannelInfo/useMessageSearch.tsx +141 -0
- package/src/components/ChannelList.tsx +177 -290
- package/src/components/CreateChannelModal.tsx +166 -88
- package/src/components/Dropdown.tsx +1 -16
- package/src/components/EditPreview.tsx +1 -0
- package/src/components/ErmisCallProvider.tsx +72 -17
- package/src/components/ErmisCallUI.tsx +43 -20
- package/src/components/FlatTopicGroupItem.tsx +232 -0
- package/src/components/ForwardMessageModal.tsx +31 -77
- package/src/components/MediaLightbox.tsx +62 -40
- package/src/components/MentionSuggestions.tsx +47 -35
- package/src/components/MessageActionsBox.tsx +4 -1
- package/src/components/MessageInput.tsx +137 -16
- package/src/components/MessageInputDefaults.tsx +127 -1
- package/src/components/MessageItem.tsx +93 -26
- package/src/components/MessageQuickReactions.tsx +153 -26
- package/src/components/MessageReactions.tsx +2 -1
- package/src/components/MessageRenderers.tsx +111 -39
- package/src/components/Panel.tsx +1 -14
- package/src/components/PinnedMessages.tsx +17 -5
- package/src/components/PreviewOverlay.tsx +24 -0
- package/src/components/ReadReceipts.tsx +2 -1
- package/src/components/TopicList.tsx +221 -0
- package/src/components/TopicModal.tsx +4 -1
- package/src/components/TypingIndicator.tsx +14 -5
- package/src/components/UserPicker.tsx +87 -10
- package/src/components/VirtualMessageList.tsx +106 -20
- package/src/context/ChatComponentsContext.tsx +14 -0
- package/src/context/ChatProvider.tsx +18 -14
- package/src/context/ErmisCallContext.tsx +4 -0
- package/src/hooks/useChannelCapabilities.ts +7 -4
- package/src/hooks/useChannelData.ts +10 -3
- package/src/hooks/useChannelListUpdates.ts +72 -20
- package/src/hooks/useChannelMessages.ts +72 -10
- package/src/hooks/useChannelRowUpdates.ts +24 -5
- package/src/hooks/useChatUser.ts +31 -0
- package/src/hooks/useContactChannels.ts +45 -0
- package/src/hooks/useContactCount.ts +50 -0
- package/src/hooks/useDownloadHandler.ts +36 -0
- package/src/hooks/useDragAndDrop.ts +79 -0
- package/src/hooks/useForwardMessage.ts +112 -0
- package/src/hooks/useInviteChannels.ts +88 -0
- package/src/hooks/useInviteCount.ts +104 -0
- package/src/hooks/useMentions.ts +0 -1
- package/src/hooks/useMessageActions.ts +13 -10
- package/src/hooks/usePendingState.ts +21 -4
- package/src/hooks/usePreviewState.ts +69 -0
- package/src/hooks/useStickerPicker.ts +62 -0
- package/src/hooks/useTopicGroupUpdates.ts +197 -0
- package/src/index.ts +56 -6
- package/src/messageTypeUtils.ts +13 -1
- package/src/styles/_base.css +0 -1
- package/src/styles/_call-ui.css +59 -2
- package/src/styles/_channel-info.css +41 -4
- package/src/styles/_channel-list.css +97 -57
- package/src/styles/_create-channel-modal.css +10 -0
- package/src/styles/_forward-modal.css +16 -1
- package/src/styles/_media-lightbox.css +32 -0
- package/src/styles/_mentions.css +1 -1
- package/src/styles/_message-actions.css +3 -4
- package/src/styles/_message-bubble.css +286 -107
- package/src/styles/_message-input.css +131 -0
- package/src/styles/_message-list.css +33 -17
- package/src/styles/_message-quick-reactions.css +40 -9
- package/src/styles/_message-reactions.css +4 -0
- package/src/styles/_modal.css +2 -1
- package/src/styles/_preview-overlay.css +38 -0
- package/src/styles/_tokens.css +17 -15
- package/src/styles/_typing-indicator.css +7 -1
- package/src/styles/index.css +1 -0
- package/src/types.ts +362 -14
- package/src/utils/avatarColors.ts +48 -0
- package/src/utils.ts +193 -10
|
@@ -1,97 +1,33 @@
|
|
|
1
|
-
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
|
2
|
-
import { VList } from 'virtua';
|
|
1
|
+
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import { VList as _VList, type VListHandle } from 'virtua';
|
|
3
|
+
const VList = _VList as any;
|
|
3
4
|
import type { Channel, Event, ChannelFilters } from '@ermis-network/ermis-chat-sdk';
|
|
4
|
-
import { parseSystemMessage, parseSignalMessage } from '@ermis-network/ermis-chat-sdk';
|
|
5
5
|
import { useChatClient } from '../hooks/useChatClient';
|
|
6
6
|
import { useChannelListUpdates } from '../hooks/useChannelListUpdates';
|
|
7
7
|
import { useOnlineUsers } from '../hooks/useOnlineUsers';
|
|
8
|
-
import {
|
|
8
|
+
import { getLastMessagePreview } from '../utils';
|
|
9
9
|
import { useChannelRowUpdates } from '../hooks/useChannelRowUpdates';
|
|
10
10
|
import { usePendingState } from '../hooks/usePendingState';
|
|
11
|
+
import {
|
|
12
|
+
SystemMessageTranslations,
|
|
13
|
+
SignalMessageTranslations,
|
|
14
|
+
} from '@ermis-network/ermis-chat-sdk';
|
|
11
15
|
import { Avatar } from './Avatar';
|
|
16
|
+
import { useChatComponents } from '../context/ChatComponentsContext';
|
|
12
17
|
import type { ChannelItemProps, ChannelListProps } from '../types';
|
|
13
18
|
|
|
14
19
|
export type { ChannelListProps, ChannelItemProps } from '../types';
|
|
15
20
|
import type { ChannelActionsProps } from '../types';
|
|
16
21
|
import { TopicModal } from './TopicModal';
|
|
17
22
|
import { DefaultChannelActions, computeDefaultActions } from './ChannelActions';
|
|
18
|
-
import {
|
|
23
|
+
import { FlatTopicGroupItem } from './FlatTopicGroupItem';
|
|
24
|
+
import { isDirectChannel, isGroupChannel, hasTopicsEnabled } from '../channelTypeUtils';
|
|
19
25
|
import { canManageChannel, isPendingMember, isSkippedMember, isFriendChannel } from '../channelRoleUtils';
|
|
20
26
|
|
|
21
27
|
export { DefaultChannelActions } from './ChannelActions';
|
|
22
28
|
export type { ChannelAction, ChannelActionsProps } from '../types';
|
|
23
29
|
|
|
24
|
-
/**
|
|
25
|
-
* Get a human-readable preview string for the last message,
|
|
26
|
-
* handling regular, system, and signal message types.
|
|
27
|
-
*/
|
|
28
|
-
function getLastMessagePreview(
|
|
29
|
-
channel: Channel,
|
|
30
|
-
myUserId?: string,
|
|
31
|
-
): { text: string; user: string; timestamp?: string | Date } {
|
|
32
|
-
const lastMsg = channel.state?.latestMessages?.slice(-1)[0];
|
|
33
|
-
if (!lastMsg) return { text: '', user: '' };
|
|
34
|
-
|
|
35
|
-
const timestamp = lastMsg.created_at;
|
|
36
|
-
|
|
37
|
-
const msgType = lastMsg.type || 'regular';
|
|
38
|
-
const rawText = lastMsg.text ?? '';
|
|
39
|
-
|
|
40
|
-
if (msgType === 'system') {
|
|
41
|
-
const userMap = buildUserMap(channel.state);
|
|
42
|
-
return { text: parseSystemMessage(rawText, userMap), user: '', timestamp };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (msgType === 'signal') {
|
|
46
|
-
const result = parseSignalMessage(rawText, myUserId || '');
|
|
47
|
-
return { text: result?.text || rawText, user: '', timestamp };
|
|
48
|
-
}
|
|
49
30
|
|
|
50
|
-
// Display 'Sticker' if message is a sticker
|
|
51
|
-
if (msgType === 'sticker' || (lastMsg as Record<string, unknown>).sticker_url) {
|
|
52
|
-
return { text: 'Sticker', user: lastMsg.user?.name || lastMsg.user_id || '', timestamp };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Regular / other
|
|
56
|
-
let displayText = rawText;
|
|
57
|
-
if (!displayText && lastMsg.attachments && lastMsg.attachments.length > 0) {
|
|
58
|
-
const att = lastMsg.attachments[0];
|
|
59
|
-
const type = att.type || '';
|
|
60
|
-
switch (type) {
|
|
61
|
-
case 'image':
|
|
62
|
-
displayText = '📷 Photo';
|
|
63
|
-
break;
|
|
64
|
-
case 'video':
|
|
65
|
-
displayText = '🎬 Video';
|
|
66
|
-
break;
|
|
67
|
-
case 'voiceRecording':
|
|
68
|
-
displayText = '🎤 Voice message';
|
|
69
|
-
break;
|
|
70
|
-
default:
|
|
71
|
-
displayText = '📎 File';
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
if (lastMsg.attachments.length > 1) {
|
|
75
|
-
displayText += ` +${lastMsg.attachments.length - 1}`;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Format mentions if necessary
|
|
80
|
-
const lastMsgRecord = lastMsg as Record<string, unknown>;
|
|
81
|
-
const mentionedUsers = lastMsgRecord.mentioned_users as string[] | undefined;
|
|
82
|
-
const mentionedAll = lastMsgRecord.mentioned_all as boolean | undefined;
|
|
83
|
-
|
|
84
|
-
if (displayText && (mentionedAll || (mentionedUsers && mentionedUsers.length > 0))) {
|
|
85
|
-
const userMap = buildUserMap(channel.state);
|
|
86
|
-
displayText = replaceMentionsForPreview(displayText, lastMsg as any, userMap);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
text: displayText,
|
|
91
|
-
user: lastMsg.user?.name || lastMsg.user_id || '',
|
|
92
|
-
timestamp,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
31
|
|
|
96
32
|
/* ----------------------------------------------------------
|
|
97
33
|
Memoized channel list item (exported for consumer reuse)
|
|
@@ -117,6 +53,8 @@ export const ChannelItem: React.FC<ChannelItemProps> = React.memo(({
|
|
|
117
53
|
onAddTopic,
|
|
118
54
|
onEditTopic,
|
|
119
55
|
onToggleCloseTopic,
|
|
56
|
+
onDeleteTopic,
|
|
57
|
+
onTruncateChannel,
|
|
120
58
|
hiddenActions,
|
|
121
59
|
actionLabels,
|
|
122
60
|
actionIcons,
|
|
@@ -141,8 +79,8 @@ export const ChannelItem: React.FC<ChannelItemProps> = React.memo(({
|
|
|
141
79
|
}, [channel]);
|
|
142
80
|
|
|
143
81
|
const defaultActions = useMemo(
|
|
144
|
-
() => computeDefaultActions(channel, currentUserId, { onAddTopic, onEditTopic, onToggleCloseTopic, isBlocked, actionLabels, actionIcons }),
|
|
145
|
-
[channel, currentUserId, updateCount, onAddTopic, onEditTopic, onToggleCloseTopic, isBlocked, actionLabels, actionIcons],
|
|
82
|
+
() => computeDefaultActions(channel, currentUserId, { onAddTopic, onEditTopic, onToggleCloseTopic, onDeleteTopic, onTruncateChannel, isBlocked, actionLabels, actionIcons }),
|
|
83
|
+
[channel, currentUserId, updateCount, onAddTopic, onEditTopic, onToggleCloseTopic, onDeleteTopic, onTruncateChannel, isBlocked, actionLabels, actionIcons],
|
|
146
84
|
);
|
|
147
85
|
|
|
148
86
|
const filteredActions = useMemo(() => {
|
|
@@ -154,6 +92,7 @@ export const ChannelItem: React.FC<ChannelItemProps> = React.memo(({
|
|
|
154
92
|
const name = channel.data?.name || channel.cid;
|
|
155
93
|
const image = channel.data?.image as string | undefined;
|
|
156
94
|
const showUnread = hasUnread && !isActive;
|
|
95
|
+
const avatarClassName = isGroupChannel(channel) ? 'ermis-avatar-wrapper--group' : undefined;
|
|
157
96
|
|
|
158
97
|
const timestampText = useMemo(() => {
|
|
159
98
|
if (!lastMessageTimestamp) return null;
|
|
@@ -180,7 +119,7 @@ export const ChannelItem: React.FC<ChannelItemProps> = React.memo(({
|
|
|
180
119
|
return (
|
|
181
120
|
<div className={itemClass} onClick={handleClick}>
|
|
182
121
|
<div className="ermis-channel-list__item-avatar-wrapper">
|
|
183
|
-
<AvatarComponent image={image} name={name} size={40} disableLightbox />
|
|
122
|
+
<AvatarComponent image={image} name={name} size={40} disableLightbox className={avatarClassName} />
|
|
184
123
|
{isOnline !== undefined && (
|
|
185
124
|
<span className={`ermis-channel-list__online-dot ermis-channel-list__online-dot--${isOnline ? 'online' : 'offline'}`} />
|
|
186
125
|
)}
|
|
@@ -268,6 +207,29 @@ const DefaultEmpty = React.memo(({ text }: { text?: string }) => (
|
|
|
268
207
|
));
|
|
269
208
|
DefaultEmpty.displayName = 'DefaultEmpty';
|
|
270
209
|
|
|
210
|
+
const DefaultError = React.memo(({ text, onRetry }: { text?: string; onRetry?: () => void }) => (
|
|
211
|
+
<div className="ermis-channel-list__error">
|
|
212
|
+
<div className="ermis-channel-list__error-icon">
|
|
213
|
+
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
214
|
+
<circle cx="12" cy="12" r="10" />
|
|
215
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
216
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
217
|
+
</svg>
|
|
218
|
+
</div>
|
|
219
|
+
<div className="ermis-channel-list__error-text">{text || 'Failed to load channels'}</div>
|
|
220
|
+
{onRetry && (
|
|
221
|
+
<button className="ermis-channel-list__error-retry" onClick={onRetry}>
|
|
222
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: '6px' }}>
|
|
223
|
+
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
|
|
224
|
+
<path d="M21 3v5h-5" />
|
|
225
|
+
</svg>
|
|
226
|
+
Retry
|
|
227
|
+
</button>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
));
|
|
231
|
+
DefaultError.displayName = 'DefaultError';
|
|
232
|
+
|
|
271
233
|
/* ----------------------------------------------------------
|
|
272
234
|
Virtual Row Component to map channel and defer parsing
|
|
273
235
|
---------------------------------------------------------- */
|
|
@@ -287,13 +249,23 @@ type ChannelRowProps = {
|
|
|
287
249
|
onAddTopic?: (channel: Channel) => void;
|
|
288
250
|
onEditTopic?: (channel: Channel) => void;
|
|
289
251
|
onToggleCloseTopic?: (channel: Channel, isClosed: boolean) => void;
|
|
252
|
+
onDeleteTopic?: (channel: Channel) => void;
|
|
253
|
+
onTruncateChannel?: (channel: Channel) => void;
|
|
290
254
|
hiddenActions?: string[];
|
|
291
255
|
actionLabels?: import('../types').ChannelActionLabels;
|
|
292
256
|
actionIcons?: import('../types').ChannelActionIcons;
|
|
293
257
|
isOnline?: boolean;
|
|
258
|
+
deletedMessageLabel?: React.ReactNode;
|
|
259
|
+
stickerMessageLabel?: React.ReactNode;
|
|
260
|
+
photoMessageLabel?: React.ReactNode;
|
|
261
|
+
videoMessageLabel?: React.ReactNode;
|
|
262
|
+
voiceRecordingMessageLabel?: React.ReactNode;
|
|
263
|
+
fileMessageLabel?: React.ReactNode;
|
|
264
|
+
systemMessageTranslations?: SystemMessageTranslations;
|
|
265
|
+
signalMessageTranslations?: SignalMessageTranslations;
|
|
294
266
|
};
|
|
295
267
|
|
|
296
|
-
const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
268
|
+
export const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
297
269
|
channel,
|
|
298
270
|
isActive,
|
|
299
271
|
handleSelect,
|
|
@@ -309,10 +281,20 @@ const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
|
309
281
|
onAddTopic,
|
|
310
282
|
onEditTopic,
|
|
311
283
|
onToggleCloseTopic,
|
|
284
|
+
onDeleteTopic,
|
|
285
|
+
onTruncateChannel,
|
|
312
286
|
hiddenActions,
|
|
313
287
|
actionLabels,
|
|
314
288
|
actionIcons,
|
|
315
289
|
isOnline,
|
|
290
|
+
deletedMessageLabel,
|
|
291
|
+
stickerMessageLabel,
|
|
292
|
+
photoMessageLabel,
|
|
293
|
+
videoMessageLabel,
|
|
294
|
+
voiceRecordingMessageLabel,
|
|
295
|
+
fileMessageLabel,
|
|
296
|
+
systemMessageTranslations,
|
|
297
|
+
signalMessageTranslations,
|
|
316
298
|
}) => {
|
|
317
299
|
// Use the new custom hook to handle all row-level realtime updates
|
|
318
300
|
const { isBannedInChannel, isBlockedInChannel, updateCount } = useChannelRowUpdates(channel, currentUserId);
|
|
@@ -330,15 +312,37 @@ const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
|
330
312
|
|
|
331
313
|
// Derive last message preview computation
|
|
332
314
|
const { text: rawLastMessageText, user: rawLastMessageUser, timestamp: rawLastMessageTimestamp } = useMemo(
|
|
333
|
-
() =>
|
|
315
|
+
() =>
|
|
316
|
+
getLastMessagePreview(channel, currentUserId, {
|
|
317
|
+
deletedMessageLabel,
|
|
318
|
+
stickerMessageLabel,
|
|
319
|
+
photoMessageLabel,
|
|
320
|
+
videoMessageLabel,
|
|
321
|
+
voiceRecordingMessageLabel,
|
|
322
|
+
fileMessageLabel,
|
|
323
|
+
systemMessageTranslations,
|
|
324
|
+
signalMessageTranslations,
|
|
325
|
+
}),
|
|
334
326
|
// Recompute if latestMessage changes or we get a force update
|
|
335
327
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
336
|
-
[
|
|
328
|
+
[
|
|
329
|
+
channel,
|
|
330
|
+
channel.state?.latestMessages,
|
|
331
|
+
updateCount,
|
|
332
|
+
deletedMessageLabel,
|
|
333
|
+
stickerMessageLabel,
|
|
334
|
+
photoMessageLabel,
|
|
335
|
+
videoMessageLabel,
|
|
336
|
+
voiceRecordingMessageLabel,
|
|
337
|
+
fileMessageLabel,
|
|
338
|
+
systemMessageTranslations,
|
|
339
|
+
signalMessageTranslations,
|
|
340
|
+
]
|
|
337
341
|
);
|
|
338
342
|
|
|
339
343
|
// Hide last message preview when banned, blocked, pending or skipped
|
|
340
344
|
const lastMessageText = (isBannedInChannel || isBlockedInChannel || isPending || isSkipped) ? '' : rawLastMessageText;
|
|
341
|
-
const lastMessageUser = (isBannedInChannel || isBlockedInChannel || isPending || isSkipped) ? '' : rawLastMessageUser;
|
|
345
|
+
const lastMessageUser = (isBannedInChannel || isBlockedInChannel || isPending || isSkipped || isDirectChannel(channel)) ? '' : rawLastMessageUser;
|
|
342
346
|
const lastMessageTimestamp = (isBannedInChannel || isBlockedInChannel || isPending || isSkipped) ? null : rawLastMessageTimestamp;
|
|
343
347
|
|
|
344
348
|
if (renderChannel) {
|
|
@@ -371,6 +375,8 @@ const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
|
371
375
|
onAddTopic={onAddTopic}
|
|
372
376
|
onEditTopic={onEditTopic}
|
|
373
377
|
onToggleCloseTopic={onToggleCloseTopic}
|
|
378
|
+
onDeleteTopic={onDeleteTopic}
|
|
379
|
+
onTruncateChannel={onTruncateChannel}
|
|
374
380
|
hiddenActions={hiddenActions}
|
|
375
381
|
actionLabels={actionLabels}
|
|
376
382
|
actionIcons={actionIcons}
|
|
@@ -380,194 +386,17 @@ const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
|
380
386
|
});
|
|
381
387
|
ChannelRow.displayName = 'ChannelRow';
|
|
382
388
|
|
|
383
|
-
export const ChannelTopicGroup = React.memo(({
|
|
384
|
-
channel,
|
|
385
|
-
activeChannel,
|
|
386
|
-
handleSelect,
|
|
387
|
-
renderChannel,
|
|
388
|
-
ChannelItemComponent,
|
|
389
|
-
AvatarComponent,
|
|
390
|
-
GeneralTopicAvatarComponent,
|
|
391
|
-
TopicAvatarComponent,
|
|
392
|
-
currentUserId,
|
|
393
|
-
pendingBadgeLabel,
|
|
394
|
-
blockedBadgeLabel,
|
|
395
|
-
generalTopicLabel,
|
|
396
|
-
closedTopicIcon,
|
|
397
|
-
PinnedIconComponent,
|
|
398
|
-
ChannelActionsComponent,
|
|
399
|
-
onAddTopic,
|
|
400
|
-
onEditTopic,
|
|
401
|
-
onToggleCloseTopic,
|
|
402
|
-
hiddenActions,
|
|
403
|
-
actionLabels,
|
|
404
|
-
actionIcons,
|
|
405
|
-
}: any) => {
|
|
406
|
-
const { updateCount } = useChannelRowUpdates(channel, currentUserId);
|
|
407
|
-
const [isExpanded, setIsExpanded] = useState(true);
|
|
408
|
-
const [topicUpdateCount, setTopicUpdateCount] = useState(0);
|
|
409
|
-
|
|
410
|
-
useEffect(() => {
|
|
411
|
-
const subs: { unsubscribe: () => void }[] = [];
|
|
412
|
-
const handleUpdate = () => setTopicUpdateCount((c) => c + 1);
|
|
413
|
-
const currentTopics = channel.state?.topics || [];
|
|
414
|
-
currentTopics.forEach((t: Channel) => {
|
|
415
|
-
subs.push(t.on('channel.pinned', handleUpdate));
|
|
416
|
-
subs.push(t.on('channel.unpinned', handleUpdate));
|
|
417
|
-
subs.push(t.on('message.new', handleUpdate));
|
|
418
|
-
subs.push(t.on('message.deleted', handleUpdate));
|
|
419
|
-
});
|
|
420
|
-
return () => {
|
|
421
|
-
subs.forEach((s) => s.unsubscribe());
|
|
422
|
-
};
|
|
423
|
-
}, [channel.state?.topics]);
|
|
424
|
-
|
|
425
|
-
const handleToggle = useCallback(() => setIsExpanded((prev) => !prev), []);
|
|
426
|
-
|
|
427
|
-
const userRole = channel.state?.members?.[currentUserId]?.channel_role;
|
|
428
|
-
const hasTopicAddPermission = canManageChannel(userRole);
|
|
429
|
-
|
|
430
|
-
const getTopicTime = (t: Channel) => {
|
|
431
|
-
const lastMsg = t.state?.latestMessages?.slice(-1)[0];
|
|
432
|
-
if (lastMsg?.created_at) return new Date(lastMsg.created_at).getTime();
|
|
433
|
-
if (t.data?.last_message_at) return new Date(t.data.last_message_at as string | Date).getTime();
|
|
434
|
-
if (t.data?.created_at) return new Date(t.data.created_at as string | Date).getTime();
|
|
435
|
-
return 0;
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
const topics = useMemo(() => {
|
|
439
|
-
const allTopics = channel.state?.topics || [];
|
|
440
|
-
return [...allTopics].sort((a: any, b: any) => {
|
|
441
|
-
const aPinned = a.data?.is_pinned === true;
|
|
442
|
-
const bPinned = b.data?.is_pinned === true;
|
|
443
|
-
if (aPinned && !bPinned) return -1;
|
|
444
|
-
if (!aPinned && bPinned) return 1;
|
|
445
|
-
|
|
446
|
-
return getTopicTime(b) - getTopicTime(a);
|
|
447
|
-
});
|
|
448
|
-
}, [channel.state?.topics, topicUpdateCount]);
|
|
449
|
-
const name = channel.data?.name || channel.cid;
|
|
450
|
-
const image = channel.data?.image as string | undefined;
|
|
451
|
-
|
|
452
|
-
const GeneralAvatar = useCallback(() => (
|
|
453
|
-
<div className="ermis-channel-list__topic-hashtag">#</div>
|
|
454
|
-
), []);
|
|
455
|
-
|
|
456
|
-
const TopicEmojiAvatar = useCallback(({ image }: any) => {
|
|
457
|
-
let emoji = '💬';
|
|
458
|
-
if (image && typeof image === 'string' && image.startsWith('emoji://')) {
|
|
459
|
-
emoji = image.replace('emoji://', '');
|
|
460
|
-
}
|
|
461
|
-
return <div className="ermis-channel-list__topic-hashtag">{emoji}</div>;
|
|
462
|
-
}, []);
|
|
463
|
-
|
|
464
|
-
const generalChannelProxy = useMemo(() => {
|
|
465
|
-
return new Proxy(channel, {
|
|
466
|
-
get(target, prop, receiver) {
|
|
467
|
-
if (prop === 'data') {
|
|
468
|
-
return { ...target.data, name: generalTopicLabel || 'general', is_pinned: false };
|
|
469
|
-
}
|
|
470
|
-
const value = Reflect.get(target, prop, receiver);
|
|
471
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
}, [channel, generalTopicLabel]);
|
|
475
|
-
|
|
476
|
-
const defaultActions = useMemo(
|
|
477
|
-
() => computeDefaultActions(channel, currentUserId, { onAddTopic, actionLabels, actionIcons }),
|
|
478
|
-
[channel, currentUserId, updateCount, onAddTopic, actionLabels, actionIcons],
|
|
479
|
-
);
|
|
480
|
-
|
|
481
|
-
const filteredActions = useMemo(() => {
|
|
482
|
-
if (!hiddenActions || hiddenActions.length === 0) return defaultActions;
|
|
483
|
-
return defaultActions.filter((a: any) => !hiddenActions.includes(a.id));
|
|
484
|
-
}, [defaultActions, hiddenActions]);
|
|
485
|
-
const ActionsComponent = ChannelActionsComponent || DefaultChannelActions;
|
|
486
|
-
|
|
487
|
-
return (
|
|
488
|
-
<div className="ermis-channel-list__topic-group">
|
|
489
|
-
<div
|
|
490
|
-
className={`ermis-channel-list__topic-header ${isExpanded ? 'ermis-channel-list__topic-header--expanded' : ''}`}
|
|
491
|
-
onClick={handleToggle}
|
|
492
|
-
>
|
|
493
|
-
<AvatarComponent image={image} name={name} size={40} disableLightbox />
|
|
494
|
-
<div className="ermis-channel-list__topic-header-name">{name}</div>
|
|
495
|
-
|
|
496
|
-
{channel.data?.is_pinned === true && PinnedIconComponent && (
|
|
497
|
-
<span className="ermis-channel-list__pinned-icon" title="Pinned">
|
|
498
|
-
<PinnedIconComponent />
|
|
499
|
-
</span>
|
|
500
|
-
)}
|
|
501
|
-
|
|
502
|
-
<div className="ermis-channel-list__topic-actions-wrapper">
|
|
503
|
-
<ActionsComponent channel={channel} actions={filteredActions} onClose={() => { }} />
|
|
504
|
-
</div>
|
|
505
|
-
|
|
506
|
-
<svg
|
|
507
|
-
className="ermis-channel-list__accordion-icon"
|
|
508
|
-
width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
|
509
|
-
>
|
|
510
|
-
<polyline points="6 9 12 15 18 9"></polyline>
|
|
511
|
-
</svg>
|
|
512
|
-
</div>
|
|
513
|
-
|
|
514
|
-
{isExpanded && (
|
|
515
|
-
<div className="ermis-channel-list__topic-sublist">
|
|
516
|
-
<ChannelRow
|
|
517
|
-
channel={generalChannelProxy as any}
|
|
518
|
-
isActive={activeChannel?.cid === channel.cid}
|
|
519
|
-
handleSelect={handleSelect}
|
|
520
|
-
renderChannel={renderChannel}
|
|
521
|
-
ChannelItemComponent={ChannelItemComponent}
|
|
522
|
-
AvatarComponent={GeneralTopicAvatarComponent || GeneralAvatar}
|
|
523
|
-
currentUserId={currentUserId}
|
|
524
|
-
pendingBadgeLabel={pendingBadgeLabel}
|
|
525
|
-
blockedBadgeLabel={blockedBadgeLabel}
|
|
526
|
-
closedTopicIcon={closedTopicIcon}
|
|
527
|
-
PinnedIconComponent={PinnedIconComponent}
|
|
528
|
-
ChannelActionsComponent={() => null}
|
|
529
|
-
hiddenActions={hiddenActions}
|
|
530
|
-
actionLabels={actionLabels}
|
|
531
|
-
actionIcons={actionIcons}
|
|
532
|
-
/>
|
|
533
|
-
{topics.map((topicChannel: any) => (
|
|
534
|
-
<ChannelRow
|
|
535
|
-
key={topicChannel.cid}
|
|
536
|
-
channel={topicChannel}
|
|
537
|
-
isActive={activeChannel?.cid === topicChannel.cid}
|
|
538
|
-
handleSelect={handleSelect}
|
|
539
|
-
renderChannel={renderChannel}
|
|
540
|
-
ChannelItemComponent={ChannelItemComponent}
|
|
541
|
-
AvatarComponent={TopicAvatarComponent || TopicEmojiAvatar}
|
|
542
|
-
currentUserId={currentUserId}
|
|
543
|
-
pendingBadgeLabel={pendingBadgeLabel}
|
|
544
|
-
blockedBadgeLabel={blockedBadgeLabel}
|
|
545
|
-
closedTopicIcon={closedTopicIcon}
|
|
546
|
-
PinnedIconComponent={PinnedIconComponent}
|
|
547
|
-
ChannelActionsComponent={ChannelActionsComponent}
|
|
548
|
-
onEditTopic={onEditTopic}
|
|
549
|
-
onToggleCloseTopic={onToggleCloseTopic}
|
|
550
|
-
hiddenActions={hiddenActions}
|
|
551
|
-
actionLabels={actionLabels}
|
|
552
|
-
actionIcons={actionIcons}
|
|
553
|
-
/>
|
|
554
|
-
))}
|
|
555
|
-
</div>
|
|
556
|
-
)}
|
|
557
|
-
</div>
|
|
558
|
-
);
|
|
559
|
-
});
|
|
560
|
-
ChannelTopicGroup.displayName = 'ChannelTopicGroup';
|
|
561
389
|
|
|
562
390
|
export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
563
|
-
filters = { type: ['messaging', 'team', 'meeting'],
|
|
391
|
+
filters = { type: ['messaging', 'team', 'meeting'], include_hidden_messages: true } as unknown as ChannelFilters,
|
|
564
392
|
sort = [],
|
|
565
|
-
options = { message_limit:
|
|
393
|
+
options = { message_limit: 1 } as unknown as ChannelListProps['options'],
|
|
566
394
|
renderChannel,
|
|
567
395
|
onChannelSelect,
|
|
568
396
|
className,
|
|
569
397
|
LoadingIndicator = DefaultLoading,
|
|
570
398
|
EmptyStateIndicator = DefaultEmpty,
|
|
399
|
+
ErrorIndicator,
|
|
571
400
|
AvatarComponent = Avatar,
|
|
572
401
|
ChannelItemComponent = ChannelItem,
|
|
573
402
|
pendingInvitesLabel,
|
|
@@ -575,11 +404,8 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
575
404
|
pendingBadgeLabel,
|
|
576
405
|
loadingLabel,
|
|
577
406
|
emptyStateLabel = 'No channels found',
|
|
407
|
+
errorLabel = 'Failed to load channels',
|
|
578
408
|
blockedBadgeLabel = 'Blocked',
|
|
579
|
-
ChannelTopicGroupComponent,
|
|
580
|
-
GeneralTopicAvatarComponent,
|
|
581
|
-
TopicAvatarComponent,
|
|
582
|
-
generalTopicLabel = 'general',
|
|
583
409
|
onAddTopic,
|
|
584
410
|
TopicEmojiPickerComponent,
|
|
585
411
|
closedTopicIcon,
|
|
@@ -587,18 +413,49 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
587
413
|
ChannelActionsComponent,
|
|
588
414
|
onEditTopic,
|
|
589
415
|
onToggleCloseTopic,
|
|
416
|
+
onDeleteTopic,
|
|
417
|
+
onTruncateChannel,
|
|
590
418
|
hiddenActions,
|
|
591
419
|
actionLabels,
|
|
592
420
|
actionIcons,
|
|
593
421
|
showOnlineStatus = true,
|
|
422
|
+
showPendingInvites = true,
|
|
423
|
+
onTopicDrillDown,
|
|
424
|
+
maxVisibleTopics,
|
|
425
|
+
moreTopicsLabel,
|
|
426
|
+
generalTopicLabel = 'general',
|
|
427
|
+
TopicPillComponent,
|
|
428
|
+
FlatTopicGroupItemComponent,
|
|
429
|
+
scrollToTopOnOwnMessage = true,
|
|
430
|
+
deletedMessageLabel,
|
|
431
|
+
stickerMessageLabel,
|
|
432
|
+
photoMessageLabel,
|
|
433
|
+
videoMessageLabel,
|
|
434
|
+
voiceRecordingMessageLabel,
|
|
435
|
+
fileMessageLabel,
|
|
436
|
+
systemMessageTranslations,
|
|
437
|
+
signalMessageTranslations,
|
|
594
438
|
}) => {
|
|
595
439
|
const { client, activeChannel, setActiveChannel } = useChatClient();
|
|
440
|
+
const { ChannelListErrorIndicator } = useChatComponents();
|
|
441
|
+
|
|
596
442
|
const [channels, setChannels] = useState<Channel[]>([]);
|
|
597
443
|
const [loading, setLoading] = useState(true);
|
|
444
|
+
const [error, setError] = useState<any>(null);
|
|
445
|
+
|
|
446
|
+
const ActualErrorIndicator = ErrorIndicator || ChannelListErrorIndicator || DefaultError;
|
|
598
447
|
const [isPendingExpanded, setIsPendingExpanded] = useState(true);
|
|
599
448
|
const [addingTopicForChannel, setAddingTopicForChannel] = useState<Channel | null>(null);
|
|
600
449
|
const [editingTopicForChannel, setEditingTopicForChannel] = useState<Channel | null>(null);
|
|
601
450
|
|
|
451
|
+
// Ref for imperative scroll control on the virtualized list
|
|
452
|
+
const vlistRef = useRef<VListHandle>(null);
|
|
453
|
+
|
|
454
|
+
// Scroll to top when the current user sends a message
|
|
455
|
+
const handleOwnMessageNew = useCallback(() => {
|
|
456
|
+
vlistRef.current?.scrollToIndex(0);
|
|
457
|
+
}, []);
|
|
458
|
+
|
|
602
459
|
const handleAddTopicClick = useCallback((channel: Channel) => {
|
|
603
460
|
if (onAddTopic) {
|
|
604
461
|
onAddTopic(channel);
|
|
@@ -648,7 +505,7 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
648
505
|
const ms = ch.state?.membership as Record<string, unknown> | undefined;
|
|
649
506
|
const isPending = isPendingMember(ms?.channel_role as string);
|
|
650
507
|
const isSkipped = isSkippedMember(ms?.channel_role as string);
|
|
651
|
-
|
|
508
|
+
|
|
652
509
|
if (isSkipped) {
|
|
653
510
|
return; // Filter out completely
|
|
654
511
|
}
|
|
@@ -670,10 +527,12 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
670
527
|
const loadChannels = useCallback(async () => {
|
|
671
528
|
try {
|
|
672
529
|
setLoading(true);
|
|
530
|
+
setError(null);
|
|
673
531
|
const result = await client.queryChannels(filters, sort, options as { message_limit?: number });
|
|
674
532
|
setChannels(result);
|
|
675
533
|
} catch (err) {
|
|
676
534
|
console.error('Failed to load channels:', err);
|
|
535
|
+
setError(err);
|
|
677
536
|
} finally {
|
|
678
537
|
setLoading(false);
|
|
679
538
|
}
|
|
@@ -684,7 +543,7 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
684
543
|
}, [loadChannels]);
|
|
685
544
|
|
|
686
545
|
// Real-time: List manipulation (move to top, add, delete)
|
|
687
|
-
useChannelListUpdates(channels, setChannels);
|
|
546
|
+
useChannelListUpdates(channels, setChannels, scrollToTopOnOwnMessage ? handleOwnMessageNew : undefined);
|
|
688
547
|
|
|
689
548
|
// Online status: compute set of online friend user IDs (skip if disabled)
|
|
690
549
|
const onlineUsers = useOnlineUsers(showOnlineStatus ? channels : []);
|
|
@@ -732,13 +591,19 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
732
591
|
);
|
|
733
592
|
|
|
734
593
|
if (loading) return <LoadingIndicator text={loadingLabel} />;
|
|
735
|
-
if (
|
|
594
|
+
if (error) return <ActualErrorIndicator text={errorLabel} onRetry={loadChannels} />;
|
|
595
|
+
|
|
596
|
+
const isEmpty = showPendingInvites
|
|
597
|
+
? (pendingChannels.length === 0 && regularChannels.length === 0)
|
|
598
|
+
: (regularChannels.length === 0);
|
|
599
|
+
|
|
600
|
+
if (isEmpty) return <EmptyStateIndicator text={emptyStateLabel} />;
|
|
736
601
|
|
|
737
602
|
return (
|
|
738
603
|
<div className={`ermis-channel-list${className ? ` ${className}` : ''}`}>
|
|
739
604
|
{/* VList requires its container to have a height to work. */}
|
|
740
|
-
<VList style={{ height: '100%' }}>
|
|
741
|
-
{pendingChannels.length > 0 && (
|
|
605
|
+
<VList ref={vlistRef} style={{ height: '100%' }}>
|
|
606
|
+
{showPendingInvites && pendingChannels.length > 0 && (
|
|
742
607
|
<div
|
|
743
608
|
className="ermis-channel-list__accordion-header"
|
|
744
609
|
onClick={() => setIsPendingExpanded(prev => !prev)}
|
|
@@ -756,7 +621,7 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
756
621
|
</svg>
|
|
757
622
|
</div>
|
|
758
623
|
)}
|
|
759
|
-
{isPendingExpanded && pendingChannels.map((channel: Channel) => {
|
|
624
|
+
{showPendingInvites && isPendingExpanded && pendingChannels.map((channel: Channel) => {
|
|
760
625
|
const isActive = activeChannel?.cid === channel.cid;
|
|
761
626
|
return (
|
|
762
627
|
<ChannelRow
|
|
@@ -773,48 +638,60 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
773
638
|
closedTopicIcon={closedTopicIcon}
|
|
774
639
|
PinnedIconComponent={PinnedIconComponent}
|
|
775
640
|
ChannelActionsComponent={ChannelActionsComponent}
|
|
641
|
+
onTruncateChannel={onTruncateChannel}
|
|
776
642
|
hiddenActions={hiddenActions}
|
|
777
643
|
actionLabels={actionLabels}
|
|
778
644
|
actionIcons={actionIcons}
|
|
779
645
|
isOnline={getIsOnline(channel)}
|
|
646
|
+
deletedMessageLabel={deletedMessageLabel}
|
|
647
|
+
stickerMessageLabel={stickerMessageLabel}
|
|
648
|
+
photoMessageLabel={photoMessageLabel}
|
|
649
|
+
videoMessageLabel={videoMessageLabel}
|
|
650
|
+
voiceRecordingMessageLabel={voiceRecordingMessageLabel}
|
|
651
|
+
fileMessageLabel={fileMessageLabel}
|
|
652
|
+
systemMessageTranslations={systemMessageTranslations}
|
|
653
|
+
signalMessageTranslations={signalMessageTranslations}
|
|
780
654
|
/>
|
|
781
655
|
);
|
|
782
656
|
})}
|
|
783
|
-
{pendingChannels.length > 0 && regularChannels.length > 0 && (
|
|
657
|
+
{/* {pendingChannels.length > 0 && regularChannels.length > 0 && (
|
|
784
658
|
<div className="ermis-channel-list__accordion-header ermis-channel-list__accordion-header--static">
|
|
785
659
|
<span>{channelsLabel}</span>
|
|
786
660
|
</div>
|
|
787
|
-
)}
|
|
661
|
+
)} */}
|
|
788
662
|
{regularChannels.map((channel: Channel) => {
|
|
789
663
|
const isActive = activeChannel?.cid === channel.cid;
|
|
790
664
|
const isTeamWithTopics = hasTopicsEnabled(channel);
|
|
791
665
|
|
|
792
666
|
if (isTeamWithTopics) {
|
|
793
|
-
|
|
667
|
+
// Drill-down mode: always render flat item with topic pills + last msg
|
|
668
|
+
const FlatComponent = FlatTopicGroupItemComponent || FlatTopicGroupItem;
|
|
794
669
|
return (
|
|
795
|
-
<
|
|
670
|
+
<FlatComponent
|
|
796
671
|
key={channel.cid}
|
|
797
672
|
channel={channel}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
renderChannel={renderChannel}
|
|
801
|
-
ChannelItemComponent={ChannelItemComponent}
|
|
673
|
+
isActive={isActive}
|
|
674
|
+
onDrillDown={onTopicDrillDown}
|
|
802
675
|
AvatarComponent={AvatarComponent}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
currentUserId={client.userID}
|
|
806
|
-
pendingBadgeLabel={pendingBadgeLabel}
|
|
807
|
-
blockedBadgeLabel={blockedBadgeLabel}
|
|
676
|
+
maxVisibleTopics={maxVisibleTopics}
|
|
677
|
+
moreTopicsLabel={moreTopicsLabel}
|
|
808
678
|
generalTopicLabel={generalTopicLabel}
|
|
809
|
-
|
|
810
|
-
closedTopicIcon={closedTopicIcon}
|
|
679
|
+
TopicPillComponent={TopicPillComponent}
|
|
811
680
|
PinnedIconComponent={PinnedIconComponent}
|
|
812
681
|
ChannelActionsComponent={ChannelActionsComponent}
|
|
813
|
-
|
|
814
|
-
|
|
682
|
+
onAddTopic={handleAddTopicClick}
|
|
683
|
+
onTruncateChannel={onTruncateChannel}
|
|
815
684
|
hiddenActions={hiddenActions}
|
|
816
685
|
actionLabels={actionLabels}
|
|
817
686
|
actionIcons={actionIcons}
|
|
687
|
+
deletedMessageLabel={deletedMessageLabel}
|
|
688
|
+
stickerMessageLabel={stickerMessageLabel}
|
|
689
|
+
photoMessageLabel={photoMessageLabel}
|
|
690
|
+
videoMessageLabel={videoMessageLabel}
|
|
691
|
+
voiceRecordingMessageLabel={voiceRecordingMessageLabel}
|
|
692
|
+
fileMessageLabel={fileMessageLabel}
|
|
693
|
+
systemMessageTranslations={systemMessageTranslations}
|
|
694
|
+
signalMessageTranslations={signalMessageTranslations}
|
|
818
695
|
/>
|
|
819
696
|
);
|
|
820
697
|
}
|
|
@@ -837,10 +714,20 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
837
714
|
onAddTopic={handleAddTopicClick}
|
|
838
715
|
onEditTopic={handleEditTopicClick}
|
|
839
716
|
onToggleCloseTopic={handleToggleCloseTopicClick}
|
|
717
|
+
onDeleteTopic={onDeleteTopic}
|
|
718
|
+
onTruncateChannel={onTruncateChannel}
|
|
840
719
|
hiddenActions={hiddenActions}
|
|
841
720
|
actionLabels={actionLabels}
|
|
842
721
|
actionIcons={actionIcons}
|
|
843
722
|
isOnline={getIsOnline(channel)}
|
|
723
|
+
deletedMessageLabel={deletedMessageLabel}
|
|
724
|
+
stickerMessageLabel={stickerMessageLabel}
|
|
725
|
+
photoMessageLabel={photoMessageLabel}
|
|
726
|
+
videoMessageLabel={videoMessageLabel}
|
|
727
|
+
voiceRecordingMessageLabel={voiceRecordingMessageLabel}
|
|
728
|
+
fileMessageLabel={fileMessageLabel}
|
|
729
|
+
systemMessageTranslations={systemMessageTranslations}
|
|
730
|
+
signalMessageTranslations={signalMessageTranslations}
|
|
844
731
|
/>
|
|
845
732
|
);
|
|
846
733
|
})}
|
|
@@ -865,4 +752,4 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
865
752
|
);
|
|
866
753
|
});
|
|
867
754
|
|
|
868
|
-
ChannelList.displayName = 'ChannelList';
|
|
755
|
+
ChannelList.displayName = 'ChannelList';
|