@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,14 +1,20 @@
|
|
|
1
|
-
import React, { useState,
|
|
1
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import { Virtualizer as _Virtualizer } from 'virtua';
|
|
3
|
+
const Virtualizer = _Virtualizer as any;
|
|
2
4
|
import { useChatClient } from '../../hooks/useChatClient';
|
|
3
5
|
import { useBannedState } from '../../hooks/useBannedState';
|
|
4
6
|
import { useBlockedState } from '../../hooks/useBlockedState';
|
|
7
|
+
import { usePreviewState } from '../../hooks/usePreviewState';
|
|
5
8
|
import { Avatar } from '../Avatar';
|
|
6
|
-
import {
|
|
9
|
+
import { DefaultChannelInfoTabHeader } from './ChannelInfoTabs';
|
|
10
|
+
import { useChannelInfoTabs } from './useChannelInfoTabs';
|
|
7
11
|
import { AddMemberModal } from './AddMemberModal';
|
|
8
12
|
import { EditChannelModal } from './EditChannelModal';
|
|
9
13
|
import { TopicModal } from '../TopicModal';
|
|
10
14
|
import { MessageSearchPanel } from './MessageSearchPanel';
|
|
11
15
|
import { ChannelSettingsPanel } from './ChannelSettingsPanel';
|
|
16
|
+
import { MediaLightbox } from '../MediaLightbox';
|
|
17
|
+
import { PENDING_STYLE, READY_STYLE } from './utils';
|
|
12
18
|
import type {
|
|
13
19
|
ChannelInfoProps,
|
|
14
20
|
ChannelInfoHeaderProps,
|
|
@@ -19,6 +25,22 @@ import { useChannelMembers, useChannelProfile } from '../../hooks/useChannelData
|
|
|
19
25
|
import { isGroupChannel, isTopicChannel } from '../../channelTypeUtils';
|
|
20
26
|
import { canManageChannel, CHANNEL_ROLES } from '../../channelRoleUtils';
|
|
21
27
|
|
|
28
|
+
const MemoizedVirtualizer = React.memo(({ scrollRef, startMargin, data, renderItem, overscan = 10 }: any) => (
|
|
29
|
+
<Virtualizer scrollRef={scrollRef} startMargin={startMargin} data={data} overscan={overscan}>
|
|
30
|
+
{renderItem}
|
|
31
|
+
</Virtualizer>
|
|
32
|
+
));
|
|
33
|
+
|
|
34
|
+
const PinIcon = () => (<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 4.5l-4 4l-4 1.5l-1.5 1.5l7 7l1.5 -1.5l1.5 -4l4 -4" /><path d="M9 15l-4.5 4.5" /><path d="M14.5 4l5.5 5.5" /></svg>);
|
|
35
|
+
const UnpinIcon = () => (<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 4.5l-4 4l-4 1.5l-1.5 1.5l7 7l1.5 -1.5l1.5 -4l4 -4" /><path d="M9 15l-4.5 4.5" /><path d="M14.5 4l5.5 5.5" /><line x1="3" y1="3" x2="21" y2="21" /></svg>);
|
|
36
|
+
const SearchIcon = () => (<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>);
|
|
37
|
+
const SettingsIcon = () => (<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>);
|
|
38
|
+
const DeleteIcon = () => (<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6V20a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>);
|
|
39
|
+
const LeaveIcon = () => (<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>);
|
|
40
|
+
const CloseTopicIcon = () => (<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg>);
|
|
41
|
+
const ReopenTopicIcon = () => (<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 9.9-1" /></svg>);
|
|
42
|
+
const BlockIcon = () => (<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><line x1="4.93" y1="4.93" x2="19.07" y2="19.07" /></svg>);
|
|
43
|
+
|
|
22
44
|
export const DefaultChannelInfoHeader: React.FC<ChannelInfoHeaderProps> = React.memo(({ title, onClose }) => {
|
|
23
45
|
return (
|
|
24
46
|
<div className="ermis-channel-info__header">
|
|
@@ -93,30 +115,31 @@ export const DefaultChannelInfoCover: React.FC<ChannelInfoCoverProps> = React.me
|
|
|
93
115
|
DefaultChannelInfoCover.displayName = 'DefaultChannelInfoCover';
|
|
94
116
|
|
|
95
117
|
export const DefaultChannelInfoActions: React.FC<ChannelInfoActionsProps> = React.memo(({
|
|
96
|
-
onSearchClick, onSettingsClick, onLeaveChannel, onDeleteChannel,
|
|
97
|
-
onBlockUser, onUnblockUser, onCloseTopic, onReopenTopic,
|
|
98
|
-
isTeamChannel, isTopic, isClosedTopic, isBlocked, currentUserRole,
|
|
99
|
-
searchLabel = 'Search', settingsLabel = 'Settings', deleteLabel = 'Delete', leaveLabel = 'Leave',
|
|
100
|
-
blockLabel = 'Block', unblockLabel = 'Unblock',
|
|
118
|
+
onSearchClick, onSettingsClick, onLeaveChannel, onDeleteChannel, onDeleteTopic, onTruncateChannel,
|
|
119
|
+
onBlockUser, onUnblockUser, onPin, onUnpin, onCloseTopic, onReopenTopic,
|
|
120
|
+
isTeamChannel, isTopic, isClosedTopic, isBlocked, isPinned, currentUserRole,
|
|
121
|
+
searchLabel = 'Search', settingsLabel = 'Settings', deleteLabel = 'Delete', truncateLabel = 'Clear history', leaveLabel = 'Leave',
|
|
122
|
+
blockLabel = 'Block', unblockLabel = 'Unblock', pinLabel = 'Pin', unpinLabel = 'Unpin',
|
|
123
|
+
closeTopicLabel = 'Close Topic', reopenTopicLabel = 'Reopen Topic', deleteTopicLabel = 'Delete Topic'
|
|
101
124
|
}) => {
|
|
102
125
|
return (
|
|
103
126
|
<div className="ermis-channel-info__actions">
|
|
104
127
|
<button className="ermis-channel-info__action-btn" onClick={onSearchClick} disabled={isBlocked}>
|
|
105
128
|
<div className="ermis-channel-info__action-icon">
|
|
106
|
-
<
|
|
107
|
-
<circle cx="11" cy="11" r="8"></circle>
|
|
108
|
-
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
109
|
-
</svg>
|
|
129
|
+
<SearchIcon />
|
|
110
130
|
</div>
|
|
111
131
|
<span>{searchLabel}</span>
|
|
112
132
|
</button>
|
|
133
|
+
<button className="ermis-channel-info__action-btn" onClick={isPinned ? onUnpin : onPin} disabled={isBlocked}>
|
|
134
|
+
<div className="ermis-channel-info__action-icon">
|
|
135
|
+
{isPinned ? <UnpinIcon /> : <PinIcon />}
|
|
136
|
+
</div>
|
|
137
|
+
<span>{isPinned ? unpinLabel : pinLabel}</span>
|
|
138
|
+
</button>
|
|
113
139
|
{isTeamChannel && canManageChannel(currentUserRole) && (
|
|
114
140
|
<button className="ermis-channel-info__action-btn" onClick={onSettingsClick}>
|
|
115
141
|
<div className="ermis-channel-info__action-icon">
|
|
116
|
-
<
|
|
117
|
-
<circle cx="12" cy="12" r="3"></circle>
|
|
118
|
-
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
|
119
|
-
</svg>
|
|
142
|
+
<SettingsIcon />
|
|
120
143
|
</div>
|
|
121
144
|
<span>{settingsLabel}</span>
|
|
122
145
|
</button>
|
|
@@ -125,21 +148,14 @@ export const DefaultChannelInfoActions: React.FC<ChannelInfoActionsProps> = Reac
|
|
|
125
148
|
currentUserRole === CHANNEL_ROLES.OWNER ? (
|
|
126
149
|
<button className="ermis-channel-info__action-btn ermis-channel-info__action-btn--danger" onClick={onDeleteChannel}>
|
|
127
150
|
<div className="ermis-channel-info__action-icon">
|
|
128
|
-
<
|
|
129
|
-
<polyline points="3 6 5 6 21 6"></polyline>
|
|
130
|
-
<path d="M19 6V20a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
|
131
|
-
</svg>
|
|
151
|
+
<DeleteIcon />
|
|
132
152
|
</div>
|
|
133
153
|
<span>{deleteLabel}</span>
|
|
134
154
|
</button>
|
|
135
155
|
) : (
|
|
136
156
|
<button className="ermis-channel-info__action-btn ermis-channel-info__action-btn--danger" onClick={onLeaveChannel}>
|
|
137
157
|
<div className="ermis-channel-info__action-icon">
|
|
138
|
-
<
|
|
139
|
-
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
|
140
|
-
<polyline points="16 17 21 12 16 7"></polyline>
|
|
141
|
-
<line x1="21" y1="12" x2="9" y2="12"></line>
|
|
142
|
-
</svg>
|
|
158
|
+
<LeaveIcon />
|
|
143
159
|
</div>
|
|
144
160
|
<span>{leaveLabel}</span>
|
|
145
161
|
</button>
|
|
@@ -150,48 +166,55 @@ export const DefaultChannelInfoActions: React.FC<ChannelInfoActionsProps> = Reac
|
|
|
150
166
|
isClosedTopic ? (
|
|
151
167
|
<button className="ermis-channel-info__action-btn" onClick={onReopenTopic}>
|
|
152
168
|
<div className="ermis-channel-info__action-icon">
|
|
153
|
-
<
|
|
154
|
-
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
|
155
|
-
<path d="M7 11V7a5 5 0 0 1 9.9-1" />
|
|
156
|
-
</svg>
|
|
169
|
+
<ReopenTopicIcon />
|
|
157
170
|
</div>
|
|
158
171
|
<span>{reopenTopicLabel}</span>
|
|
159
172
|
</button>
|
|
160
173
|
) : (
|
|
161
174
|
<button className="ermis-channel-info__action-btn ermis-channel-info__action-btn--danger" onClick={onCloseTopic}>
|
|
162
175
|
<div className="ermis-channel-info__action-icon">
|
|
163
|
-
<
|
|
164
|
-
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
|
165
|
-
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
166
|
-
</svg>
|
|
176
|
+
<CloseTopicIcon />
|
|
167
177
|
</div>
|
|
168
178
|
<span>{closeTopicLabel}</span>
|
|
169
179
|
</button>
|
|
170
180
|
)
|
|
171
181
|
)}
|
|
172
|
-
{/*
|
|
182
|
+
{/* Topics: Delete Topic for owner */}
|
|
183
|
+
{isTopic && currentUserRole === CHANNEL_ROLES.OWNER && onDeleteTopic && (
|
|
184
|
+
<button className="ermis-channel-info__action-btn ermis-channel-info__action-btn--danger" onClick={onDeleteTopic}>
|
|
185
|
+
<div className="ermis-channel-info__action-icon">
|
|
186
|
+
<DeleteIcon />
|
|
187
|
+
</div>
|
|
188
|
+
<span>{deleteTopicLabel}</span>
|
|
189
|
+
</button>
|
|
190
|
+
)}
|
|
191
|
+
{/* Block/Unblock & Truncate — messaging (1-1) channels only */}
|
|
173
192
|
{!isTeamChannel && !isTopic && (
|
|
174
|
-
|
|
193
|
+
<>
|
|
194
|
+
{onTruncateChannel && (
|
|
195
|
+
<button className="ermis-channel-info__action-btn ermis-channel-info__action-btn--danger" onClick={onTruncateChannel}>
|
|
196
|
+
<div className="ermis-channel-info__action-icon">
|
|
197
|
+
<DeleteIcon />
|
|
198
|
+
</div>
|
|
199
|
+
<span>{truncateLabel}</span>
|
|
200
|
+
</button>
|
|
201
|
+
)}
|
|
202
|
+
{isBlocked ? (
|
|
175
203
|
<button className="ermis-channel-info__action-btn" onClick={onUnblockUser}>
|
|
176
204
|
<div className="ermis-channel-info__action-icon">
|
|
177
|
-
<
|
|
178
|
-
<circle cx="12" cy="12" r="10" />
|
|
179
|
-
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07" />
|
|
180
|
-
</svg>
|
|
205
|
+
<BlockIcon />
|
|
181
206
|
</div>
|
|
182
207
|
<span>{unblockLabel}</span>
|
|
183
208
|
</button>
|
|
184
209
|
) : (
|
|
185
210
|
<button className="ermis-channel-info__action-btn ermis-channel-info__action-btn--danger" onClick={onBlockUser}>
|
|
186
211
|
<div className="ermis-channel-info__action-icon">
|
|
187
|
-
<
|
|
188
|
-
<circle cx="12" cy="12" r="10" />
|
|
189
|
-
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07" />
|
|
190
|
-
</svg>
|
|
212
|
+
<BlockIcon />
|
|
191
213
|
</div>
|
|
192
214
|
<span>{blockLabel}</span>
|
|
193
215
|
</button>
|
|
194
|
-
|
|
216
|
+
)}
|
|
217
|
+
</>
|
|
195
218
|
)}
|
|
196
219
|
</div>
|
|
197
220
|
);
|
|
@@ -205,16 +228,20 @@ export const ChannelInfo: React.FC<ChannelInfoProps> = React.memo((props) => {
|
|
|
205
228
|
AvatarComponent = Avatar,
|
|
206
229
|
onClose,
|
|
207
230
|
title: titleProp,
|
|
231
|
+
isVisible = true,
|
|
208
232
|
HeaderComponent = DefaultChannelInfoHeader,
|
|
209
233
|
CoverComponent = DefaultChannelInfoCover,
|
|
210
234
|
ActionsComponent = DefaultChannelInfoActions,
|
|
211
|
-
|
|
235
|
+
TabHeaderComponent = DefaultChannelInfoTabHeader,
|
|
212
236
|
AddMemberModalComponent,
|
|
213
237
|
EditChannelModalComponent,
|
|
238
|
+
EditTopicModalComponent,
|
|
214
239
|
actionsSearchLabel,
|
|
215
240
|
actionsSettingsLabel,
|
|
216
241
|
actionsDeleteLabel,
|
|
242
|
+
actionsTruncateLabel,
|
|
217
243
|
actionsLeaveLabel,
|
|
244
|
+
actionsCreateTopicLabel,
|
|
218
245
|
MemberItemComponent,
|
|
219
246
|
MediaItemComponent,
|
|
220
247
|
LinkItemComponent,
|
|
@@ -224,12 +251,17 @@ export const ChannelInfo: React.FC<ChannelInfoProps> = React.memo((props) => {
|
|
|
224
251
|
onSearchClick,
|
|
225
252
|
onLeaveChannel: onLeaveChannelProp,
|
|
226
253
|
onDeleteChannel: onDeleteChannelProp,
|
|
254
|
+
onTruncateChannel: onTruncateChannelProp,
|
|
255
|
+
onDeleteTopic: onDeleteTopicProp,
|
|
256
|
+
onCreateTopic: onCreateTopicProp,
|
|
227
257
|
onAddMemberClick,
|
|
228
258
|
onRemoveMember: onRemoveMemberProp,
|
|
229
259
|
onBanMember: onBanMemberProp,
|
|
230
260
|
onUnbanMember: onUnbanMemberProp,
|
|
231
261
|
onPromoteMember: onPromoteMemberProp,
|
|
232
262
|
onDemoteMember: onDemoteMemberProp,
|
|
263
|
+
onPinChannel: onPinChannelProp,
|
|
264
|
+
onUnpinChannel: onUnpinChannelProp,
|
|
233
265
|
// Add Member customization
|
|
234
266
|
addMemberModalTitle,
|
|
235
267
|
addMemberSearchPlaceholder,
|
|
@@ -239,6 +271,8 @@ export const ChannelInfo: React.FC<ChannelInfoProps> = React.memo((props) => {
|
|
|
239
271
|
addMemberAddingLabel,
|
|
240
272
|
addMemberAddedLabel,
|
|
241
273
|
addMemberButtonLabel,
|
|
274
|
+
MessageSearchPanelComponent,
|
|
275
|
+
ChannelSettingsPanelComponent,
|
|
242
276
|
AddMemberButtonComponent,
|
|
243
277
|
// Edit Channel customization
|
|
244
278
|
onEditChannel: onEditChannelProp,
|
|
@@ -260,18 +294,27 @@ export const ChannelInfo: React.FC<ChannelInfoProps> = React.memo((props) => {
|
|
|
260
294
|
onUnblockUser: onUnblockUserProp,
|
|
261
295
|
actionsBlockLabel,
|
|
262
296
|
actionsUnblockLabel,
|
|
297
|
+
actionsPinLabel,
|
|
298
|
+
actionsUnpinLabel,
|
|
299
|
+
actionsPinTopicLabel,
|
|
300
|
+
actionsUnpinTopicLabel,
|
|
263
301
|
actionsCloseTopicLabel,
|
|
264
302
|
actionsReopenTopicLabel,
|
|
303
|
+
actionsDeleteTopicLabel,
|
|
265
304
|
// Settings panel customizations
|
|
266
305
|
settingsWorkspaceTopicsTitle,
|
|
267
306
|
settingsTopicsFeatureName,
|
|
268
307
|
settingsTopicsFeatureDescription,
|
|
308
|
+
roleLabels,
|
|
269
309
|
} = props;
|
|
270
310
|
|
|
271
311
|
const { activeChannel, client } = useChatClient();
|
|
312
|
+
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
|
313
|
+
|
|
272
314
|
const channel = channelProp || activeChannel;
|
|
273
315
|
const { isBanned } = useBannedState(channel, client?.userID);
|
|
274
316
|
const { isBlocked } = useBlockedState(channel, client?.userID);
|
|
317
|
+
const { isPreviewMode } = usePreviewState(channel, client?.userID);
|
|
275
318
|
|
|
276
319
|
const currentUserId = client?.userID;
|
|
277
320
|
const currentUserRole = currentUserId ? channel?.state?.members?.[currentUserId]?.channel_role : undefined;
|
|
@@ -283,6 +326,14 @@ export const ChannelInfo: React.FC<ChannelInfoProps> = React.memo((props) => {
|
|
|
283
326
|
const parentCid = channel?.data?.parent_cid as string | undefined;
|
|
284
327
|
const parentChannel = parentCid && client ? client.activeChannels[parentCid] : undefined;
|
|
285
328
|
let parentChannelName = parentChannel?.data?.name || (parentCid ? 'Unknown' : undefined);
|
|
329
|
+
const e2eeChannel = parentChannel || channel;
|
|
330
|
+
const isE2ee = Boolean(e2eeChannel?.data?.mls_enabled);
|
|
331
|
+
const [isRotatingKey, setIsRotatingKey] = useState(false);
|
|
332
|
+
const [isEnablingE2ee, setIsEnablingE2ee] = useState(false);
|
|
333
|
+
const encryptionEpoch =
|
|
334
|
+
isE2ee && e2eeChannel?.cid && typeof client?.encryptionManager?.getEpoch === 'function'
|
|
335
|
+
? client.encryptionManager.getEpoch(e2eeChannel.cid)
|
|
336
|
+
: undefined;
|
|
286
337
|
|
|
287
338
|
const handleDeleteChannel = useCallback(async () => {
|
|
288
339
|
if (onDeleteChannelProp) return onDeleteChannelProp();
|
|
@@ -298,9 +349,14 @@ export const ChannelInfo: React.FC<ChannelInfoProps> = React.memo((props) => {
|
|
|
298
349
|
if (onLeaveChannelProp) return onLeaveChannelProp();
|
|
299
350
|
if (!channel || !currentUserId) return;
|
|
300
351
|
try {
|
|
301
|
-
|
|
352
|
+
if (channel.data?.mls_enabled) {
|
|
353
|
+
await channel.leaveChannelE2ee(currentUserId);
|
|
354
|
+
} else {
|
|
355
|
+
await channel.removeMembers([currentUserId]);
|
|
356
|
+
}
|
|
302
357
|
} catch (e) {
|
|
303
358
|
console.error("Error leaving channel", e);
|
|
359
|
+
throw e;
|
|
304
360
|
}
|
|
305
361
|
}, [channel, currentUserId, onLeaveChannelProp]);
|
|
306
362
|
|
|
@@ -308,9 +364,18 @@ export const ChannelInfo: React.FC<ChannelInfoProps> = React.memo((props) => {
|
|
|
308
364
|
if (onRemoveMemberProp) return onRemoveMemberProp(memberId);
|
|
309
365
|
if (!channel) return;
|
|
310
366
|
try {
|
|
311
|
-
|
|
367
|
+
const encryptionManager = channel.getClient().encryptionManager;
|
|
368
|
+
if (channel.data?.mls_enabled) {
|
|
369
|
+
if (!encryptionManager?.initialized || !channel.id || !channel.cid) {
|
|
370
|
+
throw new Error('[E2EE] Cannot remove member from E2EE channel before encryption is initialized');
|
|
371
|
+
}
|
|
372
|
+
await encryptionManager.evictMember(channel.type, channel.id, channel.cid, memberId);
|
|
373
|
+
} else {
|
|
374
|
+
await channel.removeMembers([memberId]);
|
|
375
|
+
}
|
|
312
376
|
} catch (e) {
|
|
313
377
|
console.error("Error removing member", e);
|
|
378
|
+
throw e;
|
|
314
379
|
}
|
|
315
380
|
}, [channel, onRemoveMemberProp]);
|
|
316
381
|
|
|
@@ -350,6 +415,66 @@ export const ChannelInfo: React.FC<ChannelInfoProps> = React.memo((props) => {
|
|
|
350
415
|
try { await channel.unblockUser(); } catch (e) { console.error('Error unblocking user', e); }
|
|
351
416
|
}, [channel, onUnblockUserProp]);
|
|
352
417
|
|
|
418
|
+
const handleRotateKey = useCallback(async () => {
|
|
419
|
+
if (!e2eeChannel?.cid || !client?.encryptionManager?.initialized || parentCid) return;
|
|
420
|
+
try {
|
|
421
|
+
setIsRotatingKey(true);
|
|
422
|
+
await client.encryptionManager.keyRotation(e2eeChannel.cid);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
console.error('Error rotating E2EE key', e);
|
|
425
|
+
} finally {
|
|
426
|
+
setIsRotatingKey(false);
|
|
427
|
+
}
|
|
428
|
+
}, [client, e2eeChannel?.cid, parentCid]);
|
|
429
|
+
|
|
430
|
+
const handleEnableE2ee = useCallback(async () => {
|
|
431
|
+
if (!channel?.id || !channel?.cid || !client?.encryptionManager?.initialized || parentCid || isE2ee) return;
|
|
432
|
+
try {
|
|
433
|
+
setIsEnablingE2ee(true);
|
|
434
|
+
const memberUserIds = Object.keys(channel.state?.members || {});
|
|
435
|
+
const recoveryPolicy = (channel.data as any)?.e2ee_recovery_policy || 'member_assisted';
|
|
436
|
+
const result = await client.encryptionManager.enableE2ee(
|
|
437
|
+
channel.type,
|
|
438
|
+
channel.id,
|
|
439
|
+
channel.cid,
|
|
440
|
+
memberUserIds,
|
|
441
|
+
recoveryPolicy,
|
|
442
|
+
);
|
|
443
|
+
channel.data = {
|
|
444
|
+
...channel.data,
|
|
445
|
+
mls_enabled: true,
|
|
446
|
+
e2ee_recovery_policy:
|
|
447
|
+
result?.channel?.e2ee_recovery_policy || result?.e2ee_recovery_policy || recoveryPolicy,
|
|
448
|
+
mls_enabled_at: result?.channel?.mls_enabled_at || result?.mls_enabled_at || new Date().toISOString(),
|
|
449
|
+
mls_epoch: result?.channel?.mls_epoch ?? result?.epoch ?? channel.data?.mls_epoch,
|
|
450
|
+
} as any;
|
|
451
|
+
channel.getClient().dispatchEvent({
|
|
452
|
+
type: 'channel.updated',
|
|
453
|
+
cid: channel.cid,
|
|
454
|
+
channel_type: channel.type,
|
|
455
|
+
channel_id: channel.id,
|
|
456
|
+
channel: channel.data,
|
|
457
|
+
user: channel.getClient().user,
|
|
458
|
+
} as any);
|
|
459
|
+
} catch (e) {
|
|
460
|
+
console.error('Error enabling E2EE', e);
|
|
461
|
+
} finally {
|
|
462
|
+
setIsEnablingE2ee(false);
|
|
463
|
+
}
|
|
464
|
+
}, [channel, client, parentCid, isE2ee]);
|
|
465
|
+
|
|
466
|
+
const handlePinChannel = useCallback(async () => {
|
|
467
|
+
if (onPinChannelProp) return onPinChannelProp();
|
|
468
|
+
if (!channel) return;
|
|
469
|
+
try { await channel.pin(); } catch (e) { console.error('Error pinning channel', e); }
|
|
470
|
+
}, [channel, onPinChannelProp]);
|
|
471
|
+
|
|
472
|
+
const handleUnpinChannel = useCallback(async () => {
|
|
473
|
+
if (onUnpinChannelProp) return onUnpinChannelProp();
|
|
474
|
+
if (!channel) return;
|
|
475
|
+
try { await channel.unpin(); } catch (e) { console.error('Error unpanning channel', e); }
|
|
476
|
+
}, [channel, onUnpinChannelProp]);
|
|
477
|
+
|
|
353
478
|
const handleCloseTopic = useCallback(async () => {
|
|
354
479
|
if (!channel || !parentChannel) return;
|
|
355
480
|
try { await parentChannel.closeTopic(channel.cid); } catch (e) { console.error('Error closing topic', e); }
|
|
@@ -360,8 +485,14 @@ export const ChannelInfo: React.FC<ChannelInfoProps> = React.memo((props) => {
|
|
|
360
485
|
try { await parentChannel.reopenTopic(channel.cid); } catch (e) { console.error('Error reopening topic', e); }
|
|
361
486
|
}, [channel, parentChannel]);
|
|
362
487
|
|
|
488
|
+
const handleDeleteTopic = useCallback(async () => {
|
|
489
|
+
if (onDeleteTopicProp) return onDeleteTopicProp(channel as any);
|
|
490
|
+
if (!channel) return;
|
|
491
|
+
try { await channel.delete(); } catch (e) { console.error('Error deleting topic', e); }
|
|
492
|
+
}, [channel, onDeleteTopicProp]);
|
|
493
|
+
|
|
363
494
|
const { members } = useChannelMembers(channel);
|
|
364
|
-
const { channelName: profileChannelName, channelImage, channelDescription } = useChannelProfile(channel);
|
|
495
|
+
const { channelName: profileChannelName, channelImage, channelDescription, isPinned } = useChannelProfile(channel);
|
|
365
496
|
|
|
366
497
|
let finalChannelName = profileChannelName;
|
|
367
498
|
let finalParentChannelName = parentChannelName;
|
|
@@ -397,161 +528,253 @@ export const ChannelInfo: React.FC<ChannelInfoProps> = React.memo((props) => {
|
|
|
397
528
|
setShowAddMemberModal(true);
|
|
398
529
|
}, [onAddMemberClick]);
|
|
399
530
|
|
|
400
|
-
|
|
531
|
+
// virtualizer anchors for page-level virtualization
|
|
532
|
+
const virtualizerAnchorRef = useRef<HTMLDivElement | null>(null);
|
|
533
|
+
const [startMargin, setStartMargin] = useState(0);
|
|
534
|
+
|
|
535
|
+
// Use the headless tabs hook
|
|
536
|
+
const tabs = useChannelInfoTabs({
|
|
537
|
+
channel: channel as any,
|
|
538
|
+
members: members as any,
|
|
539
|
+
AvatarComponent,
|
|
540
|
+
currentUserId,
|
|
541
|
+
currentUserRole,
|
|
542
|
+
onAddMemberClick: isTeamChannel && !isPreviewMode ? handleAddMemberClick : undefined,
|
|
543
|
+
onRemoveMember: handleRemoveMember,
|
|
544
|
+
onBanMember: handleBanMember,
|
|
545
|
+
onUnbanMember: handleUnbanMember,
|
|
546
|
+
onPromoteMember: handlePromoteMember,
|
|
547
|
+
onDemoteMember: handleDemoteMember,
|
|
548
|
+
addMemberButtonLabel,
|
|
549
|
+
AddMemberButtonComponent,
|
|
550
|
+
MemberItemComponent,
|
|
551
|
+
MediaItemComponent,
|
|
552
|
+
LinkItemComponent,
|
|
553
|
+
FileItemComponent,
|
|
554
|
+
EmptyStateComponent,
|
|
555
|
+
LoadingComponent,
|
|
556
|
+
isVisible,
|
|
557
|
+
isPreviewMode,
|
|
558
|
+
roleLabels,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Calculate startMargin for Virtualizer (distance from body top to virtualizer anchor)
|
|
562
|
+
useEffect(() => {
|
|
563
|
+
const body = scrollContainerRef.current;
|
|
564
|
+
const anchor = virtualizerAnchorRef.current;
|
|
565
|
+
if (!body || !anchor) return;
|
|
566
|
+
const bodyRect = body.getBoundingClientRect();
|
|
567
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
568
|
+
setStartMargin(anchorRect.top - bodyRect.top + body.scrollTop);
|
|
569
|
+
}, [isVisible, tabs.activeTab, isBanned, isPreviewMode]);
|
|
401
570
|
|
|
402
571
|
if (!channel) return null;
|
|
403
572
|
|
|
404
573
|
return (
|
|
405
574
|
<div className={`ermis-channel-info ${className}`.trim()}>
|
|
406
575
|
<HeaderComponent title={title} onClose={onClose} />
|
|
576
|
+
<div className="ermis-channel-info__body" ref={scrollContainerRef}>
|
|
577
|
+
<CoverComponent
|
|
578
|
+
channelName={finalChannelName}
|
|
579
|
+
channelImage={channelImage}
|
|
580
|
+
channelDescription={channelDescription}
|
|
581
|
+
AvatarComponent={AvatarComponent}
|
|
582
|
+
canEdit={canEditChannel}
|
|
583
|
+
onEditClick={handleEditChannelClick}
|
|
584
|
+
isPublic={Boolean(channel?.data?.public)}
|
|
585
|
+
isTeamChannel={isTeamChannel}
|
|
586
|
+
parentChannelName={finalParentChannelName}
|
|
587
|
+
isTopic={isTopic}
|
|
588
|
+
isE2ee={isE2ee}
|
|
589
|
+
encryptionEpoch={encryptionEpoch}
|
|
590
|
+
/>
|
|
407
591
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
<>
|
|
432
|
-
<ActionsComponent
|
|
433
|
-
onSearchClick={() => setShowSearchPanel(true)}
|
|
434
|
-
onSettingsClick={() => setShowSettingsPanel(true)}
|
|
435
|
-
onLeaveChannel={handleLeaveChannel}
|
|
436
|
-
onDeleteChannel={handleDeleteChannel}
|
|
437
|
-
onBlockUser={handleBlockUser}
|
|
438
|
-
onUnblockUser={handleUnblockUser}
|
|
439
|
-
onCloseTopic={handleCloseTopic}
|
|
440
|
-
onReopenTopic={handleReopenTopic}
|
|
441
|
-
isTeamChannel={isTeamChannel}
|
|
442
|
-
isTopic={isTopic}
|
|
443
|
-
isClosedTopic={isClosedTopic}
|
|
444
|
-
isBlocked={isBlocked}
|
|
445
|
-
currentUserRole={currentUserRole}
|
|
446
|
-
searchLabel={actionsSearchLabel}
|
|
447
|
-
settingsLabel={actionsSettingsLabel}
|
|
448
|
-
deleteLabel={actionsDeleteLabel}
|
|
449
|
-
leaveLabel={actionsLeaveLabel}
|
|
450
|
-
blockLabel={actionsBlockLabel}
|
|
451
|
-
unblockLabel={actionsUnblockLabel}
|
|
452
|
-
closeTopicLabel={actionsCloseTopicLabel}
|
|
453
|
-
reopenTopicLabel={actionsReopenTopicLabel}
|
|
454
|
-
/>
|
|
455
|
-
|
|
456
|
-
<TabsComponent
|
|
457
|
-
channel={channel}
|
|
458
|
-
members={members as any}
|
|
459
|
-
AvatarComponent={AvatarComponent}
|
|
460
|
-
currentUserId={currentUserId}
|
|
461
|
-
currentUserRole={currentUserRole}
|
|
462
|
-
onAddMemberClick={isTeamChannel ? handleAddMemberClick : undefined}
|
|
463
|
-
onRemoveMember={handleRemoveMember}
|
|
464
|
-
onBanMember={handleBanMember}
|
|
465
|
-
onUnbanMember={handleUnbanMember}
|
|
466
|
-
onPromoteMember={handlePromoteMember}
|
|
467
|
-
onDemoteMember={handleDemoteMember}
|
|
468
|
-
addMemberButtonLabel={addMemberButtonLabel}
|
|
469
|
-
AddMemberButtonComponent={AddMemberButtonComponent}
|
|
470
|
-
MemberItemComponent={MemberItemComponent}
|
|
471
|
-
MediaItemComponent={MediaItemComponent}
|
|
472
|
-
LinkItemComponent={LinkItemComponent}
|
|
473
|
-
FileItemComponent={FileItemComponent}
|
|
474
|
-
EmptyStateComponent={EmptyStateComponent}
|
|
475
|
-
LoadingComponent={LoadingComponent}
|
|
476
|
-
/>
|
|
477
|
-
|
|
478
|
-
{showAddMemberModal && (() => {
|
|
479
|
-
const ModalComp = AddMemberModalComponent || AddMemberModal;
|
|
480
|
-
return (
|
|
481
|
-
<ModalComp
|
|
592
|
+
{isBanned && (
|
|
593
|
+
<div className="ermis-channel-info__banned-banner">
|
|
594
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
595
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
596
|
+
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line>
|
|
597
|
+
</svg>
|
|
598
|
+
<span className="ermis-channel-info__banned-banner-text">You have been banned from this channel</span>
|
|
599
|
+
</div>
|
|
600
|
+
)}
|
|
601
|
+
{!isBanned && isPreviewMode && (
|
|
602
|
+
<div className="ermis-channel-info__preview-actions">
|
|
603
|
+
<button
|
|
604
|
+
className="ermis-channel-info__join-btn"
|
|
605
|
+
onClick={() => channel?.acceptInvite('join').catch(e => console.error('Failed to join public channel', e))}
|
|
606
|
+
>
|
|
607
|
+
Join Channel
|
|
608
|
+
</button>
|
|
609
|
+
</div>
|
|
610
|
+
)}
|
|
611
|
+
{!isBanned && (
|
|
612
|
+
<>
|
|
613
|
+
{!isPreviewMode && (
|
|
614
|
+
<ActionsComponent
|
|
482
615
|
channel={channel}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
616
|
+
onSearchClick={() => setShowSearchPanel(true)}
|
|
617
|
+
onSettingsClick={() => setShowSettingsPanel(true)}
|
|
618
|
+
onLeaveChannel={handleLeaveChannel}
|
|
619
|
+
onDeleteChannel={handleDeleteChannel}
|
|
620
|
+
onTruncateChannel={onTruncateChannelProp ? () => onTruncateChannelProp(channel) : undefined}
|
|
621
|
+
onBlockUser={handleBlockUser}
|
|
622
|
+
onUnblockUser={handleUnblockUser}
|
|
623
|
+
onPin={handlePinChannel}
|
|
624
|
+
onUnpin={handleUnpinChannel}
|
|
625
|
+
onCloseTopic={handleCloseTopic}
|
|
626
|
+
onReopenTopic={handleReopenTopic}
|
|
627
|
+
onDeleteTopic={handleDeleteTopic}
|
|
628
|
+
onCreateTopic={onCreateTopicProp ? () => onCreateTopicProp(channel) : undefined}
|
|
629
|
+
isTeamChannel={isTeamChannel}
|
|
630
|
+
isTopic={isTopic}
|
|
631
|
+
isClosedTopic={isClosedTopic}
|
|
632
|
+
isBlocked={isBlocked}
|
|
633
|
+
isPinned={isPinned}
|
|
634
|
+
topicsEnabled={channel?.data?.topics_enabled === true}
|
|
635
|
+
currentUserRole={currentUserRole}
|
|
636
|
+
isE2ee={isE2ee}
|
|
637
|
+
encryptionInitialized={Boolean(client?.encryptionManager?.initialized)}
|
|
638
|
+
encryptionEpoch={encryptionEpoch}
|
|
639
|
+
onRotateKey={!isTopic && isE2ee && canManageChannel(currentUserRole) ? handleRotateKey : undefined}
|
|
640
|
+
rotateKeyDisabled={isRotatingKey || isBlocked || isClosedTopic}
|
|
641
|
+
onEnableE2ee={!isTopic && !isE2ee && currentUserRole === CHANNEL_ROLES.OWNER ? handleEnableE2ee : undefined}
|
|
642
|
+
enableE2eeDisabled={isEnablingE2ee || isBlocked || isClosedTopic || !client?.encryptionManager?.initialized}
|
|
643
|
+
searchLabel={actionsSearchLabel}
|
|
644
|
+
settingsLabel={actionsSettingsLabel}
|
|
645
|
+
deleteLabel={actionsDeleteLabel}
|
|
646
|
+
truncateLabel={actionsTruncateLabel}
|
|
647
|
+
leaveLabel={actionsLeaveLabel}
|
|
648
|
+
blockLabel={actionsBlockLabel}
|
|
649
|
+
unblockLabel={actionsUnblockLabel}
|
|
650
|
+
pinLabel={isTopic ? (actionsPinTopicLabel || 'Pin topic') : (actionsPinLabel || 'Pin channel')}
|
|
651
|
+
unpinLabel={isTopic ? (actionsUnpinTopicLabel || 'Unpin topic') : (actionsUnpinLabel || 'Unpin channel')}
|
|
652
|
+
closeTopicLabel={actionsCloseTopicLabel}
|
|
653
|
+
reopenTopicLabel={actionsReopenTopicLabel}
|
|
654
|
+
deleteTopicLabel={actionsDeleteTopicLabel}
|
|
655
|
+
createTopicLabel={actionsCreateTopicLabel}
|
|
493
656
|
/>
|
|
494
|
-
)
|
|
495
|
-
|
|
657
|
+
)}
|
|
658
|
+
|
|
659
|
+
<TabHeaderComponent
|
|
660
|
+
activeTab={tabs.activeTab}
|
|
661
|
+
onTabChange={tabs.handleTabChange}
|
|
662
|
+
availableTabs={tabs.availableTabs}
|
|
663
|
+
tabCounts={{} as any}
|
|
664
|
+
/>
|
|
665
|
+
|
|
666
|
+
<div
|
|
667
|
+
ref={virtualizerAnchorRef}
|
|
668
|
+
className="ermis-channel-info__media-content"
|
|
669
|
+
style={tabs.isPending ? PENDING_STYLE : READY_STYLE}
|
|
670
|
+
>
|
|
671
|
+
{tabs.isPending || (tabs.loading && tabs.contentTab !== 'members') ? (
|
|
672
|
+
<tabs.Loading tab={tabs.activeTab} />
|
|
673
|
+
) : tabs.isTabEmpty ? (
|
|
674
|
+
<tabs.EmptyState label={tabs.emptyLabel} />
|
|
675
|
+
) : (
|
|
676
|
+
<MemoizedVirtualizer
|
|
677
|
+
scrollRef={scrollContainerRef}
|
|
678
|
+
startMargin={startMargin}
|
|
679
|
+
data={tabs.vlistData}
|
|
680
|
+
renderItem={tabs.renderVlistItem}
|
|
681
|
+
/>
|
|
682
|
+
)}
|
|
683
|
+
</div>
|
|
496
684
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
AvatarComponent={AvatarComponent}
|
|
505
|
-
title={editChannelModalTitle}
|
|
506
|
-
nameLabel={editChannelNameLabel}
|
|
507
|
-
descriptionLabel={editChannelDescriptionLabel}
|
|
508
|
-
namePlaceholder={editChannelNamePlaceholder}
|
|
509
|
-
descriptionPlaceholder={editChannelDescriptionPlaceholder}
|
|
510
|
-
publicLabel={editChannelPublicLabel}
|
|
511
|
-
saveLabel={editChannelSaveLabel}
|
|
512
|
-
cancelLabel={editChannelCancelLabel}
|
|
513
|
-
savingLabel={editChannelSavingLabel}
|
|
514
|
-
changeAvatarLabel={editChannelChangeAvatarLabel}
|
|
515
|
-
imageAccept={editChannelImageAccept}
|
|
516
|
-
maxImageSize={editChannelMaxImageSize}
|
|
517
|
-
maxImageSizeError={editChannelMaxImageSizeError}
|
|
518
|
-
/>
|
|
519
|
-
);
|
|
520
|
-
})()}
|
|
521
|
-
|
|
522
|
-
{showEditTopicModal && (() => {
|
|
523
|
-
return (
|
|
524
|
-
<TopicModal
|
|
525
|
-
isOpen={true}
|
|
526
|
-
onClose={() => setShowEditTopicModal(false)}
|
|
527
|
-
topic={channel}
|
|
685
|
+
{/* Media Lightbox */}
|
|
686
|
+
{tabs.lightboxItems.length > 0 && (
|
|
687
|
+
<MediaLightbox
|
|
688
|
+
items={tabs.lightboxItems}
|
|
689
|
+
initialIndex={tabs.lightboxIndex}
|
|
690
|
+
isOpen={tabs.lightboxOpen}
|
|
691
|
+
onClose={tabs.closeLightbox}
|
|
528
692
|
/>
|
|
529
|
-
)
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
693
|
+
)}
|
|
694
|
+
|
|
695
|
+
{showAddMemberModal && (() => {
|
|
696
|
+
const ModalComp = AddMemberModalComponent || AddMemberModal;
|
|
697
|
+
return (
|
|
698
|
+
<ModalComp
|
|
699
|
+
channel={channel}
|
|
700
|
+
currentMembers={members as any}
|
|
701
|
+
onClose={() => setShowAddMemberModal(false)}
|
|
702
|
+
AvatarComponent={AvatarComponent}
|
|
703
|
+
title={addMemberModalTitle}
|
|
704
|
+
searchPlaceholder={addMemberSearchPlaceholder}
|
|
705
|
+
loadingText={addMemberLoadingText}
|
|
706
|
+
emptyText={addMemberEmptyText}
|
|
707
|
+
addLabel={addMemberAddLabel}
|
|
708
|
+
/>
|
|
709
|
+
);
|
|
710
|
+
})()}
|
|
711
|
+
|
|
712
|
+
{showEditChannelModal && (() => {
|
|
713
|
+
const ModalComp = EditChannelModalComponent || EditChannelModal;
|
|
714
|
+
return (
|
|
715
|
+
<ModalComp
|
|
716
|
+
channel={channel}
|
|
717
|
+
onClose={() => setShowEditChannelModal(false)}
|
|
718
|
+
onSave={onEditChannelProp}
|
|
719
|
+
AvatarComponent={AvatarComponent}
|
|
720
|
+
title={editChannelModalTitle}
|
|
721
|
+
nameLabel={editChannelNameLabel}
|
|
722
|
+
descriptionLabel={editChannelDescriptionLabel}
|
|
723
|
+
namePlaceholder={editChannelNamePlaceholder}
|
|
724
|
+
descriptionPlaceholder={editChannelDescriptionPlaceholder}
|
|
725
|
+
publicLabel={editChannelPublicLabel}
|
|
726
|
+
saveLabel={editChannelSaveLabel}
|
|
727
|
+
cancelLabel={editChannelCancelLabel}
|
|
728
|
+
savingLabel={editChannelSavingLabel}
|
|
729
|
+
changeAvatarLabel={editChannelChangeAvatarLabel}
|
|
730
|
+
imageAccept={editChannelImageAccept}
|
|
731
|
+
maxImageSize={editChannelMaxImageSize}
|
|
732
|
+
maxImageSizeError={editChannelMaxImageSizeError}
|
|
733
|
+
/>
|
|
734
|
+
);
|
|
735
|
+
})()}
|
|
736
|
+
|
|
737
|
+
{showEditTopicModal && (() => {
|
|
738
|
+
const ModalComp = EditTopicModalComponent || TopicModal;
|
|
739
|
+
return (
|
|
740
|
+
<ModalComp
|
|
741
|
+
isOpen={true}
|
|
742
|
+
onClose={() => setShowEditTopicModal(false)}
|
|
743
|
+
topic={channel}
|
|
744
|
+
/>
|
|
745
|
+
);
|
|
746
|
+
})()}
|
|
747
|
+
</>
|
|
748
|
+
)}
|
|
749
|
+
</div>
|
|
533
750
|
|
|
534
751
|
{/* Search Panel — slides over entire ChannelInfo body */}
|
|
535
|
-
{channel && showSearchPanel && (
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
752
|
+
{channel && showSearchPanel && (() => {
|
|
753
|
+
const SearchPanel = MessageSearchPanelComponent || MessageSearchPanel;
|
|
754
|
+
return (
|
|
755
|
+
<SearchPanel
|
|
756
|
+
isOpen={showSearchPanel}
|
|
757
|
+
onClose={() => setShowSearchPanel(false)}
|
|
758
|
+
channel={channel}
|
|
759
|
+
AvatarComponent={AvatarComponent}
|
|
760
|
+
/>
|
|
761
|
+
);
|
|
762
|
+
})()}
|
|
543
763
|
|
|
544
764
|
{/* Settings Panel — slides over entire ChannelInfo body */}
|
|
545
|
-
{channel && showSettingsPanel && (
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
765
|
+
{channel && showSettingsPanel && (() => {
|
|
766
|
+
const SettingsPanel = ChannelSettingsPanelComponent || ChannelSettingsPanel;
|
|
767
|
+
return (
|
|
768
|
+
<SettingsPanel
|
|
769
|
+
isOpen={showSettingsPanel}
|
|
770
|
+
onClose={() => setShowSettingsPanel(false)}
|
|
771
|
+
channel={channel}
|
|
772
|
+
workspaceTopicsTitle={settingsWorkspaceTopicsTitle}
|
|
773
|
+
topicsFeatureName={settingsTopicsFeatureName}
|
|
774
|
+
topicsFeatureDescription={settingsTopicsFeatureDescription}
|
|
775
|
+
/>
|
|
776
|
+
);
|
|
777
|
+
})()}
|
|
555
778
|
</div>
|
|
556
779
|
);
|
|
557
780
|
});
|