@ermis-network/ermis-chat-react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/index.cjs +6593 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.css +3375 -0
  4. package/dist/index.css.map +1 -0
  5. package/dist/index.d.mts +1138 -0
  6. package/dist/index.d.ts +1138 -0
  7. package/dist/index.mjs +6500 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +42 -0
  10. package/src/components/Avatar.tsx +102 -0
  11. package/src/components/Channel.tsx +77 -0
  12. package/src/components/ChannelHeader.tsx +85 -0
  13. package/src/components/ChannelInfo/AddMemberModal.tsx +204 -0
  14. package/src/components/ChannelInfo/ChannelInfo.tsx +455 -0
  15. package/src/components/ChannelInfo/ChannelInfoTabs.tsx +282 -0
  16. package/src/components/ChannelInfo/ChannelSettingsPanel.tsx +479 -0
  17. package/src/components/ChannelInfo/EditChannelModal.tsx +272 -0
  18. package/src/components/ChannelInfo/FileListItem.tsx +49 -0
  19. package/src/components/ChannelInfo/LinkListItem.tsx +62 -0
  20. package/src/components/ChannelInfo/MediaGridItem.tsx +90 -0
  21. package/src/components/ChannelInfo/MemberListItem.tsx +85 -0
  22. package/src/components/ChannelInfo/MessageSearchPanel.tsx +333 -0
  23. package/src/components/ChannelInfo/States.tsx +36 -0
  24. package/src/components/ChannelInfo/index.ts +10 -0
  25. package/src/components/ChannelInfo/utils.tsx +49 -0
  26. package/src/components/ChannelList.tsx +395 -0
  27. package/src/components/Dropdown.tsx +120 -0
  28. package/src/components/EditPreview.tsx +102 -0
  29. package/src/components/FilesPreview.tsx +108 -0
  30. package/src/components/ForwardMessageModal.tsx +234 -0
  31. package/src/components/MentionSuggestions.tsx +59 -0
  32. package/src/components/MessageActionsBox.tsx +186 -0
  33. package/src/components/MessageInput.tsx +513 -0
  34. package/src/components/MessageInputDefaults.tsx +50 -0
  35. package/src/components/MessageItem.tsx +218 -0
  36. package/src/components/MessageQuickReactions.tsx +73 -0
  37. package/src/components/MessageReactions.tsx +59 -0
  38. package/src/components/MessageRenderers.tsx +565 -0
  39. package/src/components/Modal.tsx +58 -0
  40. package/src/components/Panel.tsx +64 -0
  41. package/src/components/PinnedMessages.tsx +165 -0
  42. package/src/components/QuotedMessagePreview.tsx +55 -0
  43. package/src/components/ReadReceipts.tsx +80 -0
  44. package/src/components/ReplyPreview.tsx +98 -0
  45. package/src/components/TypingIndicator.tsx +57 -0
  46. package/src/components/VirtualMessageList.tsx +425 -0
  47. package/src/context/ChatProvider.tsx +73 -0
  48. package/src/hooks/useBannedState.ts +48 -0
  49. package/src/hooks/useBlockedState.ts +55 -0
  50. package/src/hooks/useChannel.ts +18 -0
  51. package/src/hooks/useChannelCapabilities.ts +42 -0
  52. package/src/hooks/useChannelData.ts +55 -0
  53. package/src/hooks/useChannelListUpdates.ts +224 -0
  54. package/src/hooks/useChannelMessages.ts +159 -0
  55. package/src/hooks/useChannelRowUpdates.ts +78 -0
  56. package/src/hooks/useChatClient.ts +11 -0
  57. package/src/hooks/useEmojiPicker.ts +53 -0
  58. package/src/hooks/useFileUpload.ts +128 -0
  59. package/src/hooks/useLoadMessages.ts +178 -0
  60. package/src/hooks/useMentions.ts +287 -0
  61. package/src/hooks/useMessageActions.ts +87 -0
  62. package/src/hooks/useMessageSend.ts +164 -0
  63. package/src/hooks/usePendingState.ts +63 -0
  64. package/src/hooks/useScrollToMessage.ts +155 -0
  65. package/src/hooks/useTypingIndicator.ts +86 -0
  66. package/src/index.ts +129 -0
  67. package/src/styles/_add-member-modal.css +122 -0
  68. package/src/styles/_base.css +32 -0
  69. package/src/styles/_channel-info.css +941 -0
  70. package/src/styles/_channel-list.css +217 -0
  71. package/src/styles/_dropdown.css +69 -0
  72. package/src/styles/_forward-modal.css +191 -0
  73. package/src/styles/_mentions.css +102 -0
  74. package/src/styles/_message-actions.css +61 -0
  75. package/src/styles/_message-bubble.css +656 -0
  76. package/src/styles/_message-input.css +389 -0
  77. package/src/styles/_message-list.css +416 -0
  78. package/src/styles/_message-quick-reactions.css +62 -0
  79. package/src/styles/_message-reactions.css +67 -0
  80. package/src/styles/_modal.css +113 -0
  81. package/src/styles/_panel.css +69 -0
  82. package/src/styles/_pinned-messages.css +140 -0
  83. package/src/styles/_search-panel.css +219 -0
  84. package/src/styles/_tokens.css +92 -0
  85. package/src/styles/_typing-indicator.css +59 -0
  86. package/src/styles/index.css +24 -0
  87. package/src/types.ts +955 -0
  88. package/src/utils.ts +242 -0
