@ermis-network/ermis-chat-react 2.0.0 → 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 +5087 -11279
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +632 -152
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +273 -9
- package/dist/index.d.ts +273 -9
- package/dist/index.mjs +5085 -11295
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/Channel.tsx +0 -3
- package/src/components/ChannelActions.tsx +6 -1
- package/src/components/ChannelHeader.tsx +8 -32
- package/src/components/ChannelInfo/AddMemberModal.tsx +7 -1
- package/src/components/ChannelInfo/ChannelInfo.tsx +82 -2
- package/src/components/ChannelInfo/EditChannelModal.tsx +2 -2
- package/src/components/ChannelInfo/MediaGridItem.tsx +215 -78
- package/src/components/ChannelInfo/useChannelInfoTabs.tsx +170 -129
- package/src/components/ChannelList.tsx +72 -13
- package/src/components/CreateChannelModal.tsx +131 -12
- package/src/components/FilesPreview.tsx +8 -12
- package/src/components/FlatTopicGroupItem.tsx +27 -16
- package/src/components/ForwardMessageModal.tsx +11 -3
- package/src/components/MediaLightbox.tsx +444 -304
- package/src/components/MessageActionsBox.tsx +2 -0
- package/src/components/MessageInput.tsx +41 -12
- package/src/components/MessageItem.tsx +70 -25
- package/src/components/MessageQuickReactions.tsx +131 -128
- package/src/components/MessageReactions.tsx +47 -2
- package/src/components/MessageRenderers.tsx +1030 -433
- package/src/components/PinnedMessages.tsx +40 -12
- package/src/components/QuotedMessagePreview.tsx +99 -8
- package/src/components/RecoveryPin/RecoveryPin.tsx +279 -0
- package/src/components/RecoveryPin/index.ts +19 -0
- package/src/components/TopicList.tsx +20 -5
- package/src/components/TypingIndicator.tsx +3 -3
- package/src/components/UserPicker.tsx +26 -25
- package/src/components/VirtualMessageList.tsx +345 -125
- package/src/context/ChatProvider.tsx +27 -1
- package/src/hooks/useChannelListUpdates.ts +22 -1
- package/src/hooks/useChannelMessages.ts +338 -51
- package/src/hooks/useChannelRowUpdates.ts +18 -6
- package/src/hooks/useChatUser.ts +9 -1
- 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 +210 -13
- package/src/hooks/useLoadMessages.ts +16 -4
- package/src/hooks/useMentions.ts +60 -6
- package/src/hooks/useMessageActions.ts +14 -8
- package/src/hooks/useMessageSend.ts +64 -12
- package/src/hooks/usePendingE2eeSends.ts +29 -0
- package/src/hooks/useRecoveryPin.ts +287 -0
- package/src/hooks/useScrollToMessage.ts +29 -4
- package/src/hooks/useTopicGroupUpdates.ts +49 -11
- package/src/index.ts +23 -0
- package/src/messageTypeUtils.ts +14 -0
- package/src/styles/_channel-info.css +9 -0
- package/src/styles/_channel-list.css +37 -14
- package/src/styles/_media-lightbox.css +36 -3
- package/src/styles/_message-bubble.css +381 -41
- package/src/styles/_message-input.css +8 -0
- package/src/styles/_message-list.css +67 -10
- package/src/styles/_message-quick-reactions.css +101 -59
- package/src/styles/_message-reactions.css +18 -32
- package/src/styles/_recovery-pin.css +97 -0
- package/src/styles/_tokens.css +5 -5
- package/src/styles/_typing-indicator.css +23 -13
- package/src/styles/index.css +1 -0
- package/src/types.ts +115 -1
- package/src/utils/avatarColors.ts +1 -1
- package/src/utils.ts +38 -18
|
@@ -5,9 +5,33 @@ import { Avatar } from './Avatar';
|
|
|
5
5
|
import { useChatClient } from '../hooks/useChatClient';
|
|
6
6
|
import { useChatComponents } from '../context/ChatComponentsContext';
|
|
7
7
|
import { markChannelAsFullyQueried } from '../hooks/useChannelMessages';
|
|
8
|
-
import type { CreateChannelModalProps, UserPickerUser } from '../types';
|
|
8
|
+
import type { CreateChannelE2eeToggleProps, CreateChannelModalProps, UserPickerUser } from '../types';
|
|
9
9
|
import { isDirectChannel } from '../channelTypeUtils';
|
|
10
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
|
+
);
|
|
11
35
|
|
|
12
36
|
export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
13
37
|
isOpen,
|
|
@@ -31,11 +55,16 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
|
31
55
|
nextButtonLabel = 'Next',
|
|
32
56
|
backButtonLabel = 'Back',
|
|
33
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',
|
|
34
62
|
TabsComponent,
|
|
35
63
|
FooterComponent,
|
|
36
64
|
GroupFieldsComponent,
|
|
37
65
|
SearchInputComponent,
|
|
38
66
|
SelectedBoxComponent,
|
|
67
|
+
E2eeToggleComponent = DefaultE2eeToggle,
|
|
39
68
|
}) => {
|
|
40
69
|
const { client, setActiveChannel } = useChatClient();
|
|
41
70
|
const { ModalComponent } = useChatComponents();
|
|
@@ -50,6 +79,7 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
|
50
79
|
const [name, setName] = useState('');
|
|
51
80
|
const [description, setDescription] = useState('');
|
|
52
81
|
const [isPublic, setIsPublic] = useState(false);
|
|
82
|
+
const [e2eeEnabled, setE2eeEnabled] = useState(false);
|
|
53
83
|
|
|
54
84
|
// Users
|
|
55
85
|
const [selectedUsers, setSelectedUsers] = useState<UserPickerUser[]>([]);
|
|
@@ -57,6 +87,11 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
|
57
87
|
// Progress/Error
|
|
58
88
|
const [isCreating, setIsCreating] = useState(false);
|
|
59
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
|
+
}, []);
|
|
60
95
|
|
|
61
96
|
/* ---------- Exclude IDs for Direct ---------- */
|
|
62
97
|
const hasExistingDirectChannel = useMemo(() => {
|
|
@@ -120,14 +155,33 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
|
120
155
|
return;
|
|
121
156
|
}
|
|
122
157
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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);
|
|
126
176
|
const response = (await createdChannel.create()) as any;
|
|
127
177
|
if (response?.channel?.id) {
|
|
128
178
|
createdChannel = client.channel('messaging', response.channel.id);
|
|
129
179
|
await createdChannel.watch({ messages: { limit: 25, include_hidden_messages: true } });
|
|
130
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
|
+
}
|
|
131
185
|
}
|
|
132
186
|
} else {
|
|
133
187
|
// Group Channel
|
|
@@ -147,12 +201,36 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
|
147
201
|
payload.description = description.trim();
|
|
148
202
|
}
|
|
149
203
|
|
|
150
|
-
|
|
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
|
+
}
|
|
151
225
|
const response = (await createdChannel.create()) as any;
|
|
152
226
|
if (response?.channel?.id) {
|
|
153
227
|
createdChannel = client.channel('team', response.channel.id);
|
|
154
228
|
await createdChannel.watch({ messages: { limit: 25, include_hidden_messages: true } });
|
|
155
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
|
+
}
|
|
156
234
|
}
|
|
157
235
|
}
|
|
158
236
|
|
|
@@ -172,7 +250,21 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
|
172
250
|
} finally {
|
|
173
251
|
setIsCreating(false);
|
|
174
252
|
}
|
|
175
|
-
}, [
|
|
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
|
+
]);
|
|
176
268
|
|
|
177
269
|
|
|
178
270
|
const isValid = useMemo(() => {
|
|
@@ -214,6 +306,7 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
|
214
306
|
messageButtonLabel={messageButtonLabel}
|
|
215
307
|
nextButtonLabel={nextButtonLabel}
|
|
216
308
|
backButtonLabel={backButtonLabel}
|
|
309
|
+
e2eeEnabled={e2eeEnabled}
|
|
217
310
|
/>
|
|
218
311
|
);
|
|
219
312
|
} else if (tab === 'messaging') {
|
|
@@ -256,8 +349,9 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
|
256
349
|
onTabChange={(t) => {
|
|
257
350
|
setTab(t);
|
|
258
351
|
setStep(1);
|
|
259
|
-
|
|
260
|
-
|
|
352
|
+
setSelectedUsers([]);
|
|
353
|
+
setE2eeEnabled(false);
|
|
354
|
+
setError(null);
|
|
261
355
|
}}
|
|
262
356
|
disabled={isCreating}
|
|
263
357
|
directTabLabel={directTabLabel}
|
|
@@ -270,8 +364,9 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
|
270
364
|
onClick={() => {
|
|
271
365
|
setTab('messaging');
|
|
272
366
|
setStep(1);
|
|
273
|
-
|
|
274
|
-
|
|
367
|
+
setSelectedUsers([]);
|
|
368
|
+
setE2eeEnabled(false);
|
|
369
|
+
setError(null);
|
|
275
370
|
}}
|
|
276
371
|
disabled={isCreating}
|
|
277
372
|
>
|
|
@@ -282,8 +377,9 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
|
282
377
|
onClick={() => {
|
|
283
378
|
setTab('team');
|
|
284
379
|
setStep(1);
|
|
285
|
-
|
|
286
|
-
|
|
380
|
+
setSelectedUsers([]);
|
|
381
|
+
setE2eeEnabled(false);
|
|
382
|
+
setError(null);
|
|
287
383
|
}}
|
|
288
384
|
disabled={isCreating}
|
|
289
385
|
>
|
|
@@ -308,6 +404,11 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
|
308
404
|
groupDescriptionLabel={groupDescriptionLabel}
|
|
309
405
|
groupDescriptionPlaceholder={groupDescriptionPlaceholder}
|
|
310
406
|
groupPublicLabel={groupPublicLabel}
|
|
407
|
+
e2eeEnabled={e2eeEnabled}
|
|
408
|
+
onE2eeChange={handleE2eeChange}
|
|
409
|
+
e2eeLabel={e2eeLabel}
|
|
410
|
+
e2eeDescription={e2eeDescription}
|
|
411
|
+
e2eeDisabled={!e2eeAvailable || isCreating}
|
|
311
412
|
/>
|
|
312
413
|
) : (
|
|
313
414
|
<>
|
|
@@ -349,10 +450,28 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|
|
349
450
|
<span className="ermis-create-channel__toggle-thumb" />
|
|
350
451
|
</button>
|
|
351
452
|
</div>
|
|
453
|
+
|
|
454
|
+
<DefaultE2eeToggle
|
|
455
|
+
enabled={e2eeEnabled}
|
|
456
|
+
onChange={handleE2eeChange}
|
|
457
|
+
disabled={!e2eeAvailable || isCreating}
|
|
458
|
+
label={e2eeLabel}
|
|
459
|
+
description={e2eeAvailable ? e2eeDescription : e2eeUnavailableLabel}
|
|
460
|
+
/>
|
|
352
461
|
</>
|
|
353
462
|
)
|
|
354
463
|
)}
|
|
355
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
|
+
/>
|
|
473
|
+
)}
|
|
474
|
+
|
|
356
475
|
{/* User Selection - Step 2 (Group) or Step 1 (Messaging) */}
|
|
357
476
|
{((tab === 'team' && step === 2) || tab === 'messaging') && (
|
|
358
477
|
<div className={`ermis-create-channel__users ermis-create-channel__users--${tab}`}>
|
|
@@ -37,7 +37,7 @@ export const FilesPreview: React.FC<FilesPreviewProps> = React.memo(({ files, on
|
|
|
37
37
|
const fileName = item.file?.name || item.originalAttachment?.title || 'Unknown file';
|
|
38
38
|
const fileSize = item.file?.size || item.originalAttachment?.file_size || 0;
|
|
39
39
|
|
|
40
|
-
const isHeic = item.file ? isHeicFile(item.file) :
|
|
40
|
+
const isHeic = item.file ? isHeicFile(item.file) : fileType === 'image/heic' || fileType === 'image/heif';
|
|
41
41
|
const isImage = fileType.startsWith('image/') && !isHeic;
|
|
42
42
|
const isVideo = fileType.startsWith('video/');
|
|
43
43
|
const isUploading = item.status === 'uploading';
|
|
@@ -62,17 +62,9 @@ export const FilesPreview: React.FC<FilesPreviewProps> = React.memo(({ files, on
|
|
|
62
62
|
|
|
63
63
|
{/* Preview content */}
|
|
64
64
|
{isImage && previewUrl ? (
|
|
65
|
-
<img
|
|
66
|
-
className="ermis-files-preview__thumb"
|
|
67
|
-
src={previewUrl}
|
|
68
|
-
alt={fileName}
|
|
69
|
-
/>
|
|
65
|
+
<img className="ermis-files-preview__thumb" src={previewUrl} alt={fileName} />
|
|
70
66
|
) : isVideo && previewUrl ? (
|
|
71
|
-
<video
|
|
72
|
-
className="ermis-files-preview__thumb"
|
|
73
|
-
src={previewUrl}
|
|
74
|
-
muted
|
|
75
|
-
/>
|
|
67
|
+
<video className="ermis-files-preview__thumb" src={previewUrl} muted />
|
|
76
68
|
) : (
|
|
77
69
|
<div className="ermis-files-preview__file-icon">
|
|
78
70
|
<span>{getFileIcon(fileType)}</span>
|
|
@@ -88,7 +80,11 @@ export const FilesPreview: React.FC<FilesPreviewProps> = React.memo(({ files, on
|
|
|
88
80
|
{/* Upload status overlay */}
|
|
89
81
|
{isUploading && (
|
|
90
82
|
<div className="ermis-files-preview__uploading">
|
|
91
|
-
|
|
83
|
+
{item.progress !== undefined ? (
|
|
84
|
+
<span className="ermis-files-preview__progress">{item.progress}%</span>
|
|
85
|
+
) : (
|
|
86
|
+
<span className="ermis-files-preview__spinner" />
|
|
87
|
+
)}
|
|
92
88
|
</div>
|
|
93
89
|
)}
|
|
94
90
|
|
|
@@ -57,6 +57,7 @@ type FlatTopicGroupItemProps = {
|
|
|
57
57
|
fileMessageLabel?: React.ReactNode;
|
|
58
58
|
systemMessageTranslations?: SystemMessageTranslations;
|
|
59
59
|
signalMessageTranslations?: SignalMessageTranslations;
|
|
60
|
+
showTopicPills?: boolean;
|
|
60
61
|
};
|
|
61
62
|
|
|
62
63
|
/* ----------------------------------------------------------
|
|
@@ -87,6 +88,7 @@ export const FlatTopicGroupItem: React.FC<FlatTopicGroupItemProps> = React.memo(
|
|
|
87
88
|
fileMessageLabel,
|
|
88
89
|
systemMessageTranslations,
|
|
89
90
|
signalMessageTranslations,
|
|
91
|
+
showTopicPills = false,
|
|
90
92
|
}) => {
|
|
91
93
|
const { client } = useChatClient();
|
|
92
94
|
const currentUserId = client.userID;
|
|
@@ -113,7 +115,11 @@ export const FlatTopicGroupItem: React.FC<FlatTopicGroupItemProps> = React.memo(
|
|
|
113
115
|
const name = channel.data?.name || channel.cid;
|
|
114
116
|
const image = channel.data?.image as string | undefined;
|
|
115
117
|
const isPinned = channel.data?.is_pinned === true;
|
|
116
|
-
|
|
118
|
+
// For topic groups, always show the unread badge if there are unreads.
|
|
119
|
+
// Unlike normal channels, isActive only means the user is viewing ONE topic,
|
|
120
|
+
// not that they've read ALL topics. The aggregatedUnreadCount already correctly
|
|
121
|
+
// reflects only the truly unread topics.
|
|
122
|
+
const showUnread = hasUnread;
|
|
117
123
|
|
|
118
124
|
// Latest message data from the aggregated preview
|
|
119
125
|
const lastMessageText = latestMessagePreview?.text || '';
|
|
@@ -167,7 +173,12 @@ export const FlatTopicGroupItem: React.FC<FlatTopicGroupItemProps> = React.memo(
|
|
|
167
173
|
return (
|
|
168
174
|
<div className={itemClass} onClick={handleClick}>
|
|
169
175
|
<div className="ermis-channel-list__item-avatar-wrapper">
|
|
170
|
-
<AvatarComponent image={image} name={name} size={
|
|
176
|
+
<AvatarComponent image={image} name={name} size={45} disableLightbox className="ermis-avatar-wrapper--group" />
|
|
177
|
+
{showUnread && aggregatedUnreadCount > 0 && (
|
|
178
|
+
<span className="ermis-channel-list__avatar-unread-badge">
|
|
179
|
+
{aggregatedUnreadCount > 99 ? '99+' : aggregatedUnreadCount}
|
|
180
|
+
</span>
|
|
181
|
+
)}
|
|
171
182
|
</div>
|
|
172
183
|
<div className="ermis-channel-list__item-content">
|
|
173
184
|
{/* Row 1: name + pinned + timestamp */}
|
|
@@ -207,20 +218,20 @@ export const FlatTopicGroupItem: React.FC<FlatTopicGroupItemProps> = React.memo(
|
|
|
207
218
|
</div>
|
|
208
219
|
{/* Row 3: topic pills — always visible (at least general pill) */}
|
|
209
220
|
<div className="ermis-channel-list__item-topics-row">
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
{showTopicPills && (
|
|
222
|
+
<div className="ermis-channel-list__topic-pills">
|
|
223
|
+
<span className="ermis-channel-list__topic-pill">
|
|
224
|
+
<span className="ermis-channel-list__topic-pill-avatar">#</span>
|
|
225
|
+
<span className="ermis-channel-list__topic-pill-name">{generalTopicLabel}</span>
|
|
226
|
+
</span>
|
|
227
|
+
{visibleTopics.map((topic: Channel) => (
|
|
228
|
+
<Pill key={topic.cid} topic={topic} />
|
|
229
|
+
))}
|
|
230
|
+
{hasOverflow && (
|
|
231
|
+
<span className="ermis-channel-list__topic-overflow">{moreTopicsLabel}</span>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
224
235
|
</div>
|
|
225
236
|
</div>
|
|
226
237
|
<div className="ermis-channel-list__item-actions-wrapper">
|
|
@@ -73,6 +73,7 @@ export const ForwardMessageModal: React.FC<ForwardMessageModalProps> = ({
|
|
|
73
73
|
const { ModalComponent } = useChatComponents();
|
|
74
74
|
const Modal = ModalComponent || DefaultModal;
|
|
75
75
|
const backdropRef = useRef<HTMLDivElement>(null);
|
|
76
|
+
const { client } = useChatClient();
|
|
76
77
|
|
|
77
78
|
const {
|
|
78
79
|
search,
|
|
@@ -99,9 +100,16 @@ export const ForwardMessageModal: React.FC<ForwardMessageModalProps> = ({
|
|
|
99
100
|
}, [onDismiss]);
|
|
100
101
|
|
|
101
102
|
/* ---------- Message preview ---------- */
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
let previewText = message.text || '';
|
|
104
|
+
|
|
105
|
+
if (previewText && message.mentioned_users && message.mentioned_users.length > 0) {
|
|
106
|
+
message.mentioned_users.forEach((userId) => {
|
|
107
|
+
const name = client.state.users[userId]?.name || userId;
|
|
108
|
+
previewText = previewText.replace(new RegExp(`@${userId}`, 'g'), `@${name}`);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
previewText = previewText.length > 120 ? previewText.slice(0, 120) + '…' : previewText;
|
|
105
113
|
const attachmentCount = message.attachments?.length ?? 0;
|
|
106
114
|
|
|
107
115
|
const footer = (
|