@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,11 +1,37 @@
1
1
  import React, { useState, useMemo, useCallback } from 'react';
2
- import { Modal } from './Modal';
2
+ import { Modal as DefaultModal } from './Modal';
3
3
  import { UserPicker } from './UserPicker';
4
4
  import { Avatar } from './Avatar';
5
5
  import { useChatClient } from '../hooks/useChatClient';
6
- import type { CreateChannelModalProps, UserPickerUser } from '../types';
6
+ import { useChatComponents } from '../context/ChatComponentsContext';
7
+ import { markChannelAsFullyQueried } from '../hooks/useChannelMessages';
8
+ import type { CreateChannelE2eeToggleProps, CreateChannelModalProps, UserPickerUser } from '../types';
7
9
  import { isDirectChannel } from '../channelTypeUtils';
8
10
 
11
+ const DefaultE2eeToggle: React.FC<CreateChannelE2eeToggleProps> = ({
12
+ enabled,
13
+ onChange,
14
+ disabled,
15
+ label = 'End-to-end encrypted',
16
+ description,
17
+ }) => (
18
+ <div className="ermis-create-channel__field ermis-create-channel__field--toggle">
19
+ <div>
20
+ <label className="ermis-create-channel__label">{label}</label>
21
+ {description && <div className="ermis-create-channel__hint">{description}</div>}
22
+ </div>
23
+ <button
24
+ type="button"
25
+ role="switch"
26
+ aria-checked={enabled}
27
+ className={`ermis-create-channel__toggle ${enabled ? 'ermis-create-channel__toggle--on' : ''}`}
28
+ onClick={() => onChange(!enabled)}
29
+ disabled={disabled}
30
+ >
31
+ <span className="ermis-create-channel__toggle-thumb" />
32
+ </button>
33
+ </div>
34
+ );
9
35
 
10
36
  export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
11
37
  isOpen,
@@ -26,8 +52,23 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
26
52
  createButtonLabel = 'Create',
27
53
  creatingButtonLabel = 'Creating...',
28
54
  messageButtonLabel = 'Message',