@@ -0,0 +1,455 @@
1
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
2
+ import { useChatClient } from '../../hooks/useChatClient';
3
+ import { useBannedState } from '../../hooks/useBannedState';
4
+ import { useBlockedState } from '../../hooks/useBlockedState';
5
+ import { Avatar } from '../Avatar';
6
+ import { DefaultChannelInfoTabs } from './ChannelInfoTabs';
7
+ import { AddMemberModal } from './AddMemberModal';
8
+ import { EditChannelModal } from './EditChannelModal';
9
+ import { MessageSearchPanel } from './MessageSearchPanel';
10
+ import { ChannelSettingsPanel } from './ChannelSettingsPanel';
11
+ import type {
12
+ ChannelInfoProps,
13
+ ChannelInfoHeaderProps,
14
+ ChannelInfoCoverProps,
15
+ ChannelInfoActionsProps,
16
+ } from '../../types';
17
+ import { useChannelMembers, useChannelProfile } from '../../hooks/useChannelData';
18
+
19
+ export const DefaultChannelInfoHeader: React.FC<ChannelInfoHeaderProps> = React.memo(({ title, onClose }) => {
20
+ return (
21
+ <div className="ermis-channel-info__header">
22
+ <h3 className="ermis-channel-info__title">{title}</h3>
23
+ {onClose && (
24
+ <button className="ermis-channel-info__close" onClick={onClose} aria-label="Close">
25
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
26
+ <path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
27
+ </svg>
28
+ </button>
29
+ )}
30
+ </div>
31
+ );
32
+ });
33
+ DefaultChannelInfoHeader.displayName = 'DefaultChannelInfoHeader';
34
+
35
+ export const DefaultChannelInfoCover: React.FC<ChannelInfoCoverProps> = React.memo(({ channelName, channelImage, channelDescription, AvatarComponent, canEdit, onEditClick, isPublic, isTeamChannel }) => {
36
+ return (
37
+ <div className="ermis-channel-info__cover">
38
+ <AvatarComponent image={channelImage} name={channelName} size={80} className="ermis-channel-info__avatar" />
39
+ <div className="ermis-channel-info__name-row">
40
+ <h2 className="ermis-channel-info__name">{channelName}</h2>
41
+ {canEdit && onEditClick && (
42
+ <button className="ermis-channel-info__cover-edit-btn" onClick={onEditClick} aria-label="Edit channel">
43
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
44
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
45
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
46
+ </svg>
47
+ </button>
48
+ )}
49
+ </div>
50
+ {isTeamChannel && (
51
+ <span className={`ermis-channel-info__type-badge ${isPublic ? 'ermis-channel-info__type-badge--public' : 'ermis-channel-info__type-badge--private'}`}>
52
+ {isPublic ? (
53
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
54
+ <circle cx="12" cy="12" r="10" />
55
+ <line x1="2" y1="12" x2="22" y2="12" />
56
+ <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
57
+ </svg>
58
+ ) : (
59
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
60
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
61
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
62
+ </svg>
63
+ )}
64
+ {isPublic ? 'Public' : 'Private'}
65
+ </span>
66
+ )}
67
+ {channelDescription && (
68
+ <p className="ermis-channel-info__description">{channelDescription}</p>
69
+ )}
70
+ </div>
71
+ );
72
+ });
73
+ DefaultChannelInfoCover.displayName = 'DefaultChannelInfoCover';
74
+
75
+ export const DefaultChannelInfoActions: React.FC<ChannelInfoActionsProps> = React.memo(({
76
+ onSearchClick, onSettingsClick, onLeaveChannel, onDeleteChannel,
77
+ onBlockUser, onUnblockUser,
78
+ isTeamChannel, isBlocked, currentUserRole,
79
+ searchLabel = 'Search', settingsLabel = 'Settings', deleteLabel = 'Delete', leaveLabel = 'Leave',
80
+ blockLabel = 'Block', unblockLabel = 'Unblock'
81
+ }) => {
82
+ return (
83
+ <div className="ermis-channel-info__actions">
84
+ <button className="ermis-channel-info__action-btn" onClick={onSearchClick} disabled={isBlocked}>
85
+ <div className="ermis-channel-info__action-icon">
86
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
87
+ <circle cx="11" cy="11" r="8"></circle>
88
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
89
+ </svg>
90
+ </div>
91
+ <span>{searchLabel}</span>
92
+ </button>
93
+ {isTeamChannel && (currentUserRole === 'owner' || currentUserRole === 'moder') && (
94
+ <button className="ermis-channel-info__action-btn" onClick={onSettingsClick}>
95
+ <div className="ermis-channel-info__action-icon">
96
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
97
+ <circle cx="12" cy="12" r="3"></circle>
98
+ <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>
99
+ </svg>
100
+ </div>
101
+ <span>{settingsLabel}</span>
102
+ </button>
103
+ )}
104
+ {isTeamChannel && (
105
+ currentUserRole === 'owner' ? (
106
+ <button className="ermis-channel-info__action-btn ermis-channel-info__action-btn--danger" onClick={onDeleteChannel}>
107
+ <div className="ermis-channel-info__action-icon">
108
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
109
+ <polyline points="3 6 5 6 21 6"></polyline>
110
+ <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>
111
+ </svg>
112
+ </div>
113
+ <span>{deleteLabel}</span>
114
+ </button>
115
+ ) : (
116
+ <button className="ermis-channel-info__action-btn ermis-channel-info__action-btn--danger" onClick={onLeaveChannel}>
117
+ <div className="ermis-channel-info__action-icon">
118
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
119
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
120
+ <polyline points="16 17 21 12 16 7"></polyline>
121
+ <line x1="21" y1="12" x2="9" y2="12"></line>
122
+ </svg>
123
+ </div>
124
+ <span>{leaveLabel}</span>
125
+ </button>
126
+ )
127
+ )}
128
+ {/* Block/Unblock — messaging (1-1) channels only */}
129
+ {!isTeamChannel && (
130
+ isBlocked ? (
131
+ <button className="ermis-channel-info__action-btn" onClick={onUnblockUser}>
132
+ <div className="ermis-channel-info__action-icon">
133
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
134
+ <circle cx="12" cy="12" r="10" />
135
+ <line x1="4.93" y1="4.93" x2="19.07" y2="19.07" />
136
+ </svg>
137
+ </div>
138
+ <span>{unblockLabel}</span>
139
+ </button>
140
+ ) : (
141
+ <button className="ermis-channel-info__action-btn ermis-channel-info__action-btn--danger" onClick={onBlockUser}>
142
+ <div className="ermis-channel-info__action-icon">
143
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
144
+ <circle cx="12" cy="12" r="10" />
145
+ <line x1="4.93" y1="4.93" x2="19.07" y2="19.07" />
146
+ </svg>
147
+ </div>
148
+ <span>{blockLabel}</span>
149
+ </button>
150
+ )
151
+ )}
152
+ </div>
153
+ );
154
+ });
155
+ DefaultChannelInfoActions.displayName = 'DefaultChannelInfoActions';
156
+
157
+ export const ChannelInfo: React.FC<ChannelInfoProps> = React.memo((props) => {
158
+ const {
159
+ channel: channelProp,
160
+ className = '',
161
+ AvatarComponent = Avatar,
162
+ onClose,
163
+ title = 'Channel Info',
164
+ HeaderComponent = DefaultChannelInfoHeader,
165
+ CoverComponent = DefaultChannelInfoCover,
166
+ ActionsComponent = DefaultChannelInfoActions,
167
+ TabsComponent = DefaultChannelInfoTabs,
168
+ AddMemberModalComponent,
169
+ EditChannelModalComponent,
170
+ actionsSearchLabel,
171
+ actionsSettingsLabel,
172
+ actionsDeleteLabel,
173
+ actionsLeaveLabel,
174
+ MemberItemComponent,
175
+ MediaItemComponent,
176
+ LinkItemComponent,
177
+ FileItemComponent,
178
+ EmptyStateComponent,
179
+ LoadingComponent,
180
+ onSearchClick,
181
+ onLeaveChannel: onLeaveChannelProp,
182
+ onDeleteChannel: onDeleteChannelProp,
183
+ onAddMemberClick,
184
+ onRemoveMember: onRemoveMemberProp,
185
+ onBanMember: onBanMemberProp,
186
+ onUnbanMember: onUnbanMemberProp,
187
+ onPromoteMember: onPromoteMemberProp,
188
+ onDemoteMember: onDemoteMemberProp,
189
+ // Add Member customization
190
+ addMemberModalTitle,
191
+ addMemberSearchPlaceholder,
192
+ addMemberLoadingText,
193
+ addMemberEmptyText,
194
+ addMemberAddLabel,
195
+ addMemberAddingLabel,
196
+ addMemberAddedLabel,
197
+ addMemberButtonLabel,
198
+ AddMemberButtonComponent,
199
+ // Edit Channel customization
200
+ onEditChannel: onEditChannelProp,
201
+ editChannelModalTitle,
202
+ editChannelNameLabel,
203
+ editChannelDescriptionLabel,
204
+ editChannelNamePlaceholder,
205
+ editChannelDescriptionPlaceholder,
206
+ editChannelPublicLabel,
207
+ editChannelSaveLabel,
208
+ editChannelCancelLabel,
209
+ editChannelSavingLabel,
210
+ editChannelChangeAvatarLabel,
211
+ editChannelImageAccept,
212
+ editChannelMaxImageSize,
213
+ editChannelMaxImageSizeError,
214
+ // Block/Unblock customization (messaging channels)
215
+ onBlockUser: onBlockUserProp,
216
+ onUnblockUser: onUnblockUserProp,
217
+ actionsBlockLabel,
218
+ actionsUnblockLabel,
219
+ } = props;
220
+
221
+ const { activeChannel, client } = useChatClient();
222
+ const channel = channelProp || activeChannel;
223
+ const { isBanned } = useBannedState(channel, client?.userID);
224
+ const { isBlocked } = useBlockedState(channel, client?.userID);
225
+
226
+ const currentUserId = client?.userID;
227
+ const currentUserRole = currentUserId ? channel?.state?.members?.[currentUserId]?.channel_role : undefined;
228
+ const isTeamChannel = channel?.type === 'team';
229
+
230
+ const handleDeleteChannel = useCallback(async () => {
231
+ if (onDeleteChannelProp) return onDeleteChannelProp();
232
+ if (!channel) return;
233
+ try {
234
+ await channel.delete();
235
+ } catch (e) {
236
+ console.error("Error deleting channel", e);
237
+ }
238
+ }, [channel, onDeleteChannelProp]);
239
+
240
+ const handleLeaveChannel = useCallback(async () => {
241
+ if (onLeaveChannelProp) return onLeaveChannelProp();
242
+ if (!channel || !currentUserId) return;
243
+ try {
244
+ await channel.removeMembers([currentUserId]);
245
+ } catch (e) {
246
+ console.error("Error leaving channel", e);
247
+ }
248
+ }, [channel, currentUserId, onLeaveChannelProp]);
249
+
250
+ const handleRemoveMember = useCallback(async (memberId: string) => {
251
+ if (onRemoveMemberProp) return onRemoveMemberProp(memberId);
252
+ if (!channel) return;
253
+ try {
254
+ await channel.removeMembers([memberId]);
255
+ } catch (e) {
256
+ console.error("Error removing member", e);
257
+ }
258
+ }, [channel, onRemoveMemberProp]);
259
+
260
+ const handleBanMember = useCallback(async (memberId: string) => {
261
+ if (onBanMemberProp) return onBanMemberProp(memberId);
262
+ if (!channel) return;
263
+ try { await channel.banMembers([memberId]); } catch (e) { console.error("Error banning member", e); }
264
+ }, [channel, onBanMemberProp]);
265
+
266
+ const handleUnbanMember = useCallback(async (memberId: string) => {
267
+ if (onUnbanMemberProp) return onUnbanMemberProp(memberId);
268
+ if (!channel) return;
269
+ try { await channel.unbanMembers([memberId]); } catch (e) { console.error("Error unbanning member", e); }
270
+ }, [channel, onUnbanMemberProp]);
271
+
272
+ const handlePromoteMember = useCallback(async (memberId: string) => {
273
+ if (onPromoteMemberProp) return onPromoteMemberProp(memberId);
274
+ if (!channel) return;
275
+ try { await channel.addModerators([memberId]); } catch (e) { console.error("Error promoting member", e); }
276
+ }, [channel, onPromoteMemberProp]);
277
+
278
+ const handleDemoteMember = useCallback(async (memberId: string) => {
279
+ if (onDemoteMemberProp) return onDemoteMemberProp(memberId);
280
+ if (!channel) return;
281
+ try { await channel.demoteModerators([memberId]); } catch (e) { console.error("Error demoting member", e); }
282
+ }, [channel, onDemoteMemberProp]);
283
+
284
+ const handleBlockUser = useCallback(async () => {
285
+ if (onBlockUserProp) return onBlockUserProp();
286
+ if (!channel) return;
287
+ try { await channel.blockUser(); } catch (e) { console.error('Error blocking user', e); }
288
+ }, [channel, onBlockUserProp]);
289
+
290
+ const handleUnblockUser = useCallback(async () => {
291
+ if (onUnblockUserProp) return onUnblockUserProp();
292
+ if (!channel) return;
293
+ try { await channel.unblockUser(); } catch (e) { console.error('Error unblocking user', e); }
294
+ }, [channel, onUnblockUserProp]);
295
+
296
+ const { members } = useChannelMembers(channel);
297
+ const { channelName, channelImage, channelDescription } = useChannelProfile(channel);
298
+
299
+ const [showAddMemberModal, setShowAddMemberModal] = useState(false);
300
+ const [showEditChannelModal, setShowEditChannelModal] = useState(false);
301
+ const [showSearchPanel, setShowSearchPanel] = useState(false);
302
+ const [showSettingsPanel, setShowSettingsPanel] = useState(false);
303
+
304
+ // Permission: only owner or moderator can edit channel info (banned users cannot)
305
+ const canEditChannel = isTeamChannel && !isBanned && (currentUserRole === 'owner' || currentUserRole === 'moder');
306
+
307
+ const handleEditChannelClick = useCallback(() => {
308
+ setShowEditChannelModal(true);
309
+ }, []);
310
+
311
+ const handleAddMemberClick = useCallback(() => {
312
+ if (onAddMemberClick) return onAddMemberClick();
313
+ setShowAddMemberModal(true);
314
+ }, [onAddMemberClick]);
315
+
316
+
317
+
318
+ if (!channel) return null;
319
+
320
+ return (
321
+ <div className={`ermis-channel-info ${className}`.trim()}>
322
+ <HeaderComponent title={title} onClose={onClose} />
323
+
324
+ <CoverComponent
325
+ channelName={channelName}
326
+ channelImage={channelImage}
327
+ channelDescription={channelDescription}
328
+ AvatarComponent={AvatarComponent}
329
+ canEdit={canEditChannel}
330
+ onEditClick={handleEditChannelClick}
331
+ isPublic={Boolean(channel?.data?.public)}
332
+ isTeamChannel={isTeamChannel}
333
+ />
334
+
335
+ {isBanned ? (
336
+ <div className="ermis-channel-info__banned-banner">
337
+ <div className="ermis-channel-info__banned-banner-icon">
338
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
339
+ <circle cx="12" cy="12" r="10" />
340
+ <line x1="4.93" y1="4.93" x2="19.07" y2="19.07" />
341
+ </svg>
342
+ </div>
343
+ <span className="ermis-channel-info__banned-banner-text">You have been blocked from this channel</span>
344
+ </div>
345
+ ) : (
346
+ <>
347
+ <ActionsComponent
348
+ onSearchClick={() => setShowSearchPanel(true)}
349
+ onSettingsClick={() => setShowSettingsPanel(true)}
350
+ onLeaveChannel={handleLeaveChannel}
351
+ onDeleteChannel={handleDeleteChannel}
352
+ onBlockUser={handleBlockUser}
353
+ onUnblockUser={handleUnblockUser}
354
+ isTeamChannel={isTeamChannel}
355
+ isBlocked={isBlocked}
356
+ currentUserRole={currentUserRole}
357
+ searchLabel={actionsSearchLabel}
358
+ settingsLabel={actionsSettingsLabel}
359
+ deleteLabel={actionsDeleteLabel}
360
+ leaveLabel={actionsLeaveLabel}
361
+ blockLabel={actionsBlockLabel}
362
+ unblockLabel={actionsUnblockLabel}
363
+ />
364
+
365
+ <TabsComponent
366
+ channel={channel}
367
+ members={members as any}
368
+ AvatarComponent={AvatarComponent}
369
+ currentUserId={currentUserId}
370
+ currentUserRole={currentUserRole}
371
+ onAddMemberClick={isTeamChannel ? handleAddMemberClick : undefined}
372
+ onRemoveMember={handleRemoveMember}
373
+ onBanMember={handleBanMember}
374
+ onUnbanMember={handleUnbanMember}
375
+ onPromoteMember={handlePromoteMember}
376
+ onDemoteMember={handleDemoteMember}
377
+ addMemberButtonLabel={addMemberButtonLabel}
378
+ AddMemberButtonComponent={AddMemberButtonComponent}
379
+ MemberItemComponent={MemberItemComponent}
380
+ MediaItemComponent={MediaItemComponent}
381
+ LinkItemComponent={LinkItemComponent}
382
+ FileItemComponent={FileItemComponent}
383
+ EmptyStateComponent={EmptyStateComponent}
384
+ LoadingComponent={LoadingComponent}
385
+ />
386
+
387
+ {showAddMemberModal && (() => {
388
+ const ModalComp = AddMemberModalComponent || AddMemberModal;
389
+ return (
390
+ <ModalComp
391
+ channel={channel}
392
+ currentMembers={members as any}
393
+ onClose={() => setShowAddMemberModal(false)}
394
+ AvatarComponent={AvatarComponent}
395
+ title={addMemberModalTitle}
396
+ searchPlaceholder={addMemberSearchPlaceholder}
397
+ loadingText={addMemberLoadingText}
398
+ emptyText={addMemberEmptyText}
399
+ addLabel={addMemberAddLabel}
400
+ addingLabel={addMemberAddingLabel}
401
+ addedLabel={addMemberAddedLabel}
402
+ />
403
+ );
404
+ })()}
405
+
406
+ {showEditChannelModal && (() => {
407
+ const EditComp = EditChannelModalComponent || EditChannelModal;
408
+ return (
409
+ <EditComp
410
+ channel={channel}
411
+ onClose={() => setShowEditChannelModal(false)}
412
+ onSave={onEditChannelProp}
413
+ AvatarComponent={AvatarComponent}
414
+ title={editChannelModalTitle}
415
+ nameLabel={editChannelNameLabel}
416
+ descriptionLabel={editChannelDescriptionLabel}
417
+ namePlaceholder={editChannelNamePlaceholder}
418
+ descriptionPlaceholder={editChannelDescriptionPlaceholder}
419
+ publicLabel={editChannelPublicLabel}
420
+ saveLabel={editChannelSaveLabel}
421
+ cancelLabel={editChannelCancelLabel}
422
+ savingLabel={editChannelSavingLabel}
423
+ changeAvatarLabel={editChannelChangeAvatarLabel}
424
+ imageAccept={editChannelImageAccept}
425
+ maxImageSize={editChannelMaxImageSize}
426
+ maxImageSizeError={editChannelMaxImageSizeError}
427
+ />
428
+ );
429
+ })()}
430
+ </>
431
+ )}
432
+
433
+ {/* Search Panel — slides over entire ChannelInfo body */}
434
+ {channel && showSearchPanel && (
435
+ <MessageSearchPanel
436
+ isOpen={showSearchPanel}
437
+ onClose={() => setShowSearchPanel(false)}
438
+ channel={channel}
439
+ AvatarComponent={AvatarComponent}
440
+ />
441
+ )}
442
+
443
+ {/* Settings Panel — slides over entire ChannelInfo body */}
444
+ {channel && showSettingsPanel && (
445
+ <ChannelSettingsPanel
446
+ isOpen={showSettingsPanel}
447
+ onClose={() => setShowSettingsPanel(false)}
448
+ channel={channel}
449
+ />
450
+ )}
451
+ </div>
452
+ );
453
+ });
454
+
455
+ ChannelInfo.displayName = 'ChannelInfo';