@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.
Files changed (113) hide show
  1. package/README.md +144 -0
  2. package/dist/index.cjs +8320 -3427
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +1277 -291
  5. package/dist/index.css.map +1 -1
  6. package/dist/index.d.mts +1131 -99
  7. package/dist/index.d.ts +1131 -99
  8. package/dist/index.mjs +8168 -3319
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +9 -4
  11. package/src/channelTypeUtils.ts +1 -1
  12. package/src/components/Avatar.tsx +2 -1
  13. package/src/components/Channel.tsx +6 -5
  14. package/src/components/ChannelActions.tsx +67 -3
  15. package/src/components/ChannelHeader.tsx +27 -37
  16. package/src/components/ChannelInfo/AddMemberModal.tsx +12 -2
  17. package/src/components/ChannelInfo/ChannelInfo.tsx +410 -187
  18. package/src/components/ChannelInfo/ChannelInfoTabs.tsx +59 -297
  19. package/src/components/ChannelInfo/ChannelSettingsPanel.tsx +30 -174
  20. package/src/components/ChannelInfo/EditChannelModal.tsx +6 -3
  21. package/src/components/ChannelInfo/MediaGridItem.tsx +215 -68
  22. package/src/components/ChannelInfo/MemberListItem.tsx +2 -3
  23. package/src/components/ChannelInfo/MessageSearchPanel.tsx +27 -126
  24. package/src/components/ChannelInfo/States.tsx +1 -1
  25. package/src/components/ChannelInfo/index.ts +3 -0
  26. package/src/components/ChannelInfo/useChannelInfoTabs.tsx +427 -0
  27. package/src/components/ChannelInfo/useChannelSettings.ts +212 -0
  28. package/src/components/ChannelInfo/useMessageSearch.tsx +141 -0
  29. package/src/components/ChannelList.tsx +247 -301
  30. package/src/components/CreateChannelModal.tsx +290 -93
  31. package/src/components/Dropdown.tsx +1 -16
  32. package/src/components/EditPreview.tsx +1 -0
  33. package/src/components/ErmisCallProvider.tsx +72 -17
  34. package/src/components/ErmisCallUI.tsx +43 -20
  35. package/src/components/FilesPreview.tsx +8 -12
  36. package/src/components/FlatTopicGroupItem.tsx +243 -0
  37. package/src/components/ForwardMessageModal.tsx +43 -81
  38. package/src/components/MediaLightbox.tsx +454 -292
  39. package/src/components/MentionSuggestions.tsx +47 -35
  40. package/src/components/MessageActionsBox.tsx +6 -1
  41. package/src/components/MessageInput.tsx +165 -17
  42. package/src/components/MessageInputDefaults.tsx +127 -1
  43. package/src/components/MessageItem.tsx +155 -43
  44. package/src/components/MessageQuickReactions.tsx +153 -23
  45. package/src/components/MessageReactions.tsx +49 -3
  46. package/src/components/MessageRenderers.tsx +1114 -445
  47. package/src/components/Panel.tsx +1 -14
  48. package/src/components/PinnedMessages.tsx +55 -15
  49. package/src/components/PreviewOverlay.tsx +24 -0
  50. package/src/components/QuotedMessagePreview.tsx +99 -8
  51. package/src/components/ReadReceipts.tsx +2 -1
  52. package/src/components/RecoveryPin/RecoveryPin.tsx +279 -0
  53. package/src/components/RecoveryPin/index.ts +19 -0
  54. package/src/components/TopicList.tsx +236 -0
  55. package/src/components/TopicModal.tsx +4 -1
  56. package/src/components/TypingIndicator.tsx +17 -8
  57. package/src/components/UserPicker.tsx +94 -16
  58. package/src/components/VirtualMessageList.tsx +419 -113
  59. package/src/context/ChatComponentsContext.tsx +14 -0
  60. package/src/context/ChatProvider.tsx +44 -14
  61. package/src/context/ErmisCallContext.tsx +4 -0
  62. package/src/hooks/useChannelCapabilities.ts +7 -4
  63. package/src/hooks/useChannelData.ts +10 -3
  64. package/src/hooks/useChannelListUpdates.ts +94 -21
  65. package/src/hooks/useChannelMessages.ts +391 -42
  66. package/src/hooks/useChannelRowUpdates.ts +36 -5
  67. package/src/hooks/useChatUser.ts +39 -0
  68. package/src/hooks/useContactChannels.ts +45 -0
  69. package/src/hooks/useContactCount.ts +50 -0
  70. package/src/hooks/useDownloadHandler.ts +36 -0
  71. package/src/hooks/useDragAndDrop.ts +79 -0
  72. package/src/hooks/useE2eeAttachmentRenderer.ts +204 -0
  73. package/src/hooks/useE2eeFileUpload.ts +38 -0
  74. package/src/hooks/useFileUpload.ts +25 -5
  75. package/src/hooks/useForwardMessage.ts +309 -0
  76. package/src/hooks/useInviteChannels.ts +88 -0
  77. package/src/hooks/useInviteCount.ts +104 -0
  78. package/src/hooks/useLoadMessages.ts +16 -4
  79. package/src/hooks/useMentions.ts +60 -7
  80. package/src/hooks/useMessageActions.ts +19 -10
  81. package/src/hooks/useMessageSend.ts +64 -12
  82. package/src/hooks/usePendingE2eeSends.ts +29 -0
  83. package/src/hooks/usePendingState.ts +21 -4
  84. package/src/hooks/usePreviewState.ts +69 -0
  85. package/src/hooks/useRecoveryPin.ts +287 -0
  86. package/src/hooks/useScrollToMessage.ts +29 -4
  87. package/src/hooks/useStickerPicker.ts +62 -0
  88. package/src/hooks/useTopicGroupUpdates.ts +235 -0
  89. package/src/index.ts +79 -6
  90. package/src/messageTypeUtils.ts +27 -1
  91. package/src/styles/_base.css +0 -1
  92. package/src/styles/_call-ui.css +59 -2
  93. package/src/styles/_channel-info.css +50 -4
  94. package/src/styles/_channel-list.css +131 -68
  95. package/src/styles/_create-channel-modal.css +10 -0
  96. package/src/styles/_forward-modal.css +16 -1
  97. package/src/styles/_media-lightbox.css +67 -2
  98. package/src/styles/_mentions.css +1 -1
  99. package/src/styles/_message-actions.css +3 -4
  100. package/src/styles/_message-bubble.css +631 -112
  101. package/src/styles/_message-input.css +139 -0
  102. package/src/styles/_message-list.css +91 -18
  103. package/src/styles/_message-quick-reactions.css +105 -32
  104. package/src/styles/_message-reactions.css +22 -32
  105. package/src/styles/_modal.css +2 -1
  106. package/src/styles/_preview-overlay.css +38 -0
  107. package/src/styles/_recovery-pin.css +97 -0
  108. package/src/styles/_tokens.css +22 -20
  109. package/src/styles/_typing-indicator.css +26 -10
  110. package/src/styles/index.css +2 -0
  111. package/src/types.ts +477 -15
  112. package/src/utils/avatarColors.ts +48 -0
  113. package/src/utils.ts +219 -16