55
+ nextButtonLabel = 'Next',
56
+ backButtonLabel = 'Back',
57
+ emptyStateLabel = 'No users found',
58
+ e2eeLabel = 'End-to-end encrypted',
59
+ e2eeDescription = 'Only channel members can read encrypted messages.',
60
+ e2eeUnavailableLabel = 'E2EE is unavailable on this device.',
61
+ e2eeRecoveryPolicy = 'member_assisted',
62
+ TabsComponent,
63
+ FooterComponent,
64
+ GroupFieldsComponent,
65
+ SearchInputComponent,
66
+ SelectedBoxComponent,
67
+ E2eeToggleComponent = DefaultE2eeToggle,
29
68
  }) => {
30
69
  const { client, setActiveChannel } = useChatClient();
70
+ const { ModalComponent } = useChatComponents();
71
+ const Modal = ModalComponent || DefaultModal;
31
72
  const currentUserId = client?.userID;
32
73
 
33
74
  /* ---------- State ---------- */
@@ -38,6 +79,7 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
38
79
  const [name, setName] = useState('');
39
80
  const [description, setDescription] = useState('');
40
81
  const [isPublic, setIsPublic] = useState(false);
82
+ const [e2eeEnabled, setE2eeEnabled] = useState(false);
41
83
 
42
84
  // Users
43
85
  const [selectedUsers, setSelectedUsers] = useState<UserPickerUser[]>([]);
@@ -45,6 +87,11 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
45
87
  // Progress/Error
46
88
  const [isCreating, setIsCreating] = useState(false);
47
89
  const [error, setError] = useState<string | null>(null);
90
+ const e2eeAvailable = Boolean(client?.encryptionManager?.initialized);
91
+
92
+ const handleE2eeChange = useCallback((enabled: boolean) => {
93
+ setE2eeEnabled(enabled);
94
+ }, []);
48
95
 
49
96
  /* ---------- Exclude IDs for Direct ---------- */
50
97
  const hasExistingDirectChannel = useMemo(() => {
@@ -54,9 +101,9 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
54
101
  return Object.values(client.activeChannels).some((ch: any) => {
55
102
  if (isDirectChannel(ch) && ch.state?.members) {
56
103
  const membersList = Object.keys(ch.state.members);
57
- return membersList.length === 2 &&
58
- membersList.includes(currentUserId) &&
59
- membersList.includes(targetUserId);
104
+ return membersList.length === 2 &&
105
+ membersList.includes(currentUserId) &&
106
+ membersList.includes(targetUserId);
60
107
  }
61
108
  return false;
62
109
  });
@@ -90,9 +137,9 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
90
137
  const existingChannel = Object.values(client.activeChannels).find((ch: any) => {
91
138
  if (isDirectChannel(ch) && ch.state?.members) {
92
139
  const membersList = Object.keys(ch.state.members);
93
- return membersList.length === 2 &&
94
- membersList.includes(currentUserId) &&
95
- membersList.includes(targetUserId);
140
+ return membersList.length === 2 &&
141
+ membersList.includes(currentUserId) &&
142
+ membersList.includes(targetUserId);
96
143
  }
97
144
  return false;
98
145
  });
@@ -108,13 +155,33 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
108
155
  return;
109
156
  }
110
157
 
111
- createdChannel = client.channel('messaging', {
112
- members: [currentUserId, targetUserId],
113
- } as any);
158
+ const members = [currentUserId, targetUserId];
159
+ const payload: Record<string, any> = { members };
160
+
161
+ if (e2eeEnabled) {
162
+ const encryptionManager = client.encryptionManager;
163
+ if (!encryptionManager?.initialized) {
164
+ throw new Error(e2eeUnavailableLabel);
165
+ }
166
+ const bundle = await encryptionManager.createE2eeChannel('messaging', null, null, members);
167
+ Object.assign(payload, {
168
+ mls_enabled: true,
169
+ e2ee_recovery_policy: e2eeRecoveryPolicy,
170
+ channel_id: bundle.channel_id,
171
+ ...bundle,
172
+ });
173
+ }
174
+
175
+ createdChannel = client.channel('messaging', payload as any);
114
176
  const response = (await createdChannel.create()) as any;
115
177
  if (response?.channel?.id) {
116
178
  createdChannel = client.channel('messaging', response.channel.id);
117
- await createdChannel.watch();
179
+ await createdChannel.watch({ messages: { limit: 25, include_hidden_messages: true } });
180
+ markChannelAsFullyQueried(createdChannel.cid);
181
+ if (e2eeEnabled && client.encryptionManager?.initialized && createdChannel.id) {
182
+ client.encryptionManager.archiveCurrentEpoch(createdChannel.type, createdChannel.id)
183
+ .catch((err: unknown) => console.warn('[E2EE] Initial epoch archive failed:', err));
184
+ }
118
185
  }
119
186
  } else {
120
187
  // Group Channel
@@ -134,11 +201,36 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
134
201
  payload.description = description.trim();
135
202
  }
136
203
 
137
- createdChannel = client.channel('team', payload);
204
+ if (e2eeEnabled) {
205
+ const encryptionManager = client.encryptionManager;
206
+ if (!encryptionManager?.initialized) {
207
+ throw new Error(e2eeUnavailableLabel);
208
+ }
209
+ const uuid =
210
+ typeof crypto !== 'undefined' && 'randomUUID' in crypto
211
+ ? crypto.randomUUID()
212
+ : Math.random().toString(36).slice(2);
213
+ const channelId = `${client.projectId}:${uuid}`;
214
+ const cid = `team:${channelId}`;
215
+ const bundle = await encryptionManager.createE2eeChannel('team', channelId, cid, memberIds);
216
+ Object.assign(payload, {
217
+ mls_enabled: true,
218
+ e2ee_recovery_policy: e2eeRecoveryPolicy,
219
+ ...bundle,
220
+ });
221
+ createdChannel = client.channel('team', channelId, payload);
222
+ } else {
223
+ createdChannel = client.channel('team', payload);
224
+ }
138
225
  const response = (await createdChannel.create()) as any;
139
226
  if (response?.channel?.id) {
140
227
  createdChannel = client.channel('team', response.channel.id);
141
- await createdChannel.watch();
228
+ await createdChannel.watch({ messages: { limit: 25, include_hidden_messages: true } });
229
+ markChannelAsFullyQueried(createdChannel.cid);
230
+ if (e2eeEnabled && client.encryptionManager?.initialized && createdChannel.id) {
231
+ client.encryptionManager.archiveCurrentEpoch(createdChannel.type, createdChannel.id)
232
+ .catch((err: unknown) => console.warn('[E2EE] Initial epoch archive failed:', err));
233
+ }
142
234
  }
143
235
  }
