@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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash-based avatar color palette.
|
|
3
|
+
*
|
|
4
|
+
* Each user/channel is assigned a deterministic gradient based on
|
|
5
|
+
* the hash of their name. This provides visual variety in the
|
|
6
|
+
* channel list and improves readability over a single brand-color
|
|
7
|
+
* fallback.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Curated gradient pairs [from, to] – all pass WCAG AA contrast with white text. */
|
|
11
|
+
const AVATAR_GRADIENTS: readonly [string, string][] = [
|
|
12
|
+
['#0EA5E9', '#38BDF8'], // Teal
|
|
13
|
+
['#10B981', '#34D399'], // Emerald
|
|
14
|
+
['#D97706', '#F59E0B'], // Amber
|
|
15
|
+
['#E11D48', '#FB7185'], // Rose
|
|
16
|
+
['#7C3AED', '#A78BFA'], // Violet
|
|
17
|
+
['#4F46E5', '#818CF8'], // Indigo
|
|
18
|
+
['#DB2777', '#F472B6'], // Pink
|
|
19
|
+
['#EA580C', '#FB923C'], // Orange
|
|
20
|
+
['#0891B2', '#22D3EE'], // Cyan
|
|
21
|
+
['#65A30D', '#A3E635'], // Lime
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
/** Neutral fallback when no name is available. */
|
|
25
|
+
const FALLBACK_GRADIENT = 'linear-gradient(135deg, #6B7280 0%, #545f71 100%)';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Simple djb2-variant string hash → non-negative integer.
|
|
29
|
+
*/
|
|
30
|
+
function hashString(str: string): number {
|
|
31
|
+
let hash = 0;
|
|
32
|
+
for (let i = 0; i < str.length; i++) {
|
|
33
|
+
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
|
34
|
+
hash |= 0; // Convert to 32-bit integer
|
|
35
|
+
}
|
|
36
|
+
return Math.abs(hash);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns a CSS `linear-gradient(...)` string for a given name.
|
|
41
|
+
* The same name always produces the same gradient (deterministic).
|
|
42
|
+
*/
|
|
43
|
+
export function getAvatarGradient(name?: string): string {
|
|
44
|
+
if (!name) return FALLBACK_GRADIENT;
|
|
45
|
+
const idx = hashString(name) % AVATAR_GRADIENTS.length;
|
|
46
|
+
const [from, to] = AVATAR_GRADIENTS[idx];
|
|
47
|
+
return `linear-gradient(135deg, ${from} 0%, ${to} 100%)`;
|
|
48
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import type { MentionMember } from './types';
|
|
2
|
-
import type { Attachment, FormatMessageResponse } from '@ermis-network/ermis-chat-sdk';
|
|
3
|
+
import type { Attachment, FormatMessageResponse, Channel } from '@ermis-network/ermis-chat-sdk';
|
|
4
|
+
import {
|
|
5
|
+
parseSystemMessage,
|
|
6
|
+
parseSignalMessage,
|
|
7
|
+
SystemMessageTranslations,
|
|
8
|
+
SignalMessageTranslations,
|
|
9
|
+
} from '@ermis-network/ermis-chat-sdk';
|
|
10
|
+
import { isDeletedDisplayMessage } from './messageTypeUtils';
|
|
3
11
|
|
|
4
12
|
/**
|
|
5
|
-
*
|
|
13
|
+
* Remove Vietnamese diacritics (accents) from a string.
|
|
14
|
+
*/
|
|
15
|
+
export function removeAccents(str: string): string {
|
|
16
|
+
if (!str) return '';
|
|
17
|
+
return str
|
|
18
|
+
.normalize('NFD')
|
|
19
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
20
|
+
.replace(/đ/g, 'd')
|
|
21
|
+
.replace(/Đ/g, 'D');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Format a Date or date-string to a short time string (HH:MM, 24-hour).
|
|
26
|
+
* Matches Telegram's compact time display.
|
|
6
27
|
*/
|
|
7
28
|
export function formatTime(date: Date | string | undefined): string {
|
|
8
29
|
if (!date) return '';
|
|
9
30
|
const d = date instanceof Date ? date : new Date(date);
|
|
10
|
-
|
|
31
|
+
const hours = String(d.getHours()).padStart(2, '0');
|
|
32
|
+
const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
33
|
+
return `${hours}:${minutes}`;
|
|
11
34
|
}
|
|
12
35
|
|
|
13
36
|
/**
|
|
@@ -42,8 +65,10 @@ export function getDateKey(date: Date | string | undefined): string {
|
|
|
42
65
|
|
|
43
66
|
/**
|
|
44
67
|
* Format a date into a human-friendly label (Today / Yesterday / full date).
|
|
68
|
+
* When `locale` is provided, "Today" / "Yesterday" are localised via
|
|
69
|
+
* `Intl.RelativeTimeFormat` and the full date uses that locale for formatting.
|
|
45
70
|
*/
|
|
46
|
-
export function formatDateLabel(date: Date | string | undefined): string {
|
|
71
|
+
export function formatDateLabel(date: Date | string | undefined, locale?: string): string {
|
|
47
72
|
if (!date) return '';
|
|
48
73
|
const d = date instanceof Date ? date : new Date(date);
|
|
49
74
|
const now = new Date();
|
|
@@ -52,9 +77,26 @@ export function formatDateLabel(date: Date | string | undefined): string {
|
|
|
52
77
|
const diffMs = today.getTime() - msgDay.getTime();
|
|
53
78
|
const diffDays = Math.round(diffMs / 86400000);
|
|
54
79
|
|
|
55
|
-
if (diffDays === 0)
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
if (diffDays === 0) {
|
|
81
|
+
if (locale && typeof Intl !== 'undefined' && Intl.RelativeTimeFormat) {
|
|
82
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
|
|
83
|
+
// Format -0 days → "today" / "hôm nay" etc.
|
|
84
|
+
const parts = rtf.formatToParts(0, 'day');
|
|
85
|
+
const label = parts.map((p) => p.value).join('');
|
|
86
|
+
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
87
|
+
}
|
|
88
|
+
return 'Today';
|
|
89
|
+
}
|
|
90
|
+
if (diffDays === 1) {
|
|
91
|
+
if (locale && typeof Intl !== 'undefined' && Intl.RelativeTimeFormat) {
|
|
92
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
|
|
93
|
+
const parts = rtf.formatToParts(-1, 'day');
|
|
94
|
+
const label = parts.map((p) => p.value).join('');
|
|
95
|
+
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
96
|
+
}
|
|
97
|
+
return 'Yesterday';
|
|
98
|
+
}
|
|
99
|
+
return d.toLocaleDateString(locale || undefined, {
|
|
58
100
|
year: 'numeric',
|
|
59
101
|
month: 'long',
|
|
60
102
|
day: 'numeric',
|
|
@@ -74,11 +116,11 @@ export function getMessageUserId(message: FormatMessageResponse): string {
|
|
|
74
116
|
*/
|
|
75
117
|
export function replaceMentionsForPreview(
|
|
76
118
|
text: string,
|
|
77
|
-
message: FormatMessageResponse | { mentioned_users?:
|
|
119
|
+
message: FormatMessageResponse | { mentioned_users?: any[]; mentioned_all?: boolean },
|
|
78
120
|
userMap: Record<string, string>,
|
|
79
|
-
renderWrapper?: (userId: string, name: string) => string
|
|
121
|
+
renderWrapper?: (userId: string, name: string) => string,
|
|
80
122
|
): string {
|
|
81
|
-
const mentionedUsers:
|
|
123
|
+
const mentionedUsers: any[] = (message as any).mentioned_users ?? [];
|
|
82
124
|
const mentionedAll: boolean = (message as any).mentioned_all ?? false;
|
|
83
125
|
|
|
84
126
|
// If no mentions, nothing to replace
|
|
@@ -88,9 +130,12 @@ export function replaceMentionsForPreview(
|
|
|
88
130
|
|
|
89
131
|
const replacements: { pattern: string; label: string }[] = [];
|
|
90
132
|
|
|
91
|
-
for (const
|
|
133
|
+
for (const userItem of mentionedUsers) {
|
|
134
|
+
if (!userItem) continue;
|
|
135
|
+
const userId = typeof userItem === 'string' ? userItem : userItem.id;
|
|
92
136
|
if (!userId) continue;
|
|
93
|
-
const
|
|
137
|
+
const itemObjName = typeof userItem === 'object' ? userItem.name : undefined;
|
|
138
|
+
const name = userMap[userId] ?? itemObjName ?? userId;
|
|
94
139
|
replacements.push({
|
|
95
140
|
pattern: `@${userId}`,
|
|
96
141
|
label: renderWrapper ? renderWrapper(userId, name) : `@${name}`,
|
|
@@ -100,7 +145,7 @@ export function replaceMentionsForPreview(
|
|
|
100
145
|
if (mentionedAll) {
|
|
101
146
|
replacements.push({
|
|
102
147
|
pattern: '@all',
|
|
103
|
-
label: renderWrapper ? renderWrapper('__all__', 'all') : '@all'
|
|
148
|
+
label: renderWrapper ? renderWrapper('__all__', 'all') : '@all',
|
|
104
149
|
});
|
|
105
150
|
}
|
|
106
151
|
|
|
@@ -120,14 +165,48 @@ export function replaceMentionsForPreview(
|
|
|
120
165
|
* Common helper to build a dictionary of User ID -> Display Name
|
|
121
166
|
* from the channel state, used for rendering Mentions and System logs.
|
|
122
167
|
*/
|
|
123
|
-
export function buildUserMap(channelState: any): Record<string, string> {
|
|
168
|
+
export function buildUserMap(channelState: any, extraUsers?: Record<string, any>): Record<string, string> {
|
|
124
169
|
const map: Record<string, string> = {};
|
|
170
|
+
const setDisplayName = (id: string, name?: string) => {
|
|
171
|
+
if (!id || !name) return;
|
|
172
|
+
const current = map[id];
|
|
173
|
+
if (current && current !== id) return;
|
|
174
|
+
map[id] = name;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// 1. Fallback: Global user cache from client state
|
|
178
|
+
if (extraUsers && typeof extraUsers === 'object') {
|
|
179
|
+
for (const [id, user] of Object.entries<any>(extraUsers)) {
|
|
180
|
+
setDisplayName(id, user?.name);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 2. Current members
|
|
125
185
|
const members = channelState?.members;
|
|
126
186
|
if (members && typeof members === 'object') {
|
|
127
187
|
for (const [id, member] of Object.entries<any>(members)) {
|
|
128
|
-
|
|
188
|
+
const name = member?.user?.name || member?.user_id || id;
|
|
189
|
+
setDisplayName(id, name);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 3. Fallback: scan latest messages
|
|
194
|
+
const messages = channelState?.latestMessages;
|
|
195
|
+
if (Array.isArray(messages)) {
|
|
196
|
+
messages.forEach((msg: any) => {
|
|
197
|
+
const u = msg.user;
|
|
198
|
+
setDisplayName(u?.id, u?.name);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 4. Fallback: check watchers
|
|
203
|
+
const watchers = channelState?.watchers;
|
|
204
|
+
if (watchers && typeof watchers === 'object') {
|
|
205
|
+
for (const [id, user] of Object.entries<any>(watchers)) {
|
|
206
|
+
setDisplayName(id, user?.name);
|
|
129
207
|
}
|
|
130
208
|
}
|
|
209
|
+
|
|
131
210
|
return map;
|
|
132
211
|
}
|
|
133
212
|
|
|
@@ -214,7 +293,11 @@ export function formatRelativeDate(dateStr: string): string {
|
|
|
214
293
|
if (diffDays === 1) return 'Yesterday';
|
|
215
294
|
if (diffDays < 7) return `${diffDays}d ago`;
|
|
216
295
|
|
|
217
|
-
return date.toLocaleDateString('en-US', {
|
|
296
|
+
return date.toLocaleDateString('en-US', {
|
|
297
|
+
month: 'short',
|
|
298
|
+
day: 'numeric',
|
|
299
|
+
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
|
|
300
|
+
});
|
|
218
301
|
}
|
|
219
302
|
|
|
220
303
|
/**
|
|
@@ -240,3 +323,123 @@ export function extractDomain(url: string): string {
|
|
|
240
323
|
return url;
|
|
241
324
|
}
|
|
242
325
|
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get a human-readable preview string for the last message,
|
|
329
|
+
* handling regular, system, and signal message types.
|
|
330
|
+
*/
|
|
331
|
+
export function getLastMessagePreview(
|
|
332
|
+
channel: Channel,
|
|
333
|
+
myUserId?: string,
|
|
334
|
+
options?: {
|
|
335
|
+
deletedMessageLabel?: React.ReactNode;
|
|
336
|
+
stickerMessageLabel?: React.ReactNode;
|
|
337
|
+
photoMessageLabel?: React.ReactNode;
|
|
338
|
+
videoMessageLabel?: React.ReactNode;
|
|
339
|
+
voiceRecordingMessageLabel?: React.ReactNode;
|
|
340
|
+
fileMessageLabel?: React.ReactNode;
|
|
341
|
+
encryptedMessageLabel?: React.ReactNode;
|
|
342
|
+
encryptedMessageUnavailableLabel?: React.ReactNode;
|
|
343
|
+
systemMessageTranslations?: SystemMessageTranslations;
|
|
344
|
+
signalMessageTranslations?: SignalMessageTranslations;
|
|
345
|
+
},
|
|
346
|
+
): { text: React.ReactNode; user: string; timestamp?: string | Date } {
|
|
347
|
+
const lastMsg = channel.state?.latestMessages?.slice(-1)[0];
|
|
348
|
+
if (!lastMsg) return { text: '', user: '' };
|
|
349
|
+
|
|
350
|
+
const timestamp = lastMsg.created_at;
|
|
351
|
+
|
|
352
|
+
const msgType = lastMsg.type || 'regular';
|
|
353
|
+
const isDeleted = isDeletedDisplayMessage(lastMsg);
|
|
354
|
+
const rawText = lastMsg.text ?? '';
|
|
355
|
+
const isEncrypted = lastMsg.content_type === 'mls' || Boolean((lastMsg as any).mls_ciphertext);
|
|
356
|
+
|
|
357
|
+
const client = (channel as any).getClient?.() || (channel as any).client;
|
|
358
|
+
const userMap = buildUserMap(channel.state, client?.state?.users);
|
|
359
|
+
|
|
360
|
+
if (isDeleted) {
|
|
361
|
+
return { text: options?.deletedMessageLabel || 'This message was deleted', user: '', timestamp };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (msgType === 'system') {
|
|
365
|
+
return { text: parseSystemMessage(rawText, userMap, options?.systemMessageTranslations), user: '', timestamp };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (msgType === 'signal') {
|
|
369
|
+
const result = parseSignalMessage(rawText, myUserId || '', options?.signalMessageTranslations);
|
|
370
|
+
return { text: result?.text || rawText, user: '', timestamp };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const userId = lastMsg.user_id || '';
|
|
374
|
+
const currentUser = userId && userId === myUserId ? client?.user : undefined;
|
|
375
|
+
const senderName = currentUser?.name || lastMsg.user?.name || (userId && userMap[userId]) || userId || '';
|
|
376
|
+
|
|
377
|
+
// Display 'Sticker' if message is a sticker
|
|
378
|
+
const isSticker = msgType === 'sticker' || (lastMsg as any).sticker_url;
|
|
379
|
+
if (isSticker) {
|
|
380
|
+
return { text: options?.stickerMessageLabel || 'Sticker', user: senderName, timestamp };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Regular / other
|
|
384
|
+
let displayText: React.ReactNode = rawText;
|
|
385
|
+
if (!displayText && lastMsg.attachments && lastMsg.attachments.length > 0) {
|
|
386
|
+
const att = lastMsg.attachments[0];
|
|
387
|
+
const type = att.type || '';
|
|
388
|
+
switch (type) {
|
|
389
|
+
case 'image':
|
|
390
|
+
displayText = options?.photoMessageLabel || '📷 Photo';
|
|
391
|
+
break;
|
|
392
|
+
case 'video':
|
|
393
|
+
displayText = options?.videoMessageLabel || '🎬 Video';
|
|
394
|
+
break;
|
|
395
|
+
case 'voiceRecording':
|
|
396
|
+
displayText = options?.voiceRecordingMessageLabel || '🎤 Voice message';
|
|
397
|
+
break;
|
|
398
|
+
default:
|
|
399
|
+
displayText = options?.fileMessageLabel || '📎 File';
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
if (lastMsg.attachments.length > 1) {
|
|
403
|
+
if (typeof displayText === 'string') {
|
|
404
|
+
displayText += ` +${lastMsg.attachments.length - 1}`;
|
|
405
|
+
} else {
|
|
406
|
+
const extraText = ` +${lastMsg.attachments.length - 1}`;
|
|
407
|
+
displayText = React.createElement(React.Fragment, null, displayText, extraText);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (!displayText && isEncrypted) {
|
|
412
|
+
displayText =
|
|
413
|
+
(lastMsg as any).e2ee_status === 'failed'
|
|
414
|
+
? options?.encryptedMessageUnavailableLabel || 'Encrypted message unavailable'
|
|
415
|
+
: options?.encryptedMessageLabel || 'Encrypted message';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Format mentions if necessary
|
|
419
|
+
const lastMsgRecord = lastMsg as any;
|
|
420
|
+
const mentionedUsers = lastMsgRecord.mentioned_users as string[] | undefined;
|
|
421
|
+
const mentionedAll = lastMsgRecord.mentioned_all as boolean | undefined;
|
|
422
|
+
|
|
423
|
+
if (
|
|
424
|
+
typeof displayText === 'string' &&
|
|
425
|
+
displayText &&
|
|
426
|
+
(mentionedAll || (mentionedUsers && mentionedUsers.length > 0))
|
|
427
|
+
) {
|
|
428
|
+
displayText = replaceMentionsForPreview(displayText, lastMsg as any, userMap);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
text: displayText,
|
|
433
|
+
user: senderName,
|
|
434
|
+
timestamp,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Count the number of words in a string.
|
|
440
|
+
* A word is defined as a non-empty sequence of characters separated by whitespace.
|
|
441
|
+
*/
|
|
442
|
+
export function countWords(str: string): number {
|
|
443
|
+
if (!str) return 0;
|
|
444
|
+
return str.trim().split(/\s+/).filter(Boolean).length;
|
|
445
|
+
}
|