@ermis-network/ermis-chat-react 1.0.1 → 1.0.2
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/dist/index.cjs +752 -333
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +382 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +88 -1
- package/dist/index.d.ts +88 -1
- package/dist/index.mjs +691 -274
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/ChannelInfo/AddMemberModal.tsx +48 -174
- package/src/components/ChannelList.tsx +5 -0
- package/src/components/CreateChannelModal.tsx +274 -0
- package/src/components/UserPicker.tsx +377 -0
- package/src/index.ts +11 -0
- package/src/styles/_create-channel-modal.css +183 -0
- package/src/styles/_user-picker.css +268 -0
- package/src/styles/index.css +3 -0
- package/src/types.ts +100 -0
package/package.json
CHANGED
|
@@ -1,58 +1,14 @@
|
|
|
1
|
-
import React, { useState,
|
|
2
|
-
import { useChatClient } from '../../hooks/useChatClient';
|
|
1
|
+
import React, { useState, useMemo, useCallback } from 'react';
|
|
3
2
|
import { Modal } from '../Modal';
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
/* ---------- Static styles hoisted outside render ---------- */
|
|
8
|
-
const LIST_CONTAINER_STYLE: React.CSSProperties = { overflow: 'hidden', height: '400px' };
|
|
9
|
-
const VLIST_STYLE: React.CSSProperties = { height: '100%' };
|
|
10
|
-
const DISABLED_BTN_STYLE: React.CSSProperties = { opacity: 0.5, cursor: 'not-allowed' };
|
|
11
|
-
|
|
12
|
-
/* ---------- Default user row ---------- */
|
|
13
|
-
const DefaultUserItem: React.FC<AddMemberUserItemProps> = React.memo(({
|
|
14
|
-
user, isExisting, isAdding, onAdd, AvatarComponent,
|
|
15
|
-
addedLabel = 'Added', addingLabel = 'Adding...', addLabel = 'Add',
|
|
16
|
-
}) => (
|
|
17
|
-
<div className="ermis-modal-user-item">
|
|
18
|
-
<AvatarComponent image={user.avatar} name={user.name || user.id} size={36} />
|
|
19
|
-
<div className="ermis-modal-user-info">
|
|
20
|
-
<span className="ermis-modal-user-name">{user.name || user.id}</span>
|
|
21
|
-
</div>
|
|
22
|
-
<button
|
|
23
|
-
className={`ermis-modal-add-btn ${isExisting ? 'ermis-modal-add-btn--disabled' : ''}`}
|
|
24
|
-
onClick={() => onAdd(user.id)}
|
|
25
|
-
disabled={isAdding || isExisting}
|
|
26
|
-
style={isExisting ? DISABLED_BTN_STYLE : undefined}
|
|
27
|
-
>
|
|
28
|
-
{isExisting ? addedLabel : (isAdding ? addingLabel : addLabel)}
|
|
29
|
-
</button>
|
|
30
|
-
</div>
|
|
31
|
-
));
|
|
32
|
-
DefaultUserItem.displayName = 'DefaultUserItem';
|
|
33
|
-
|
|
34
|
-
/* ---------- Default search input ---------- */
|
|
35
|
-
const DefaultSearchInput: React.FC<{ value: string; onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; placeholder: string }> = ({ value, onChange, placeholder }) => (
|
|
36
|
-
<div className="ermis-modal-search">
|
|
37
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
38
|
-
<circle cx="11" cy="11" r="8"></circle>
|
|
39
|
-
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
40
|
-
</svg>
|
|
41
|
-
<input
|
|
42
|
-
type="text"
|
|
43
|
-
placeholder={placeholder}
|
|
44
|
-
value={value}
|
|
45
|
-
onChange={onChange}
|
|
46
|
-
autoFocus
|
|
47
|
-
/>
|
|
48
|
-
</div>
|
|
49
|
-
);
|
|
3
|
+
import { UserPicker } from '../UserPicker';
|
|
4
|
+
import { Avatar } from '../Avatar';
|
|
5
|
+
import type { AddMemberModalProps, UserPickerUser } from '../../types';
|
|
50
6
|
|
|
51
7
|
export const AddMemberModal: React.FC<AddMemberModalProps> = ({
|
|
52
8
|
channel,
|
|
53
9
|
currentMembers,
|
|
54
10
|
onClose,
|
|
55
|
-
AvatarComponent,
|
|
11
|
+
AvatarComponent = Avatar,
|
|
56
12
|
title = 'Add Member',
|
|
57
13
|
searchPlaceholder = 'Search by name, email or phone...',
|
|
58
14
|
loadingText = 'Loading users...',
|
|
@@ -63,142 +19,60 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({
|
|
|
63
19
|
UserItemComponent,
|
|
64
20
|
SearchInputComponent,
|
|
65
21
|
}) => {
|
|
66
|
-
const
|
|
67
|
-
const [
|
|
68
|
-
const [remoteUsers, setRemoteUsers] = useState<any[]>([]);
|
|
69
|
-
const [searchInput, setSearchInput] = useState('');
|
|
70
|
-
const [search, setSearch] = useState('');
|
|
71
|
-
const [isPendingFilter, startTransition] = useTransition();
|
|
72
|
-
|
|
73
|
-
const [loading, setLoading] = useState(true);
|
|
74
|
-
const [isSearching, setIsSearching] = useState(false);
|
|
75
|
-
const [addingUser, setAddingUser] = useState<string | null>(null);
|
|
76
|
-
const [justAdded, setJustAdded] = useState<Set<string>>(new Set());
|
|
77
|
-
|
|
78
|
-
const UserRow = UserItemComponent || DefaultUserItem;
|
|
79
|
-
const SearchInput = SearchInputComponent || DefaultSearchInput;
|
|
80
|
-
|
|
81
|
-
// Handle immediate input + deferred filter via transition
|
|
82
|
-
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
83
|
-
const val = e.target.value;
|
|
84
|
-
setSearchInput(val);
|
|
85
|
-
startTransition(() => {
|
|
86
|
-
setSearch(val);
|
|
87
|
-
});
|
|
88
|
-
}, [startTransition]);
|
|
89
|
-
|
|
90
|
-
// 1. Fetch initial 100 users
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
let active = true;
|
|
93
|
-
const fetchUsers = async () => {
|
|
94
|
-
if (!client) return;
|
|
95
|
-
try {
|
|
96
|
-
const response = await client.queryUsers('100', 1);
|
|
97
|
-
if (active && response.data) {
|
|
98
|
-
setInitialUsers(response.data);
|
|
99
|
-
}
|
|
100
|
-
} catch (err) {
|
|
101
|
-
console.error('Error fetching users:', err);
|
|
102
|
-
} finally {
|
|
103
|
-
if (active) setLoading(false);
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
fetchUsers();
|
|
107
|
-
return () => { active = false; };
|
|
108
|
-
}, [client]);
|
|
109
|
-
|
|
110
|
-
// 2. Local filter by name/email/phone
|
|
111
|
-
const localFilteredUsers = useMemo(() => {
|
|
112
|
-
if (!search.trim()) return initialUsers;
|
|
113
|
-
const term = search.toLowerCase().trim();
|
|
114
|
-
return initialUsers.filter(u => {
|
|
115
|
-
const email = (u.email || '').toLowerCase();
|
|
116
|
-
const phone = (u.phone || '').toLowerCase();
|
|
117
|
-
const name = (u.name || '').toLowerCase();
|
|
118
|
-
return email.includes(term) || phone.includes(term) || name.includes(term);
|
|
119
|
-
});
|
|
120
|
-
}, [search, initialUsers]);
|
|
22
|
+
const [selectedUsers, setSelectedUsers] = useState<UserPickerUser[]>([]);
|
|
23
|
+
const [isAdding, setIsAdding] = useState(false);
|
|
121
24
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
let cancelled = false;
|
|
131
|
-
const timer = setTimeout(async () => {
|
|
132
|
-
setIsSearching(true);
|
|
133
|
-
try {
|
|
134
|
-
const response = await client.searchUsers(1, 25, search.trim());
|
|
135
|
-
if (!cancelled && response.data) {
|
|
136
|
-
setRemoteUsers(response.data);
|
|
137
|
-
}
|
|
138
|
-
} catch (err) {
|
|
139
|
-
console.error('Error searching remote users:', err);
|
|
140
|
-
} finally {
|
|
141
|
-
if (!cancelled) setIsSearching(false);
|
|
142
|
-
}
|
|
143
|
-
}, 500);
|
|
144
|
-
|
|
145
|
-
return () => {
|
|
146
|
-
cancelled = true;
|
|
147
|
-
clearTimeout(timer);
|
|
148
|
-
};
|
|
149
|
-
}, [search, localFilteredUsers.length, client]);
|
|
25
|
+
// Exclude existing members from the picker
|
|
26
|
+
const excludeUserIds = useMemo(
|
|
27
|
+
() => currentMembers.map((m: any) => m.user_id),
|
|
28
|
+
[currentMembers],
|
|
29
|
+
);
|
|
150
30
|
|
|
151
|
-
// 4. Derived state
|
|
152
|
-
const usersToDisplay = (search.trim() && localFilteredUsers.length === 0) ? remoteUsers : localFilteredUsers;
|
|
153
|
-
const isListLoading = loading || isSearching || isPendingFilter;
|
|
154
31
|
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
}, [
|
|
32
|
+
const handleSelectionChange = useCallback((users: UserPickerUser[]) => {
|
|
33
|
+
setSelectedUsers(users);
|
|
34
|
+
}, []);
|
|
158
35
|
|
|
159
|
-
const handleAdd = useCallback(async (
|
|
36
|
+
const handleAdd = useCallback(async () => {
|
|
37
|
+
if (selectedUsers.length === 0 || isAdding) return;
|
|
160
38
|
try {
|
|
161
|
-
|
|
162
|
-
await channel.addMembers(
|
|
163
|
-
|
|
39
|
+
setIsAdding(true);
|
|
40
|
+
await channel.addMembers(selectedUsers.map(u => u.id));
|
|
41
|
+
onClose();
|
|
164
42
|
} catch (err) {
|
|
165
|
-
console.error('Failed to add
|
|
43
|
+
console.error('Failed to add members:', err);
|
|
166
44
|
} finally {
|
|
167
|
-
|
|
45
|
+
setIsAdding(false);
|
|
168
46
|
}
|
|
169
|
-
}, [channel]);
|
|
47
|
+
}, [selectedUsers, isAdding, channel, onClose]);
|
|
48
|
+
|
|
49
|
+
const footer = (
|
|
50
|
+
<button
|
|
51
|
+
className="ermis-modal-add-btn"
|
|
52
|
+
onClick={handleAdd}
|
|
53
|
+
disabled={selectedUsers.length === 0 || isAdding}
|
|
54
|
+
>
|
|
55
|
+
{isAdding
|
|
56
|
+
? addingLabel
|
|
57
|
+
: `${addLabel} ${selectedUsers.length > 0 ? `(${selectedUsers.length})` : ''}`}
|
|
58
|
+
</button>
|
|
59
|
+
);
|
|
170
60
|
|
|
171
61
|
return (
|
|
172
|
-
<Modal isOpen onClose={onClose} title={title} maxWidth="480px">
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
62
|
+
<Modal isOpen onClose={onClose} title={title} maxWidth="480px" footer={footer}>
|
|
63
|
+
<UserPicker
|
|
64
|
+
mode="checkbox"
|
|
65
|
+
onSelectionChange={handleSelectionChange}
|
|
66
|
+
excludeUserIds={excludeUserIds}
|
|
67
|
+
pageSize={30}
|
|
68
|
+
AvatarComponent={AvatarComponent}
|
|
69
|
+
UserItemComponent={UserItemComponent as any}
|
|
70
|
+
SearchInputComponent={SearchInputComponent}
|
|
71
|
+
searchPlaceholder={searchPlaceholder}
|
|
72
|
+
loadingText={loadingText}
|
|
73
|
+
emptyText={emptyText}
|
|
74
|
+
selectedEmptyLabel="Select users to add..."
|
|
177
75
|
/>
|
|
178
|
-
|
|
179
|
-
<div className="ermis-modal-user-list" style={LIST_CONTAINER_STYLE}>
|
|
180
|
-
{isListLoading ? (
|
|
181
|
-
<div className="ermis-modal-loading">{loadingText}</div>
|
|
182
|
-
) : usersToDisplay.length === 0 ? (
|
|
183
|
-
<div className="ermis-modal-empty">{emptyText}</div>
|
|
184
|
-
) : (
|
|
185
|
-
<VList style={VLIST_STYLE}>
|
|
186
|
-
{usersToDisplay.map(user => (
|
|
187
|
-
<UserRow
|
|
188
|
-
key={user.id}
|
|
189
|
-
user={user}
|
|
190
|
-
isExisting={existingMemberIds.has(user.id) || justAdded.has(user.id)}
|
|
191
|
-
isAdding={addingUser === user.id}
|
|
192
|
-
onAdd={handleAdd}
|
|
193
|
-
AvatarComponent={AvatarComponent}
|
|
194
|
-
addedLabel={addedLabel}
|
|
195
|
-
addingLabel={addingLabel}
|
|
196
|
-
addLabel={addLabel}
|
|
197
|
-
/>
|
|
198
|
-
))}
|
|
199
|
-
</VList>
|
|
200
|
-
)}
|
|
201
|
-
</div>
|
|
202
76
|
</Modal>
|
|
203
77
|
);
|
|
204
78
|
};
|
|
@@ -35,6 +35,11 @@ function getLastMessagePreview(
|
|
|
35
35
|
return { text: parseSignalMessage(rawText, userMap), user: '' };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// Display 'Sticker' if message is a sticker
|
|
39
|
+
if (msgType === 'sticker' || (lastMsg as Record<string, unknown>).sticker_url) {
|
|
40
|
+
return { text: 'Sticker', user: lastMsg.user?.name || lastMsg.user_id || '' };
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
// Regular / other
|
|
39
44
|
let displayText = rawText;
|
|
40
45
|
if (!displayText && lastMsg.attachments && lastMsg.attachments.length > 0) {
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback } from 'react';
|
|
2
|
+
import { Modal } from './Modal';
|
|
3
|
+
import { UserPicker } from './UserPicker';
|
|
4
|
+
import { Avatar } from './Avatar';
|
|
5
|
+
import { useChatClient } from '../hooks/useChatClient';
|
|
6
|
+
import type { CreateChannelModalProps, UserPickerUser } from '../types';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
10
|
+
isOpen,
|
|
11
|
+
onClose,
|
|
12
|
+
onSuccess,
|
|
13
|
+
AvatarComponent = Avatar,
|
|
14
|
+
UserItemComponent,
|
|
15
|
+
title = 'New Message',
|
|
16
|
+
directTabLabel = 'Direct',
|
|
17
|
+
groupTabLabel = 'Group',
|
|
18
|
+
groupNameLabel = 'Channel Name',
|
|
19
|
+
groupNamePlaceholder = 'Enter channel name (required)',
|
|
20
|
+
groupDescriptionLabel = 'Description',
|
|
21
|
+
groupDescriptionPlaceholder = 'Optional description',
|
|
22
|
+
groupPublicLabel = 'Public Channel',
|
|
23
|
+
userSearchPlaceholder = 'Search users...',
|
|
24
|
+
cancelButtonLabel = 'Cancel',
|
|
25
|
+
createButtonLabel = 'Create',
|
|
26
|
+
creatingButtonLabel = 'Creating...',
|
|
27
|
+
}) => {
|
|
28
|
+
const { client } = useChatClient();
|
|
29
|
+
const currentUserId = client?.userID;
|
|
30
|
+
|
|
31
|
+
/* ---------- State ---------- */
|
|
32
|
+
const [tab, setTab] = useState<'messaging' | 'team'>('messaging');
|
|
33
|
+
const [step, setStep] = useState<1 | 2>(1); // Only for team channel
|
|
34
|
+
|
|
35
|
+
// Group specific
|
|
36
|
+
const [name, setName] = useState('');
|
|
37
|
+
const [description, setDescription] = useState('');
|
|
38
|
+
const [isPublic, setIsPublic] = useState(false);
|
|
39
|
+
|
|
40
|
+
// Users
|
|
41
|
+
const [selectedUsers, setSelectedUsers] = useState<UserPickerUser[]>([]);
|
|
42
|
+
|
|
43
|
+
// Progress/Error
|
|
44
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
45
|
+
const [error, setError] = useState<string | null>(null);
|
|
46
|
+
|
|
47
|
+
/* ---------- Exclude IDs for Direct ---------- */
|
|
48
|
+
const existingDirectUserIds = useMemo(() => {
|
|
49
|
+
if (!client || !currentUserId || tab !== 'messaging') return [];
|
|
50
|
+
|
|
51
|
+
const ids = new Set<string>();
|
|
52
|
+
Object.values(client.activeChannels).forEach((channel: any) => {
|
|
53
|
+
if (channel.type === 'messaging' && channel.state?.members) {
|
|
54
|
+
Object.keys(channel.state.members).forEach(uid => {
|
|
55
|
+
if (uid !== currentUserId) ids.add(uid);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return Array.from(ids);
|
|
60
|
+
}, [client, currentUserId, tab]);
|
|
61
|
+
|
|
62
|
+
/* ---------- Handlers ---------- */
|
|
63
|
+
const handleCreate = useCallback(async () => {
|
|
64
|
+
if (!client || !currentUserId || isCreating) return;
|
|
65
|
+
|
|
66
|
+
// Validations
|
|
67
|
+
if (selectedUsers.length === 0) {
|
|
68
|
+
setError('Please select at least one user.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (tab === 'team' && !name.trim()) {
|
|
73
|
+
setError('Group name is required.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setIsCreating(true);
|
|
78
|
+
setError(null);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
let createdChannel;
|
|
82
|
+
|
|
83
|
+
if (tab === 'messaging') {
|
|
84
|
+
const targetUserId = selectedUsers[0].id;
|
|
85
|
+
createdChannel = client.channel('messaging', {
|
|
86
|
+
members: [currentUserId, targetUserId],
|
|
87
|
+
} as any);
|
|
88
|
+
await createdChannel.create();
|
|
89
|
+
} else {
|
|
90
|
+
// Group Channel
|
|
91
|
+
const memberIds = selectedUsers.map(member => member.id);
|
|
92
|
+
// Ensure current user is in the group members
|
|
93
|
+
if (!memberIds.includes(currentUserId)) {
|
|
94
|
+
memberIds.push(currentUserId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const payload: any = {
|
|
98
|
+
name: name.trim(),
|
|
99
|
+
members: memberIds,
|
|
100
|
+
public: isPublic,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (description.trim()) {
|
|
104
|
+
payload.description = description.trim();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
createdChannel = client.channel('team', payload);
|
|
108
|
+
await createdChannel.create();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Cleanup and execute callback
|
|
112
|
+
if (onSuccess) {
|
|
113
|
+
onSuccess(createdChannel);
|
|
114
|
+
} else {
|
|
115
|
+
onClose();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
} catch (err: any) {
|
|
119
|
+
setError(err?.message || 'Failed to create channel');
|
|
120
|
+
} finally {
|
|
121
|
+
setIsCreating(false);
|
|
122
|
+
}
|
|
123
|
+
}, [client, currentUserId, isCreating, selectedUsers, tab, name, isPublic, description, onSuccess, onClose]);
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
const isValid = useMemo(() => {
|
|
127
|
+
if (tab === 'messaging' && selectedUsers.length === 0) return false;
|
|
128
|
+
if (tab === 'team' && step === 1 && !name.trim()) return false;
|
|
129
|
+
if (tab === 'team' && step === 2 && selectedUsers.length === 0) return false;
|
|
130
|
+
return true;
|
|
131
|
+
}, [selectedUsers, tab, name, step]);
|
|
132
|
+
|
|
133
|
+
let footer;
|
|
134
|
+
if (tab === 'messaging') {
|
|
135
|
+
footer = (
|
|
136
|
+
<div className="ermis-create-channel__footer">
|
|
137
|
+
<button className="ermis-create-channel__btn ermis-create-channel__btn--cancel" onClick={onClose} disabled={isCreating}>{cancelButtonLabel}</button>
|
|
138
|
+
<button className="ermis-create-channel__btn ermis-create-channel__btn--create" onClick={handleCreate} disabled={isCreating || !isValid}>
|
|
139
|
+
{isCreating ? creatingButtonLabel : createButtonLabel}
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
} else if (tab === 'team' && step === 1) {
|
|
144
|
+
footer = (
|
|
145
|
+
<div className="ermis-create-channel__footer">
|
|
146
|
+
<button className="ermis-create-channel__btn ermis-create-channel__btn--cancel" onClick={onClose} disabled={isCreating}>{cancelButtonLabel}</button>
|
|
147
|
+
<button className="ermis-create-channel__btn ermis-create-channel__btn--create" onClick={() => { setError(null); setStep(2); }} disabled={isCreating || !isValid}>
|
|
148
|
+
Next
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
} else if (tab === 'team' && step === 2) {
|
|
153
|
+
footer = (
|
|
154
|
+
<div className="ermis-create-channel__footer">
|
|
155
|
+
<button className="ermis-create-channel__btn ermis-create-channel__btn--cancel" onClick={() => { setError(null); setStep(1); }} disabled={isCreating}>Back</button>
|
|
156
|
+
<button className="ermis-create-channel__btn ermis-create-channel__btn--create" onClick={handleCreate} disabled={isCreating || !isValid}>
|
|
157
|
+
{isCreating ? creatingButtonLabel : createButtonLabel}
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<Modal isOpen={isOpen} onClose={isCreating ? () => { } : onClose} title={title} maxWidth="480px" footer={footer}>
|
|
165
|
+
<div className="ermis-create-channel__body">
|
|
166
|
+
|
|
167
|
+
{/* Type Toggle */}
|
|
168
|
+
<div className="ermis-create-channel__tabs">
|
|
169
|
+
<button
|
|
170
|
+
className={`ermis-create-channel__tab ${tab === 'messaging' ? 'ermis-create-channel__tab--active' : ''}`}
|
|
171
|
+
onClick={() => {
|
|
172
|
+
setTab('messaging');
|
|
173
|
+
setStep(1);
|
|
174
|
+
setSelectedUsers([]);
|
|
175
|
+
setError(null);
|
|
176
|
+
}}
|
|
177
|
+
disabled={isCreating}
|
|
178
|
+
>
|
|
179
|
+
{directTabLabel}
|
|
180
|
+
</button>
|
|
181
|
+
<button
|
|
182
|
+
className={`ermis-create-channel__tab ${tab === 'team' ? 'ermis-create-channel__tab--active' : ''}`}
|
|
183
|
+
onClick={() => {
|
|
184
|
+
setTab('team');
|
|
185
|
+
setStep(1);
|
|
186
|
+
setSelectedUsers([]);
|
|
187
|
+
setError(null);
|
|
188
|
+
}}
|
|
189
|
+
disabled={isCreating}
|
|
190
|
+
>
|
|
191
|
+
{groupTabLabel}
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Group Specific Fields - Step 1 */}
|
|
196
|
+
{tab === 'team' && step === 1 && (
|
|
197
|
+
<>
|
|
198
|
+
|
|
199
|
+
<div className="ermis-create-channel__field">
|
|
200
|
+
<label className="ermis-create-channel__label">{groupNameLabel} <span style={{ color: 'var(--ermis-error)' }}>*</span></label>
|
|
201
|
+
<input
|
|
202
|
+
className="ermis-create-channel__input"
|
|
203
|
+
value={name}
|
|
204
|
+
onChange={(e) => setName(e.target.value)}
|
|
205
|
+
placeholder={groupNamePlaceholder}
|
|
206
|
+
disabled={isCreating}
|
|
207
|
+
maxLength={100}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div className="ermis-create-channel__field">
|
|
212
|
+
<label className="ermis-create-channel__label">{groupDescriptionLabel}</label>
|
|
213
|
+
<textarea
|
|
214
|
+
className="ermis-create-channel__textarea"
|
|
215
|
+
value={description}
|
|
216
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
217
|
+
placeholder={groupDescriptionPlaceholder}
|
|
218
|
+
disabled={isCreating}
|
|
219
|
+
maxLength={500}
|
|
220
|
+
rows={2}
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div className="ermis-create-channel__field ermis-create-channel__field--toggle">
|
|
225
|
+
<label className="ermis-create-channel__label">{groupPublicLabel}</label>
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
role="switch"
|
|
229
|
+
aria-checked={isPublic}
|
|
230
|
+
className={`ermis-create-channel__toggle ${isPublic ? 'ermis-create-channel__toggle--on' : ''}`}
|
|
231
|
+
onClick={() => setIsPublic(v => !v)}
|
|
232
|
+
disabled={isCreating}
|
|
233
|
+
>
|
|
234
|
+
<span className="ermis-create-channel__toggle-thumb" />
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
</>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
{/* User Selection - Step 2 (Group) or Step 1 (Messaging) */}
|
|
241
|
+
{((tab === 'team' && step === 2) || tab === 'messaging') && (
|
|
242
|
+
<div className="ermis-create-channel__users">
|
|
243
|
+
<div className="ermis-create-channel__users-title">
|
|
244
|
+
Members <span style={{ color: 'var(--ermis-error)' }}>*</span>
|
|
245
|
+
</div>
|
|
246
|
+
<div style={{ height: tab === 'team' ? '280px' : '400px', display: 'flex', flexDirection: 'column' }}>
|
|
247
|
+
<UserPicker
|
|
248
|
+
mode={tab === 'messaging' ? 'radio' : 'checkbox'}
|
|
249
|
+
onSelectionChange={setSelectedUsers}
|
|
250
|
+
excludeUserIds={tab === 'messaging' ? existingDirectUserIds : []}
|
|
251
|
+
initialSelectedUsers={selectedUsers}
|
|
252
|
+
AvatarComponent={AvatarComponent}
|
|
253
|
+
UserItemComponent={UserItemComponent as any}
|
|
254
|
+
searchPlaceholder={userSearchPlaceholder}
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
{error && (
|
|
261
|
+
<div className="ermis-create-channel__error">
|
|
262
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
263
|
+
<circle cx="12" cy="12" r="10" />
|
|
264
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
265
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
266
|
+
</svg>
|
|
267
|
+
{error}
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
</div>
|
|
272
|
+
</Modal>
|
|
273
|
+
);
|
|
274
|
+
};
|