144
236
 
@@ -158,7 +250,21 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
158
250
  } finally {
159
251
  setIsCreating(false);
160
252
  }
161
- }, [client, currentUserId, isCreating, selectedUsers, tab, name, isPublic, description, onSuccess, onClose]);
253
+ }, [
254
+ client,
255
+ currentUserId,
256
+ isCreating,
257
+ selectedUsers,
258
+ tab,
259
+ name,
260
+ isPublic,
261
+ description,
262
+ e2eeEnabled,
263
+ e2eeRecoveryPolicy,
264
+ e2eeUnavailableLabel,
265
+ onSuccess,
266
+ onClose,
267
+ ]);
162
268
 
163
269
 
164
270
  const isValid = useMemo(() => {
@@ -169,7 +275,41 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
169
275
  }, [selectedUsers, tab, name, step]);
170
276
 
171
277
  let footer;
172
- if (tab === 'messaging') {
278
+ if (FooterComponent) {
279
+ footer = (
280
+ <FooterComponent
281
+ tab={tab}
282
+ step={step}
283
+ onCancel={() => {
284
+ if (tab === 'team' && step === 2) {
285
+ setError(null);
286
+ setStep(1);
287
+ } else {
288
+ onClose();
289
+ }
290
+ }}
291
+ onNext={() => {
292
+ setError(null);
293
+ setStep(2);
294
+ }}
295
+ onBack={() => {
296
+ setError(null);
297
+ setStep(1);
298
+ }}
299
+ onCreate={handleCreate}
300
+ isCreating={isCreating}
301
+ isValid={isValid}
302
+ hasExistingDirectChannel={hasExistingDirectChannel}
303
+ cancelButtonLabel={cancelButtonLabel}
304
+ createButtonLabel={createButtonLabel}
305
+ creatingButtonLabel={creatingButtonLabel}
306
+ messageButtonLabel={messageButtonLabel}
307
+ nextButtonLabel={nextButtonLabel}
308
+ backButtonLabel={backButtonLabel}
309
+ e2eeEnabled={e2eeEnabled}
310
+ />
311
+ );
312
+ } else if (tab === 'messaging') {
173
313
  footer = (
174
314
  <div className="ermis-create-channel__footer">
175
315
  <button className="ermis-create-channel__btn ermis-create-channel__btn--cancel" onClick={onClose} disabled={isCreating}>{cancelButtonLabel}</button>
@@ -183,14 +323,14 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
183
323
  <div className="ermis-create-channel__footer">
184
324
  <button className="ermis-create-channel__btn ermis-create-channel__btn--cancel" onClick={onClose} disabled={isCreating}>{cancelButtonLabel}</button>
185
325
  <button className="ermis-create-channel__btn ermis-create-channel__btn--create" onClick={() => { setError(null); setStep(2); }} disabled={isCreating || !isValid}>
186
- Next
326
+ {nextButtonLabel}
187
327
  </button>
188
328
  </div>
189
329
  );
190
330
  } else if (tab === 'team' && step === 2) {
191
331
  footer = (
192
332
  <div className="ermis-create-channel__footer">
193
- <button className="ermis-create-channel__btn ermis-create-channel__btn--cancel" onClick={() => { setError(null); setStep(1); }} disabled={isCreating}>Back</button>
333
+ <button className="ermis-create-channel__btn ermis-create-channel__btn--cancel" onClick={() => { setError(null); setStep(1); }} disabled={isCreating}>{backButtonLabel}</button>
194
334
  <button className="ermis-create-channel__btn ermis-create-channel__btn--create" onClick={handleCreate} disabled={isCreating || !isValid}>
195
335
  {isCreating ? creatingButtonLabel : createButtonLabel}
196
336
  </button>
@@ -203,94 +343,151 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
203
343
  <div className="ermis-create-channel__body">
204
344
 
205
345
  {/* Type Toggle */}
206
- <div className="ermis-create-channel__tabs">
207
- <button
208
- className={`ermis-create-channel__tab ${tab === 'messaging' ? 'ermis-create-channel__tab--active' : ''}`}
209
- onClick={() => {
210
- setTab('messaging');
346
+ {TabsComponent ? (
347
+ <TabsComponent
348
+ activeTab={tab}
349
+ onTabChange={(t) => {
350
+ setTab(t);
211
351
  setStep(1);
212
- setSelectedUsers([]);
213
- setError(null);
352
+ setSelectedUsers([]);
353
+ setE2eeEnabled(false);
354
+ setError(null);
214
355
  }}
215
356
  disabled={isCreating}
216
- >
217
- {directTabLabel}
218
- </button>
219
- <button
220
- className={`ermis-create-channel__tab ${tab === 'team' ? 'ermis-create-channel__tab--active' : ''}`}
221
- onClick={() => {
222
- setTab('team');
223
- setStep(1);
357
+ directTabLabel={directTabLabel}
358
+ groupTabLabel={groupTabLabel}
359
+ />
360
+ ) : (
361
+ <div className="ermis-create-channel__tabs">
362
+ <button
363
+ className={`ermis-create-channel__tab ${tab === 'messaging' ? 'ermis-create-channel__tab--active' : ''}`}
364
+ onClick={() => {
365
+ setTab('messaging');
366
+ setStep(1);
224
367
  setSelectedUsers([]);
368
+ setE2eeEnabled(false);
225
369
  setError(null);
226
- }}
227
- disabled={isCreating}
228
- >
229
- {groupTabLabel}
230
- </button>
231
- </div>
370
+ }}
371
+ disabled={isCreating}
372
+ >
373
+ {directTabLabel}
374
+ </button>
375
+ <button
376
+ className={`ermis-create-channel__tab ${tab === 'team' ? 'ermis-create-channel__tab--active' : ''}`}
377
+ onClick={() => {
378
+ setTab('team');
379
+ setStep(1);
380
+ setSelectedUsers([]);
381
+ setE2eeEnabled(false);
382
+ setError(null);
383
+ }}
384
+ disabled={isCreating}
385
+ >
386
+ {groupTabLabel}
387
+ </button>
388
+ </div>
389
+ )}
232
390
 
233
391
  {/* Group Specific Fields - Step 1 */}
234
392
  {tab === 'team' && step === 1 && (
235
- <>
236
-
237
- <div className="ermis-create-channel__field">
238
- <label className="ermis-create-channel__label">{groupNameLabel} <span style={{ color: 'var(--ermis-error)' }}>*</span></label>
239
- <input
240
- className="ermis-create-channel__input"
241
- value={name}
242
- onChange={(e) => setName(e.target.value)}
243
- placeholder={groupNamePlaceholder}
244
- disabled={isCreating}
245
- maxLength={100}
246
- />
247
- </div>
248
-
249
- <div className="ermis-create-channel__field">
250
- <label className="ermis-create-channel__label">{groupDescriptionLabel}</label>
251
- <textarea
252
- className="ermis-create-channel__textarea"
253
- value={description}
254
- onChange={(e) => setDescription(e.target.value)}
255
- placeholder={groupDescriptionPlaceholder}
256
- disabled={isCreating}
257
- maxLength={500}
258
- rows={2}
393
+ GroupFieldsComponent ? (
394
+ <GroupFieldsComponent
395
+ name={name}
396
+ onNameChange={setName}
397
+ description={description}
398
+ onDescriptionChange={setDescription}
399
+ isPublic={isPublic}
400
+ onPublicChange={setIsPublic}
401
+ disabled={isCreating}
402
+ groupNameLabel={groupNameLabel}
403
+ groupNamePlaceholder={groupNamePlaceholder}
404
+ groupDescriptionLabel={groupDescriptionLabel}
405
+ groupDescriptionPlaceholder={groupDescriptionPlaceholder}
406
+ groupPublicLabel={groupPublicLabel}
407
+ e2eeEnabled={e2eeEnabled}
408
+ onE2eeChange={handleE2eeChange}
409
+ e2eeLabel={e2eeLabel}
410
+ e2eeDescription={e2eeDescription}
411
+ e2eeDisabled={!e2eeAvailable || isCreating}
412
+ />
413
+ ) : (
414
+ <>
415
+ <div className="ermis-create-channel__field">
416
+ <label className="ermis-create-channel__label">{groupNameLabel} <span style={{ color: 'var(--ermis-error)' }}>*</span></label>
417
+ <input
418
+ className="ermis-create-channel__input"
419
+ value={name}
420
+ onChange={(e) => setName(e.target.value)}
421
+ placeholder={groupNamePlaceholder}
422
+ disabled={isCreating}
423
+ maxLength={100}
424
+ />
425
+ </div>
426
+
427
+ <div className="ermis-create-channel__field">
428
+ <label className="ermis-create-channel__label">{groupDescriptionLabel}</label>
429
+ <textarea
430
+ className="ermis-create-channel__textarea"
431
+ value={description}
432
+ onChange={(e) => setDescription(e.target.value)}
433
+ placeholder={groupDescriptionPlaceholder}
434
+ disabled={isCreating}
435
+ maxLength={500}
436
+ rows={2}
437
+ />
438
+ </div>
439
+
440
+ <div className="ermis-create-channel__field ermis-create-channel__field--toggle">
441
+ <label className="ermis-create-channel__label">{groupPublicLabel}</label>
442
+ <button
443
+ type="button"
444
+ role="switch"
445
+ aria-checked={isPublic}
446
+ className={`ermis-create-channel__toggle ${isPublic ? 'ermis-create-channel__toggle--on' : ''}`}
447
+ onClick={() => setIsPublic(v => !v)}
448
+ disabled={isCreating}
449
+ >
450
+ <span className="ermis-create-channel__toggle-thumb" />
451
+ </button>
452
+ </div>
453
+
454
+ <DefaultE2eeToggle
455
+ enabled={e2eeEnabled}
456
+ onChange={handleE2eeChange}
457
+ disabled={!e2eeAvailable || isCreating}
458
+ label={e2eeLabel}
459
+ description={e2eeAvailable ? e2eeDescription : e2eeUnavailableLabel}
259
460
  />
260
- </div>
261
-
262
- <div className="ermis-create-channel__field ermis-create-channel__field--toggle">
263
- <label className="ermis-create-channel__label">{groupPublicLabel}</label>
264
- <button
265
- type="button"
266
- role="switch"
267
- aria-checked={isPublic}
268
- className={`ermis-create-channel__toggle ${isPublic ? 'ermis-create-channel__toggle--on' : ''}`}
269
- onClick={() => setIsPublic(v => !v)}
270
- disabled={isCreating}
271
- >
272
- <span className="ermis-create-channel__toggle-thumb" />
273
- </button>
274
- </div>
275
- </>
461
+ </>
462
+ )
463
+ )}
464
+
465
+ {tab === 'messaging' && (
466
+ <E2eeToggleComponent
467
+ enabled={e2eeEnabled}
468
+ onChange={handleE2eeChange}
469
+ disabled={!e2eeAvailable || isCreating}
470
+ label={e2eeLabel}
471
+ description={e2eeAvailable ? e2eeDescription : e2eeUnavailableLabel}
472
+ />
276
473
  )}
277
474
 
278
475
  {/* User Selection - Step 2 (Group) or Step 1 (Messaging) */}
279
476
  {((tab === 'team' && step === 2) || tab === 'messaging') && (
280
- <div className="ermis-create-channel__users">
281
- <div className="ermis-create-channel__users-title">
282
- Members <span style={{ color: 'var(--ermis-error)' }}>*</span>
283
- </div>
284
- <div style={{ height: tab === 'team' ? '280px' : '400px', display: 'flex', flexDirection: 'column' }}>
285
- <UserPicker
286
- mode={tab === 'messaging' ? 'radio' : 'checkbox'}
287
- onSelectionChange={setSelectedUsers}
288
- initialSelectedUsers={selectedUsers}
289
- AvatarComponent={AvatarComponent}
290
- UserItemComponent={UserItemComponent as any}
291
- searchPlaceholder={userSearchPlaceholder}
292
- />
293
- </div>
477
+ <div className={`ermis-create-channel__users ermis-create-channel__users--${tab}`}>
478
+ <UserPicker
479
+ key={tab}
480
+ mode={tab === 'messaging' ? 'radio' : 'checkbox'}
481
+ friendsOnly={tab === 'team'}
482
+ onSelectionChange={setSelectedUsers}
483
+ initialSelectedUsers={selectedUsers}
484
+ emptyText={emptyStateLabel}
485
+ AvatarComponent={AvatarComponent}
486
+ UserItemComponent={UserItemComponent as any}
487
+ SearchInputComponent={SearchInputComponent as any}
488
+ SelectedBoxComponent={SelectedBoxComponent as any}
489
+ searchPlaceholder={userSearchPlaceholder}
490
+ />
294
491
  </div>
295
492
  )}
296
493
 
@@ -9,22 +9,7 @@ export const closeAllDropdowns = () => {
9
9
  document.dispatchEvent(new CustomEvent(CLOSE_ALL_EVENT));
10
10
  };
11
11
 
12
- export interface DropdownProps {
13
- /** Whether the dropdown is open */
14
- isOpen: boolean;
15
- /** Rect from getBoundingClientRect() of the anchor element */
16
- anchorRect: DOMRect | null;
17
- /** Callback when dropdown requests to close (e.g., click outside, scroll, Escape) */
18
- onClose: () => void;
19
- /** Dropdown menu content */
20
- children: React.ReactNode;
21
- /** Horizontal alignment relative to the anchor. Default: 'left' */
22
- align?: 'left' | 'right';
23
- /** Optional custom CSS class for the container */
24
- className?: string;
25
- /** Optional custom CSS style for the container */
26
- style?: React.CSSProperties;
27
- }
12
+ import type { DropdownProps } from '../types';
28
13
 
29
14
  export const Dropdown: React.FC<DropdownProps> = ({
30
15
  isOpen,
@@ -40,6 +40,7 @@ function getAttachmentSummary(attachments: any[]): string {
40
40
  export const EditPreview: React.FC<{
41
41
  message: FormatMessageResponse;
42
42
  onDismiss: () => void;
43
+ editingMessageLabel?: string;
43
44
  }> = React.memo(({
44
45
  message,
45
46
  onDismiss,