@ermis-network/ermis-chat-react 1.0.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -0
- package/dist/index.cjs +8320 -3427
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1277 -291
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +1131 -99
- package/dist/index.d.ts +1131 -99
- package/dist/index.mjs +8168 -3319
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -4
- package/src/channelTypeUtils.ts +1 -1
- package/src/components/Avatar.tsx +2 -1
- package/src/components/Channel.tsx +6 -5
- package/src/components/ChannelActions.tsx +67 -3
- package/src/components/ChannelHeader.tsx +27 -37
- package/src/components/ChannelInfo/AddMemberModal.tsx +12 -2
- package/src/components/ChannelInfo/ChannelInfo.tsx +410 -187
- package/src/components/ChannelInfo/ChannelInfoTabs.tsx +59 -297
- package/src/components/ChannelInfo/ChannelSettingsPanel.tsx +30 -174
- package/src/components/ChannelInfo/EditChannelModal.tsx +6 -3
- package/src/components/ChannelInfo/MediaGridItem.tsx +215 -68
- package/src/components/ChannelInfo/MemberListItem.tsx +2 -3
- package/src/components/ChannelInfo/MessageSearchPanel.tsx +27 -126
- package/src/components/ChannelInfo/States.tsx +1 -1
- package/src/components/ChannelInfo/index.ts +3 -0
- package/src/components/ChannelInfo/useChannelInfoTabs.tsx +427 -0
- package/src/components/ChannelInfo/useChannelSettings.ts +212 -0
- package/src/components/ChannelInfo/useMessageSearch.tsx +141 -0
- package/src/components/ChannelList.tsx +247 -301
- package/src/components/CreateChannelModal.tsx +290 -93
- package/src/components/Dropdown.tsx +1 -16
- package/src/components/EditPreview.tsx +1 -0
- package/src/components/ErmisCallProvider.tsx +72 -17
- package/src/components/ErmisCallUI.tsx +43 -20
- package/src/components/FilesPreview.tsx +8 -12
- package/src/components/FlatTopicGroupItem.tsx +243 -0
- package/src/components/ForwardMessageModal.tsx +43 -81
- package/src/components/MediaLightbox.tsx +454 -292
- package/src/components/MentionSuggestions.tsx +47 -35
- package/src/components/MessageActionsBox.tsx +6 -1
- package/src/components/MessageInput.tsx +165 -17
- package/src/components/MessageInputDefaults.tsx +127 -1
- package/src/components/MessageItem.tsx +155 -43
- package/src/components/MessageQuickReactions.tsx +153 -23
- package/src/components/MessageReactions.tsx +49 -3
- package/src/components/MessageRenderers.tsx +1114 -445
- package/src/components/Panel.tsx +1 -14
- package/src/components/PinnedMessages.tsx +55 -15
- package/src/components/PreviewOverlay.tsx +24 -0
- package/src/components/QuotedMessagePreview.tsx +99 -8
- package/src/components/ReadReceipts.tsx +2 -1
- package/src/components/RecoveryPin/RecoveryPin.tsx +279 -0
- package/src/components/RecoveryPin/index.ts +19 -0
- package/src/components/TopicList.tsx +236 -0
- package/src/components/TopicModal.tsx +4 -1
- package/src/components/TypingIndicator.tsx +17 -8
- package/src/components/UserPicker.tsx +94 -16
- package/src/components/VirtualMessageList.tsx +419 -113
- package/src/context/ChatComponentsContext.tsx +14 -0
- package/src/context/ChatProvider.tsx +44 -14
- package/src/context/ErmisCallContext.tsx +4 -0
- package/src/hooks/useChannelCapabilities.ts +7 -4
- package/src/hooks/useChannelData.ts +10 -3
- package/src/hooks/useChannelListUpdates.ts +94 -21
- package/src/hooks/useChannelMessages.ts +391 -42
- package/src/hooks/useChannelRowUpdates.ts +36 -5
- package/src/hooks/useChatUser.ts +39 -0
- package/src/hooks/useContactChannels.ts +45 -0
- package/src/hooks/useContactCount.ts +50 -0
- package/src/hooks/useDownloadHandler.ts +36 -0
- package/src/hooks/useDragAndDrop.ts +79 -0
- package/src/hooks/useE2eeAttachmentRenderer.ts +204 -0
- package/src/hooks/useE2eeFileUpload.ts +38 -0
- package/src/hooks/useFileUpload.ts +25 -5
- package/src/hooks/useForwardMessage.ts +309 -0
- package/src/hooks/useInviteChannels.ts +88 -0
- package/src/hooks/useInviteCount.ts +104 -0
- package/src/hooks/useLoadMessages.ts +16 -4
- package/src/hooks/useMentions.ts +60 -7
- package/src/hooks/useMessageActions.ts +19 -10
- package/src/hooks/useMessageSend.ts +64 -12
- package/src/hooks/usePendingE2eeSends.ts +29 -0
- package/src/hooks/usePendingState.ts +21 -4
- package/src/hooks/usePreviewState.ts +69 -0
- package/src/hooks/useRecoveryPin.ts +287 -0
- package/src/hooks/useScrollToMessage.ts +29 -4
- package/src/hooks/useStickerPicker.ts +62 -0
- package/src/hooks/useTopicGroupUpdates.ts +235 -0
- package/src/index.ts +79 -6
- package/src/messageTypeUtils.ts +27 -1
- package/src/styles/_base.css +0 -1
- package/src/styles/_call-ui.css +59 -2
- package/src/styles/_channel-info.css +50 -4
- package/src/styles/_channel-list.css +131 -68
- package/src/styles/_create-channel-modal.css +10 -0
- package/src/styles/_forward-modal.css +16 -1
- package/src/styles/_media-lightbox.css +67 -2
- package/src/styles/_mentions.css +1 -1
- package/src/styles/_message-actions.css +3 -4
- package/src/styles/_message-bubble.css +631 -112
- package/src/styles/_message-input.css +139 -0
- package/src/styles/_message-list.css +91 -18
- package/src/styles/_message-quick-reactions.css +105 -32
- package/src/styles/_message-reactions.css +22 -32
- package/src/styles/_modal.css +2 -1
- package/src/styles/_preview-overlay.css +38 -0
- package/src/styles/_recovery-pin.css +97 -0
- package/src/styles/_tokens.css +22 -20
- package/src/styles/_typing-indicator.css +26 -10
- package/src/styles/index.css +2 -0
- package/src/types.ts +477 -15
- package/src/utils/avatarColors.ts +48 -0
- package/src/utils.ts +219 -16
|
@@ -1,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
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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 (
|
|
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
|
-
|
|
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}>
|
|
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
|
-
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
setTab(
|
|
346
|
+
{TabsComponent ? (
|
|
347
|
+
<TabsComponent
|
|
348
|
+
activeTab={tab}
|
|
349
|
+
onTabChange={(t) => {
|
|
350
|
+
setTab(t);
|
|
211
351
|
setStep(1);
|
|
212
|
-
|
|
213
|
-
|
|
352
|
+
setSelectedUsers([]);
|
|
353
|
+
setE2eeEnabled(false);
|
|
354
|
+
setError(null);
|
|
214
355
|
}}
|
|
215
356
|
disabled={isCreating}
|
|
216
|
-
|
|
217
|
-
{
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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=
|
|
281
|
-
<
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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,
|