@@ -1,14 +1,20 @@
1
- import React, { useState, useEffect, useCallback, useMemo } from 'react';
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 { DefaultChannelInfoTabs } from './ChannelInfoTabs';
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', closeTopicLabel = 'Close Topic', reopenTopicLabel = 'Reopen Topic'
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
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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
- {/* Block/Unblock messaging (1-1) channels only */}
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
- isBlocked ? (
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
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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
- TabsComponent = DefaultChannelInfoTabs,
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
- await channel.removeMembers([currentUserId]);
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
- await channel.removeMembers([memberId]);
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
- <CoverComponent
409
- channelName={finalChannelName}
410
- channelImage={channelImage}
411
- channelDescription={channelDescription}
412
- AvatarComponent={AvatarComponent}
413
- canEdit={canEditChannel}
414
- onEditClick={handleEditChannelClick}
415
- isPublic={Boolean(channel?.data?.public)}
416
- isTeamChannel={isTeamChannel}
417
- parentChannelName={finalParentChannelName}
418
- isTopic={isTopic}
419
- />
420
-
421
- {isBanned && (
422
- <div className="ermis-channel-info__banned-banner">
423
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
424
- <circle cx="12" cy="12" r="10"></circle>
425
- <line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line>
426
- </svg>
427
- <span className="ermis-channel-info__banned-banner-text">You have been banned from this channel</span>
428
- </div>
429
- )}
430
- {!isBanned && (
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
- currentMembers={members as any}
484
- onClose={() => setShowAddMemberModal(false)}
485
- AvatarComponent={AvatarComponent}
486
- title={addMemberModalTitle}
487
- searchPlaceholder={addMemberSearchPlaceholder}
488
- loadingText={addMemberLoadingText}
489
- emptyText={addMemberEmptyText}
490
- addLabel={addMemberAddLabel}
491
- addingLabel={addMemberAddingLabel}
492
- addedLabel={addMemberAddedLabel}
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
- {showEditChannelModal && (() => {
498
- const EditComp = EditChannelModalComponent || EditChannelModal;
499
- return (
500
- <EditComp
501
- channel={channel}
502
- onClose={() => setShowEditChannelModal(false)}
503
- onSave={onEditChannelProp}
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
- <MessageSearchPanel
537
- isOpen={showSearchPanel}
538
- onClose={() => setShowSearchPanel(false)}
539
- channel={channel}
540
- AvatarComponent={AvatarComponent}
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
- <ChannelSettingsPanel
547
- isOpen={showSettingsPanel}
548
- onClose={() => setShowSettingsPanel(false)}
549
- channel={channel}
550
- workspaceTopicsTitle={settingsWorkspaceTopicsTitle}
551
- topicsFeatureName={settingsTopicsFeatureName}
552
- topicsFeatureDescription={settingsTopicsFeatureDescription}
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
  });