@ermis-network/ermis-chat-react 1.0.9 → 2.0.1
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 +144 -0
- package/dist/index.cjs +8320 -3427
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1277 -291
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +1131 -99
- package/dist/index.d.ts +1131 -99
- package/dist/index.mjs +8168 -3319
- 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 -5
- package/src/components/ChannelActions.tsx +67 -3
- package/src/components/ChannelHeader.tsx +27 -37
- package/src/components/ChannelInfo/AddMemberModal.tsx +12 -2
- package/src/components/ChannelInfo/ChannelInfo.tsx +410 -187
- package/src/components/ChannelInfo/ChannelInfoTabs.tsx +59 -297
- package/src/components/ChannelInfo/ChannelSettingsPanel.tsx +30 -174
- package/src/components/ChannelInfo/EditChannelModal.tsx +6 -3
- package/src/components/ChannelInfo/MediaGridItem.tsx +215 -68
- 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 +427 -0
- package/src/components/ChannelInfo/useChannelSettings.ts +212 -0
- package/src/components/ChannelInfo/useMessageSearch.tsx +141 -0
- package/src/components/ChannelList.tsx +247 -301
- package/src/components/CreateChannelModal.tsx +290 -93
- 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/FilesPreview.tsx +8 -12
- package/src/components/FlatTopicGroupItem.tsx +243 -0
- package/src/components/ForwardMessageModal.tsx +43 -81
- package/src/components/MediaLightbox.tsx +454 -292
- package/src/components/MentionSuggestions.tsx +47 -35
- package/src/components/MessageActionsBox.tsx +6 -1
- package/src/components/MessageInput.tsx +165 -17
- package/src/components/MessageInputDefaults.tsx +127 -1
- package/src/components/MessageItem.tsx +155 -43
- package/src/components/MessageQuickReactions.tsx +153 -23
- package/src/components/MessageReactions.tsx +49 -3
- package/src/components/MessageRenderers.tsx +1114 -445
- package/src/components/Panel.tsx +1 -14
- package/src/components/PinnedMessages.tsx +55 -15
- package/src/components/PreviewOverlay.tsx +24 -0
- package/src/components/QuotedMessagePreview.tsx +99 -8
- package/src/components/ReadReceipts.tsx +2 -1
- package/src/components/RecoveryPin/RecoveryPin.tsx +279 -0
- package/src/components/RecoveryPin/index.ts +19 -0
- package/src/components/TopicList.tsx +236 -0
- package/src/components/TopicModal.tsx +4 -1
- package/src/components/TypingIndicator.tsx +17 -8
- package/src/components/UserPicker.tsx +94 -16
- package/src/components/VirtualMessageList.tsx +419 -113
- package/src/context/ChatComponentsContext.tsx +14 -0
- package/src/context/ChatProvider.tsx +44 -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 +94 -21
- package/src/hooks/useChannelMessages.ts +391 -42
- package/src/hooks/useChannelRowUpdates.ts +36 -5
- package/src/hooks/useChatUser.ts +39 -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/useE2eeAttachmentRenderer.ts +204 -0
- package/src/hooks/useE2eeFileUpload.ts +38 -0
- package/src/hooks/useFileUpload.ts +25 -5
- package/src/hooks/useForwardMessage.ts +309 -0
- package/src/hooks/useInviteChannels.ts +88 -0
- package/src/hooks/useInviteCount.ts +104 -0
- package/src/hooks/useLoadMessages.ts +16 -4
- package/src/hooks/useMentions.ts +60 -7
- package/src/hooks/useMessageActions.ts +19 -10
- package/src/hooks/useMessageSend.ts +64 -12
- package/src/hooks/usePendingE2eeSends.ts +29 -0
- package/src/hooks/usePendingState.ts +21 -4
- package/src/hooks/usePreviewState.ts +69 -0
- package/src/hooks/useRecoveryPin.ts +287 -0
- package/src/hooks/useScrollToMessage.ts +29 -4
- package/src/hooks/useStickerPicker.ts +62 -0
- package/src/hooks/useTopicGroupUpdates.ts +235 -0
- package/src/index.ts +79 -6
- package/src/messageTypeUtils.ts +27 -1
- package/src/styles/_base.css +0 -1
- package/src/styles/_call-ui.css +59 -2
- package/src/styles/_channel-info.css +50 -4
- package/src/styles/_channel-list.css +131 -68
- package/src/styles/_create-channel-modal.css +10 -0
- package/src/styles/_forward-modal.css +16 -1
- package/src/styles/_media-lightbox.css +67 -2
- package/src/styles/_mentions.css +1 -1
- package/src/styles/_message-actions.css +3 -4
- package/src/styles/_message-bubble.css +631 -112
- package/src/styles/_message-input.css +139 -0
- package/src/styles/_message-list.css +91 -18
- package/src/styles/_message-quick-reactions.css +105 -32
- package/src/styles/_message-reactions.css +22 -32
- package/src/styles/_modal.css +2 -1
- package/src/styles/_preview-overlay.css +38 -0
- package/src/styles/_recovery-pin.css +97 -0
- package/src/styles/_tokens.css +22 -20
- package/src/styles/_typing-indicator.css +26 -10
- package/src/styles/index.css +2 -0
- package/src/types.ts +477 -15
- package/src/utils/avatarColors.ts +48 -0
- package/src/utils.ts +219 -16
|
@@ -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
|
-
|
|
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
30
|
|
|
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(() => {
|
|
@@ -151,9 +89,30 @@ export const ChannelItem: React.FC<ChannelItemProps> = React.memo(({
|
|
|
151
89
|
}, [defaultActions, hiddenActions]);
|
|
152
90
|
const ActionsComponent = ChannelActionsComponent || DefaultChannelActions;
|
|
153
91
|
|
|
154
|
-
|
|
155
|
-
const
|
|
92
|
+
// For DM channels, resolve name/image from the other member if channel.data.name is missing
|
|
93
|
+
const resolvedNameImage = useMemo(() => {
|
|
94
|
+
if (channel.data?.name) {
|
|
95
|
+
return { name: channel.data.name as string, image: channel.data.image as string | undefined };
|
|
96
|
+
}
|
|
97
|
+
// For DM (messaging) channels, find the other member's info
|
|
98
|
+
if (isDirectChannel(channel) && currentUserId && channel.state?.members) {
|
|
99
|
+
const members = Object.values(channel.state.members) as any[];
|
|
100
|
+
const other = members.find((m: any) => (m.user_id || m.user?.id) !== currentUserId);
|
|
101
|
+
if (other) {
|
|
102
|
+
const otherUser = other.user || other;
|
|
103
|
+
return {
|
|
104
|
+
name: otherUser.name || otherUser.id || channel.cid,
|
|
105
|
+
image: otherUser.image || otherUser.avatar || otherUser.avatar_url,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { name: channel.cid, image: channel.data?.image as string | undefined };
|
|
110
|
+
}, [channel.data?.name, channel.data?.image, channel.state?.members, currentUserId, channel.cid, updateCount]);
|
|
111
|
+
|
|
112
|
+
const name = resolvedNameImage.name;
|
|
113
|
+
const image = resolvedNameImage.image;
|
|
156
114
|
const showUnread = hasUnread && !isActive;
|
|
115
|
+
const avatarClassName = isGroupChannel(channel) ? 'ermis-avatar-wrapper--group' : undefined;
|
|
157
116
|
|
|
158
117
|
const timestampText = useMemo(() => {
|
|
159
118
|
if (!lastMessageTimestamp) return null;
|
|
@@ -180,10 +139,15 @@ export const ChannelItem: React.FC<ChannelItemProps> = React.memo(({
|
|
|
180
139
|
return (
|
|
181
140
|
<div className={itemClass} onClick={handleClick}>
|
|
182
141
|
<div className="ermis-channel-list__item-avatar-wrapper">
|
|
183
|
-
<AvatarComponent image={image} name={name} size={
|
|
142
|
+
<AvatarComponent image={image} name={name} size={45} disableLightbox className={avatarClassName} />
|
|
184
143
|
{isOnline !== undefined && (
|
|
185
144
|
<span className={`ermis-channel-list__online-dot ermis-channel-list__online-dot--${isOnline ? 'online' : 'offline'}`} />
|
|
186
145
|
)}
|
|
146
|
+
{showUnread && unreadCount > 0 && (
|
|
147
|
+
<span className="ermis-channel-list__avatar-unread-badge">
|
|
148
|
+
{unreadCount > 99 ? '99+' : unreadCount}
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
187
151
|
</div>
|
|
188
152
|
<div className="ermis-channel-list__item-content">
|
|
189
153
|
<div className="ermis-channel-list__item-top-row">
|
|
@@ -268,6 +232,29 @@ const DefaultEmpty = React.memo(({ text }: { text?: string }) => (
|
|
|
268
232
|
));
|
|
269
233
|
DefaultEmpty.displayName = 'DefaultEmpty';
|
|
270
234
|
|
|
235
|
+
const DefaultError = React.memo(({ text, onRetry }: { text?: string; onRetry?: () => void }) => (
|
|
236
|
+
<div className="ermis-channel-list__error">
|
|
237
|
+
<div className="ermis-channel-list__error-icon">
|
|
238
|
+
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
239
|
+
<circle cx="12" cy="12" r="10" />
|
|
240
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
241
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
242
|
+
</svg>
|
|
243
|
+
</div>
|
|
244
|
+
<div className="ermis-channel-list__error-text">{text || 'Failed to load channels'}</div>
|
|
245
|
+
{onRetry && (
|
|
246
|
+
<button className="ermis-channel-list__error-retry" onClick={onRetry}>
|
|
247
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: '6px' }}>
|
|
248
|
+
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
|
|
249
|
+
<path d="M21 3v5h-5" />
|
|
250
|
+
</svg>
|
|
251
|
+
Retry
|
|
252
|
+
</button>
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
));
|
|
256
|
+
DefaultError.displayName = 'DefaultError';
|
|
257
|
+
|
|
271
258
|
/* ----------------------------------------------------------
|
|
272
259
|
Virtual Row Component to map channel and defer parsing
|
|
273
260
|
---------------------------------------------------------- */
|
|
@@ -287,13 +274,25 @@ type ChannelRowProps = {
|
|
|
287
274
|
onAddTopic?: (channel: Channel) => void;
|
|
288
275
|
onEditTopic?: (channel: Channel) => void;
|
|
289
276
|
onToggleCloseTopic?: (channel: Channel, isClosed: boolean) => void;
|
|
277
|
+
onDeleteTopic?: (channel: Channel) => void;
|
|
278
|
+
onTruncateChannel?: (channel: Channel) => void;
|
|
290
279
|
hiddenActions?: string[];
|
|
291
280
|
actionLabels?: import('../types').ChannelActionLabels;
|
|
292
281
|
actionIcons?: import('../types').ChannelActionIcons;
|
|
293
282
|
isOnline?: boolean;
|
|
283
|
+
deletedMessageLabel?: React.ReactNode;
|
|
284
|
+
stickerMessageLabel?: React.ReactNode;
|
|
285
|
+
photoMessageLabel?: React.ReactNode;
|
|
286
|
+
videoMessageLabel?: React.ReactNode;
|
|
287
|
+
voiceRecordingMessageLabel?: React.ReactNode;
|
|
288
|
+
fileMessageLabel?: React.ReactNode;
|
|
289
|
+
encryptedMessageLabel?: React.ReactNode;
|
|
290
|
+
encryptedMessageUnavailableLabel?: React.ReactNode;
|
|
291
|
+
systemMessageTranslations?: SystemMessageTranslations;
|
|
292
|
+
signalMessageTranslations?: SignalMessageTranslations;
|
|
294
293
|
};
|
|
295
294
|
|
|
296
|
-
const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
295
|
+
export const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
297
296
|
channel,
|
|
298
297
|
isActive,
|
|
299
298
|
handleSelect,
|
|
@@ -309,10 +308,22 @@ const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
|
309
308
|
onAddTopic,
|
|
310
309
|
onEditTopic,
|
|
311
310
|
onToggleCloseTopic,
|
|
311
|
+
onDeleteTopic,
|
|
312
|
+
onTruncateChannel,
|
|
312
313
|
hiddenActions,
|
|
313
314
|
actionLabels,
|
|
314
315
|
actionIcons,
|
|
315
316
|
isOnline,
|
|
317
|
+
deletedMessageLabel,
|
|
318
|
+
stickerMessageLabel,
|
|
319
|
+
photoMessageLabel,
|
|
320
|
+
videoMessageLabel,
|
|
321
|
+
voiceRecordingMessageLabel,
|
|
322
|
+
fileMessageLabel,
|
|
323
|
+
encryptedMessageLabel,
|
|
324
|
+
encryptedMessageUnavailableLabel,
|
|
325
|
+
systemMessageTranslations,
|
|
326
|
+
signalMessageTranslations,
|
|
316
327
|
}) => {
|
|
317
328
|
// Use the new custom hook to handle all row-level realtime updates
|
|
318
329
|
const { isBannedInChannel, isBlockedInChannel, updateCount } = useChannelRowUpdates(channel, currentUserId);
|
|
@@ -330,15 +341,41 @@ const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
|
330
341
|
|
|
331
342
|
// Derive last message preview computation
|
|
332
343
|
const { text: rawLastMessageText, user: rawLastMessageUser, timestamp: rawLastMessageTimestamp } = useMemo(
|
|
333
|
-
() =>
|
|
344
|
+
() =>
|
|
345
|
+
getLastMessagePreview(channel, currentUserId, {
|
|
346
|
+
deletedMessageLabel,
|
|
347
|
+
stickerMessageLabel,
|
|
348
|
+
photoMessageLabel,
|
|
349
|
+
videoMessageLabel,
|
|
350
|
+
voiceRecordingMessageLabel,
|
|
351
|
+
fileMessageLabel,
|
|
352
|
+
encryptedMessageLabel,
|
|
353
|
+
encryptedMessageUnavailableLabel,
|
|
354
|
+
systemMessageTranslations,
|
|
355
|
+
signalMessageTranslations,
|
|
356
|
+
}),
|
|
334
357
|
// Recompute if latestMessage changes or we get a force update
|
|
335
358
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
336
|
-
[
|
|
359
|
+
[
|
|
360
|
+
channel,
|
|
361
|
+
channel.state?.latestMessages,
|
|
362
|
+
updateCount,
|
|
363
|
+
deletedMessageLabel,
|
|
364
|
+
stickerMessageLabel,
|
|
365
|
+
photoMessageLabel,
|
|
366
|
+
videoMessageLabel,
|
|
367
|
+
voiceRecordingMessageLabel,
|
|
368
|
+
fileMessageLabel,
|
|
369
|
+
encryptedMessageLabel,
|
|
370
|
+
encryptedMessageUnavailableLabel,
|
|
371
|
+
systemMessageTranslations,
|
|
372
|
+
signalMessageTranslations,
|
|
373
|
+
]
|
|
337
374
|
);
|
|
338
375
|
|
|
339
376
|
// Hide last message preview when banned, blocked, pending or skipped
|
|
340
377
|
const lastMessageText = (isBannedInChannel || isBlockedInChannel || isPending || isSkipped) ? '' : rawLastMessageText;
|
|
341
|
-
const lastMessageUser = (isBannedInChannel || isBlockedInChannel || isPending || isSkipped) ? '' : rawLastMessageUser;
|
|
378
|
+
const lastMessageUser = (isBannedInChannel || isBlockedInChannel || isPending || isSkipped || isDirectChannel(channel)) ? '' : rawLastMessageUser;
|
|
342
379
|
const lastMessageTimestamp = (isBannedInChannel || isBlockedInChannel || isPending || isSkipped) ? null : rawLastMessageTimestamp;
|
|
343
380
|
|
|
344
381
|
if (renderChannel) {
|
|
@@ -371,6 +408,8 @@ const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
|
371
408
|
onAddTopic={onAddTopic}
|
|
372
409
|
onEditTopic={onEditTopic}
|
|
373
410
|
onToggleCloseTopic={onToggleCloseTopic}
|
|
411
|
+
onDeleteTopic={onDeleteTopic}
|
|
412
|
+
onTruncateChannel={onTruncateChannel}
|
|
374
413
|
hiddenActions={hiddenActions}
|
|
375
414
|
actionLabels={actionLabels}
|
|
376
415
|
actionIcons={actionIcons}
|
|
@@ -380,194 +419,17 @@ const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
|
380
419
|
});
|
|
381
420
|
ChannelRow.displayName = 'ChannelRow';
|
|
382
421
|
|
|
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
422
|
|
|
562
423
|
export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
563
|
-
filters = { type: ['messaging', 'team', 'meeting'],
|
|
424
|
+
filters = { type: ['messaging', 'team', 'meeting'], include_hidden_messages: true } as unknown as ChannelFilters,
|
|
564
425
|
sort = [],
|
|
565
|
-
options = { message_limit:
|
|
426
|
+
options = { message_limit: 1 } as unknown as ChannelListProps['options'],
|
|
566
427
|
renderChannel,
|
|
567
428
|
onChannelSelect,
|
|
568
429
|
className,
|
|
569
430
|
LoadingIndicator = DefaultLoading,
|
|
570
431
|
EmptyStateIndicator = DefaultEmpty,
|
|
432
|
+
ErrorIndicator,
|
|
571
433
|
AvatarComponent = Avatar,
|
|
572
434
|
ChannelItemComponent = ChannelItem,
|
|
573
435
|
pendingInvitesLabel,
|
|
@@ -575,11 +437,8 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
575
437
|
pendingBadgeLabel,
|
|
576
438
|
loadingLabel,
|
|
577
439
|
emptyStateLabel = 'No channels found',
|
|
440
|
+
errorLabel = 'Failed to load channels',
|
|
578
441
|
blockedBadgeLabel = 'Blocked',
|
|
579
|
-
ChannelTopicGroupComponent,
|
|
580
|
-
GeneralTopicAvatarComponent,
|
|
581
|
-
TopicAvatarComponent,
|
|
582
|
-
generalTopicLabel = 'general',
|
|
583
442
|
onAddTopic,
|
|
584
443
|
TopicEmojiPickerComponent,
|
|
585
444
|
closedTopicIcon,
|
|
@@ -587,18 +446,52 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
587
446
|
ChannelActionsComponent,
|
|
588
447
|
onEditTopic,
|
|
589
448
|
onToggleCloseTopic,
|
|
449
|
+
onDeleteTopic,
|
|
450
|
+
onTruncateChannel,
|
|
590
451
|
hiddenActions,
|
|
591
452
|
actionLabels,
|
|
592
453
|
actionIcons,
|
|
593
454
|
showOnlineStatus = true,
|
|
455
|
+
showPendingInvites = true,
|
|
456
|
+
onTopicDrillDown,
|
|
457
|
+
maxVisibleTopics,
|
|
458
|
+
moreTopicsLabel,
|
|
459
|
+
generalTopicLabel = 'general',
|
|
460
|
+
TopicPillComponent,
|
|
461
|
+
FlatTopicGroupItemComponent,
|
|
462
|
+
scrollToTopOnOwnMessage = true,
|
|
463
|
+
deletedMessageLabel,
|
|
464
|
+
stickerMessageLabel,
|
|
465
|
+
photoMessageLabel,
|
|
466
|
+
videoMessageLabel,
|
|
467
|
+
voiceRecordingMessageLabel,
|
|
468
|
+
fileMessageLabel,
|
|
469
|
+
encryptedMessageLabel,
|
|
470
|
+
encryptedMessageUnavailableLabel,
|
|
471
|
+
systemMessageTranslations,
|
|
472
|
+
signalMessageTranslations,
|
|
473
|
+
showTopicPills = false,
|
|
594
474
|
}) => {
|
|
595
475
|
const { client, activeChannel, setActiveChannel } = useChatClient();
|
|
476
|
+
const { ChannelListErrorIndicator } = useChatComponents();
|
|
477
|
+
|
|
596
478
|
const [channels, setChannels] = useState<Channel[]>([]);
|
|
597
479
|
const [loading, setLoading] = useState(true);
|
|
480
|
+
const [error, setError] = useState<any>(null);
|
|
481
|
+
|
|
482
|
+
const ActualErrorIndicator = ErrorIndicator || ChannelListErrorIndicator || DefaultError;
|
|
598
483
|
const [isPendingExpanded, setIsPendingExpanded] = useState(true);
|
|
599
484
|
const [addingTopicForChannel, setAddingTopicForChannel] = useState<Channel | null>(null);
|
|
600
485
|
const [editingTopicForChannel, setEditingTopicForChannel] = useState<Channel | null>(null);
|
|
601
486
|
|
|
487
|
+
// Ref for imperative scroll control on the virtualized list
|
|
488
|
+
const vlistRef = useRef<VListHandle>(null);
|
|
489
|
+
|
|
490
|
+
// Scroll to top when the current user sends a message
|
|
491
|
+
const handleOwnMessageNew = useCallback(() => {
|
|
492
|
+
vlistRef.current?.scrollToIndex(0);
|
|
493
|
+
}, []);
|
|
494
|
+
|
|
602
495
|
const handleAddTopicClick = useCallback((channel: Channel) => {
|
|
603
496
|
if (onAddTopic) {
|
|
604
497
|
onAddTopic(channel);
|
|
@@ -648,7 +541,7 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
648
541
|
const ms = ch.state?.membership as Record<string, unknown> | undefined;
|
|
649
542
|
const isPending = isPendingMember(ms?.channel_role as string);
|
|
650
543
|
const isSkipped = isSkippedMember(ms?.channel_role as string);
|
|
651
|
-
|
|
544
|
+
|
|
652
545
|
if (isSkipped) {
|
|
653
546
|
return; // Filter out completely
|
|
654
547
|
}
|
|
@@ -670,10 +563,12 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
670
563
|
const loadChannels = useCallback(async () => {
|
|
671
564
|
try {
|
|
672
565
|
setLoading(true);
|
|
566
|
+
setError(null);
|
|
673
567
|
const result = await client.queryChannels(filters, sort, options as { message_limit?: number });
|
|
674
568
|
setChannels(result);
|
|
675
569
|
} catch (err) {
|
|
676
570
|
console.error('Failed to load channels:', err);
|
|
571
|
+
setError(err);
|
|
677
572
|
} finally {
|
|
678
573
|
setLoading(false);
|
|
679
574
|
}
|
|
@@ -684,7 +579,7 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
684
579
|
}, [loadChannels]);
|
|
685
580
|
|
|
686
581
|
// Real-time: List manipulation (move to top, add, delete)
|
|
687
|
-
useChannelListUpdates(channels, setChannels);
|
|
582
|
+
useChannelListUpdates(channels, setChannels, scrollToTopOnOwnMessage ? handleOwnMessageNew : undefined);
|
|
688
583
|
|
|
689
584
|
// Online status: compute set of online friend user IDs (skip if disabled)
|
|
690
585
|
const onlineUsers = useOnlineUsers(showOnlineStatus ? channels : []);
|
|
@@ -714,31 +609,51 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
714
609
|
onChannelSelect?.(channel);
|
|
715
610
|
|
|
716
611
|
// Mark as read when user selects a channel (skip if banned, blocked, or pending)
|
|
717
|
-
const
|
|
718
|
-
const
|
|
612
|
+
const activeCh = client.activeChannels[channel.cid] || channel;
|
|
613
|
+
const ms = activeCh.state?.membership as Record<string, unknown> | undefined;
|
|
614
|
+
const chState = activeCh.state as unknown as Record<string, unknown> | undefined;
|
|
719
615
|
const isBannedInChannel = Boolean(ms?.banned);
|
|
720
|
-
const isBlockedInChannel = isDirectChannel(
|
|
616
|
+
const isBlockedInChannel = isDirectChannel(activeCh) && Boolean(ms?.blocked);
|
|
721
617
|
const isPending = isPendingMember(ms?.channel_role as string);
|
|
722
618
|
const isSkipped = isSkippedMember(ms?.channel_role as string);
|
|
723
619
|
|
|
724
|
-
if (!isBannedInChannel && !isBlockedInChannel && !isPending && !isSkipped
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
620
|
+
if (!isBannedInChannel && !isBlockedInChannel && !isPending && !isSkipped) {
|
|
621
|
+
let shouldUpdate = false;
|
|
622
|
+
if ((chState?.unreadCount as number) > 0) {
|
|
623
|
+
activeCh.markRead().catch(() => { });
|
|
624
|
+
// Optimistically reset unread to update UI immediately
|
|
625
|
+
if (chState) chState.unreadCount = 0;
|
|
626
|
+
shouldUpdate = true;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Also optimistic update on the stale channel just in case
|
|
630
|
+
if (channel.state && (channel.state as any).unreadCount > 0) {
|
|
631
|
+
(channel.state as any).unreadCount = 0;
|
|
632
|
+
shouldUpdate = true;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (shouldUpdate) {
|
|
636
|
+
setChannels((prev) => [...prev]);
|
|
637
|
+
}
|
|
729
638
|
}
|
|
730
639
|
},
|
|
731
640
|
[setActiveChannel, onChannelSelect, setChannels],
|
|
732
641
|
);
|
|
733
642
|
|
|
734
643
|
if (loading) return <LoadingIndicator text={loadingLabel} />;
|
|
735
|
-
if (
|
|
644
|
+
if (error) return <ActualErrorIndicator text={errorLabel} onRetry={loadChannels} />;
|
|
645
|
+
|
|
646
|
+
const isEmpty = showPendingInvites
|
|
647
|
+
? (pendingChannels.length === 0 && regularChannels.length === 0)
|
|
648
|
+
: (regularChannels.length === 0);
|
|
649
|
+
|
|
650
|
+
if (isEmpty) return <EmptyStateIndicator text={emptyStateLabel} />;
|
|
736
651
|
|
|
737
652
|
return (
|
|
738
653
|
<div className={`ermis-channel-list${className ? ` ${className}` : ''}`}>
|
|
739
654
|
{/* VList requires its container to have a height to work. */}
|
|
740
|
-
<VList style={{ height: '100%' }}>
|
|
741
|
-
{pendingChannels.length > 0 && (
|
|
655
|
+
<VList ref={vlistRef} style={{ height: '100%' }}>
|
|
656
|
+
{showPendingInvites && pendingChannels.length > 0 && (
|
|
742
657
|
<div
|
|
743
658
|
className="ermis-channel-list__accordion-header"
|
|
744
659
|
onClick={() => setIsPendingExpanded(prev => !prev)}
|
|
@@ -756,7 +671,7 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
756
671
|
</svg>
|
|
757
672
|
</div>
|
|
758
673
|
)}
|
|
759
|
-
{isPendingExpanded && pendingChannels.map((channel: Channel) => {
|
|
674
|
+
{showPendingInvites && isPendingExpanded && pendingChannels.map((channel: Channel) => {
|
|
760
675
|
const isActive = activeChannel?.cid === channel.cid;
|
|
761
676
|
return (
|
|
762
677
|
<ChannelRow
|
|
@@ -773,48 +688,67 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
773
688
|
closedTopicIcon={closedTopicIcon}
|
|
774
689
|
PinnedIconComponent={PinnedIconComponent}
|
|
775
690
|
ChannelActionsComponent={ChannelActionsComponent}
|
|
691
|
+
onTruncateChannel={onTruncateChannel}
|
|
776
692
|
hiddenActions={hiddenActions}
|
|
777
693
|
actionLabels={actionLabels}
|
|
778
694
|
actionIcons={actionIcons}
|
|
779
695
|
isOnline={getIsOnline(channel)}
|
|
696
|
+
deletedMessageLabel={deletedMessageLabel}
|
|
697
|
+
stickerMessageLabel={stickerMessageLabel}
|
|
698
|
+
photoMessageLabel={photoMessageLabel}
|
|
699
|
+
videoMessageLabel={videoMessageLabel}
|
|
700
|
+
voiceRecordingMessageLabel={voiceRecordingMessageLabel}
|
|
701
|
+
fileMessageLabel={fileMessageLabel}
|
|
702
|
+
encryptedMessageLabel={encryptedMessageLabel}
|
|
703
|
+
encryptedMessageUnavailableLabel={encryptedMessageUnavailableLabel}
|
|
704
|
+
systemMessageTranslations={systemMessageTranslations}
|
|
705
|
+
signalMessageTranslations={signalMessageTranslations}
|
|
780
706
|
/>
|
|
781
707
|
);
|
|
782
708
|
})}
|
|
783
|
-
{pendingChannels.length > 0 && regularChannels.length > 0 && (
|
|
709
|
+
{/* {pendingChannels.length > 0 && regularChannels.length > 0 && (
|
|
784
710
|
<div className="ermis-channel-list__accordion-header ermis-channel-list__accordion-header--static">
|
|
785
711
|
<span>{channelsLabel}</span>
|
|
786
712
|
</div>
|
|
787
|
-
)}
|
|
713
|
+
)} */}
|
|
788
714
|
{regularChannels.map((channel: Channel) => {
|
|
789
|
-
const isActive = activeChannel?.cid === channel.cid
|
|
715
|
+
const isActive = activeChannel?.cid === channel.cid ||
|
|
716
|
+
(activeChannel?.data?.parent_cid === channel.cid);
|
|
790
717
|
const isTeamWithTopics = hasTopicsEnabled(channel);
|
|
791
718
|
|
|
792
719
|
if (isTeamWithTopics) {
|
|
793
|
-
|
|
720
|
+
// Drill-down mode: always render flat item with topic pills + last msg
|
|
721
|
+
const FlatComponent = FlatTopicGroupItemComponent || FlatTopicGroupItem;
|
|
794
722
|
return (
|
|
795
|
-
<
|
|
723
|
+
<FlatComponent
|
|
796
724
|
key={channel.cid}
|
|
797
725
|
channel={channel}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
726
|
+
isActive={isActive}
|
|
727
|
+
onDrillDown={(c) => {
|
|
728
|
+
handleSelect(c);
|
|
729
|
+
if (onTopicDrillDown) onTopicDrillDown(c);
|
|
730
|
+
}}
|
|
802
731
|
AvatarComponent={AvatarComponent}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
currentUserId={client.userID}
|
|
806
|
-
pendingBadgeLabel={pendingBadgeLabel}
|
|
807
|
-
blockedBadgeLabel={blockedBadgeLabel}
|
|
732
|
+
maxVisibleTopics={maxVisibleTopics}
|
|
733
|
+
moreTopicsLabel={moreTopicsLabel}
|
|
808
734
|
generalTopicLabel={generalTopicLabel}
|
|
809
|
-
|
|
810
|
-
closedTopicIcon={closedTopicIcon}
|
|
735
|
+
TopicPillComponent={TopicPillComponent}
|
|
811
736
|
PinnedIconComponent={PinnedIconComponent}
|
|
812
737
|
ChannelActionsComponent={ChannelActionsComponent}
|
|
813
|
-
|
|
814
|
-
|
|
738
|
+
onAddTopic={handleAddTopicClick}
|
|
739
|
+
onTruncateChannel={onTruncateChannel}
|
|
815
740
|
hiddenActions={hiddenActions}
|
|
816
741
|
actionLabels={actionLabels}
|
|
817
742
|
actionIcons={actionIcons}
|
|
743
|
+
deletedMessageLabel={deletedMessageLabel}
|
|
744
|
+
stickerMessageLabel={stickerMessageLabel}
|
|
745
|
+
photoMessageLabel={photoMessageLabel}
|
|
746
|
+
videoMessageLabel={videoMessageLabel}
|
|
747
|
+
voiceRecordingMessageLabel={voiceRecordingMessageLabel}
|
|
748
|
+
fileMessageLabel={fileMessageLabel}
|
|
749
|
+
systemMessageTranslations={systemMessageTranslations}
|
|
750
|
+
signalMessageTranslations={signalMessageTranslations}
|
|
751
|
+
showTopicPills={showTopicPills}
|
|
818
752
|
/>
|
|
819
753
|
);
|
|
820
754
|
}
|
|
@@ -837,10 +771,22 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
837
771
|
onAddTopic={handleAddTopicClick}
|
|
838
772
|
onEditTopic={handleEditTopicClick}
|
|
839
773
|
onToggleCloseTopic={handleToggleCloseTopicClick}
|
|
774
|
+
onDeleteTopic={onDeleteTopic}
|
|
775
|
+
onTruncateChannel={onTruncateChannel}
|
|
840
776
|
hiddenActions={hiddenActions}
|
|
841
777
|
actionLabels={actionLabels}
|
|
842
778
|
actionIcons={actionIcons}
|
|
843
779
|
isOnline={getIsOnline(channel)}
|
|
780
|
+
deletedMessageLabel={deletedMessageLabel}
|
|
781
|
+
stickerMessageLabel={stickerMessageLabel}
|
|
782
|
+
photoMessageLabel={photoMessageLabel}
|
|
783
|
+
videoMessageLabel={videoMessageLabel}
|
|
784
|
+
voiceRecordingMessageLabel={voiceRecordingMessageLabel}
|
|
785
|
+
fileMessageLabel={fileMessageLabel}
|
|
786
|
+
encryptedMessageLabel={encryptedMessageLabel}
|
|
787
|
+
encryptedMessageUnavailableLabel={encryptedMessageUnavailableLabel}
|
|
788
|
+
systemMessageTranslations={systemMessageTranslations}
|
|
789
|
+
signalMessageTranslations={signalMessageTranslations}
|
|
844
790
|
/>
|
|
845
791
|
);
|
|
846
792
|
})}
|
|
@@ -865,4 +811,4 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
865
811
|
);
|
|
866
812
|
});
|
|
867
813
|
|
|
868
|
-
ChannelList.displayName = 'ChannelList';
|
|
814
|
+
ChannelList.displayName = 'ChannelList';
|