@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
package/src/types.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
ChannelFilters,
|
|
7
7
|
ChannelSort,
|
|
8
8
|
ChannelQueryOptions,
|
|
9
|
+
E2eeRecoveryPolicy,
|
|
10
|
+
E2eeAttachmentManifest,
|
|
9
11
|
UserCallInfo,
|
|
10
12
|
SystemMessageTranslations,
|
|
11
13
|
SignalMessageTranslations,
|
|
@@ -56,6 +58,12 @@ export type ChatContextValue = {
|
|
|
56
58
|
setJumpToMessageId: (id: string | null) => void;
|
|
57
59
|
/** Indicates whether the direct call feature is enabled */
|
|
58
60
|
enableCall?: boolean;
|
|
61
|
+
/** Save a draft message (innerHTML and files) for a specific channel */
|
|
62
|
+
setDraft: (cid: string, draft: { html: string; files: any[] }) => void;
|
|
63
|
+
/** Retrieve the saved draft for a specific channel */
|
|
64
|
+
getDraft: (cid: string) => { html: string; files: any[] } | undefined;
|
|
65
|
+
/** Clear all saved drafts (e.g. on logout) */
|
|
66
|
+
clearAllDrafts: () => void;
|
|
59
67
|
};
|
|
60
68
|
|
|
61
69
|
import type { ChatComponentsContextValue } from './context/ChatComponentsContext';
|
|
@@ -474,6 +482,8 @@ export type TopicListProps = {
|
|
|
474
482
|
videoMessageLabel?: React.ReactNode;
|
|
475
483
|
voiceRecordingMessageLabel?: React.ReactNode;
|
|
476
484
|
fileMessageLabel?: React.ReactNode;
|
|
485
|
+
encryptedMessageLabel?: React.ReactNode;
|
|
486
|
+
encryptedMessageUnavailableLabel?: React.ReactNode;
|
|
477
487
|
systemMessageTranslations?: SystemMessageTranslations;
|
|
478
488
|
signalMessageTranslations?: SignalMessageTranslations;
|
|
479
489
|
};
|
|
@@ -522,6 +532,10 @@ export type ChannelListProps = {
|
|
|
522
532
|
voiceRecordingMessageLabel?: React.ReactNode;
|
|
523
533
|
/** Label for file messages in the preview strip (default: '📎 File') */
|
|
524
534
|
fileMessageLabel?: React.ReactNode;
|
|
535
|
+
/** Label for encrypted messages in the preview strip (default: 'Encrypted message') */
|
|
536
|
+
encryptedMessageLabel?: React.ReactNode;
|
|
537
|
+
/** Label for encrypted messages that failed in the preview strip (default: 'Encrypted message unavailable') */
|
|
538
|
+
encryptedMessageUnavailableLabel?: React.ReactNode;
|
|
525
539
|
/** Custom translation templates for system messages in the preview strip */
|
|
526
540
|
systemMessageTranslations?: SystemMessageTranslations;
|
|
527
541
|
/** Custom translation templates for signal (call) messages in the preview strip */
|
|
@@ -562,6 +576,8 @@ export type ChannelListProps = {
|
|
|
562
576
|
FlatTopicGroupItemComponent?: React.ComponentType<any>;
|
|
563
577
|
/** Auto-scroll the channel list to the top when the current user sends a message (default: true) */
|
|
564
578
|
scrollToTopOnOwnMessage?: boolean;
|
|
579
|
+
/** Whether to show topic pills on team channels (default: false) */
|
|
580
|
+
showTopicPills?: boolean;
|
|
565
581
|
};
|
|
566
582
|
|
|
567
583
|
/* ----------------------------------------------------------
|
|
@@ -579,6 +595,12 @@ export type MessageRendererProps = {
|
|
|
579
595
|
systemMessageTranslations?: SystemMessageTranslations;
|
|
580
596
|
signalMessageTranslations?: SignalMessageTranslations;
|
|
581
597
|
onMentionClick?: (userId: string) => void;
|
|
598
|
+
/** I18n Label for encrypted messages (default: 'Encrypted message') */
|
|
599
|
+
encryptedMessageLabel?: string;
|
|
600
|
+
/** I18n Label for encrypted messages that failed to decrypt (default: 'Encrypted message could not be decrypted') */
|
|
601
|
+
encryptedMessageFailedLabel?: string;
|
|
602
|
+
/** I18n Label for encrypted messages being decrypted (default: 'Decrypting encrypted message...') */
|
|
603
|
+
encryptedMessageDecryptingLabel?: string;
|
|
582
604
|
};
|
|
583
605
|
|
|
584
606
|
export type MessageBubbleProps = {
|
|
@@ -600,9 +622,14 @@ export type JumpToLatestProps = {
|
|
|
600
622
|
---------------------------------------------------------- */
|
|
601
623
|
export type MediaLightboxItem = {
|
|
602
624
|
type: 'image' | 'video';
|
|
603
|
-
src
|
|
625
|
+
src?: string;
|
|
604
626
|
alt?: string;
|
|
605
627
|
posterSrc?: string;
|
|
628
|
+
loading?: boolean;
|
|
629
|
+
progressLabel?: string;
|
|
630
|
+
download?: () => Promise<void> | void;
|
|
631
|
+
onPlaybackError?: (context?: { currentTime?: number }) => Promise<void> | void;
|
|
632
|
+
onDispose?: () => Promise<void> | void;
|
|
606
633
|
};
|
|
607
634
|
|
|
608
635
|
export type MediaLightboxProps = {
|
|
@@ -713,6 +740,18 @@ export type MessageListProps = {
|
|
|
713
740
|
typingIndicatorLabel?: (users: Array<{ id: string; name?: string }>) => string;
|
|
714
741
|
/** I18n Label for deleted display messages (display_type === 'deleted') */
|
|
715
742
|
deletedMessageLabel?: string;
|
|
743
|
+
/** I18n Label for attachment-only previews */
|
|
744
|
+
attachmentLabel?: string;
|
|
745
|
+
/** I18n Label for messages whose contents are unavailable */
|
|
746
|
+
unavailableMessageLabel?: string;
|
|
747
|
+
/** I18n Label for encrypted messages (default: 'Encrypted message') */
|
|
748
|
+
encryptedMessageLabel?: string;
|
|
749
|
+
/** I18n Label for encrypted messages that failed to decrypt (default: 'Encrypted message could not be decrypted') */
|
|
750
|
+
encryptedMessageFailedLabel?: string;
|
|
751
|
+
/** I18n Label for encrypted messages being decrypted (default: 'Decrypting encrypted message...') */
|
|
752
|
+
encryptedMessageDecryptingLabel?: string;
|
|
753
|
+
/** I18n Label for encrypted messages unavailable in sidebar preview (default: 'Encrypted message unavailable') */
|
|
754
|
+
encryptedMessageUnavailableLabel?: string;
|
|
716
755
|
/** Custom translation templates for system messages */
|
|
717
756
|
systemMessageTranslations?: SystemMessageTranslations;
|
|
718
757
|
/** Custom translation templates for signal (call) messages */
|
|
@@ -815,6 +854,18 @@ export type MessageItemProps = {
|
|
|
815
854
|
editedLabel?: string;
|
|
816
855
|
/** I18n Label for deleted display messages (display_type === 'deleted') */
|
|
817
856
|
deletedMessageLabel?: React.ReactNode;
|
|
857
|
+
/** I18n Label for attachment-only previews */
|
|
858
|
+
attachmentLabel?: string;
|
|
859
|
+
/** I18n Label for messages whose contents are unavailable */
|
|
860
|
+
unavailableMessageLabel?: string;
|
|
861
|
+
/** I18n Label for sticker message previews */
|
|
862
|
+
stickerLabel?: string;
|
|
863
|
+
/** I18n Label for encrypted messages (default: 'Encrypted message') */
|
|
864
|
+
encryptedMessageLabel?: string;
|
|
865
|
+
/** I18n Label for encrypted messages that failed to decrypt (default: 'Encrypted message could not be decrypted') */
|
|
866
|
+
encryptedMessageFailedLabel?: string;
|
|
867
|
+
/** I18n Label for encrypted messages being decrypted (default: 'Decrypting encrypted message...') */
|
|
868
|
+
encryptedMessageDecryptingLabel?: string;
|
|
818
869
|
/** Custom translation templates for system messages */
|
|
819
870
|
systemMessageTranslations?: SystemMessageTranslations;
|
|
820
871
|
/** Custom translation templates for signal (call) messages */
|
|
@@ -825,6 +876,8 @@ export type MessageItemProps = {
|
|
|
825
876
|
onUserNameClick?: (userId: string) => void;
|
|
826
877
|
/** Handler when clicking to add a custom reaction */
|
|
827
878
|
onAddReactionClick?: (e: React.MouseEvent, messageId: string) => void;
|
|
879
|
+
/** When true, the avatar column is not rendered (handled by group wrapper) */
|
|
880
|
+
hideAvatar?: boolean;
|
|
828
881
|
};
|
|
829
882
|
|
|
830
883
|
export type SystemMessageItemProps = {
|
|
@@ -1007,6 +1060,8 @@ export type PinnedMessageItemProps = {
|
|
|
1007
1060
|
AvatarComponent: React.ComponentType<AvatarProps>;
|
|
1008
1061
|
unpinLabel?: string;
|
|
1009
1062
|
stickerLabel?: string;
|
|
1063
|
+
attachmentLabel?: string;
|
|
1064
|
+
unavailableMessageLabel?: string;
|
|
1010
1065
|
};
|
|
1011
1066
|
|
|
1012
1067
|
export type PinnedMessagesProps = {
|
|
@@ -1026,6 +1081,8 @@ export type PinnedMessagesProps = {
|
|
|
1026
1081
|
collapseLabel?: string;
|
|
1027
1082
|
unpinLabel?: string;
|
|
1028
1083
|
stickerLabel?: string;
|
|
1084
|
+
attachmentLabel?: string;
|
|
1085
|
+
unavailableMessageLabel?: string;
|
|
1029
1086
|
};
|
|
1030
1087
|
|
|
1031
1088
|
/* ----------------------------------------------------------
|
|
@@ -1037,11 +1094,28 @@ export type QuotedMessagePreviewProps = {
|
|
|
1037
1094
|
id: string;
|
|
1038
1095
|
text?: string;
|
|
1039
1096
|
user?: { id?: string; name?: string };
|
|
1097
|
+
attachments?: Attachment[];
|
|
1098
|
+
content_type?: string;
|
|
1099
|
+
mls_ciphertext?: unknown;
|
|
1100
|
+
e2ee_status?: string;
|
|
1101
|
+
sticker_url?: string;
|
|
1102
|
+
type?: string;
|
|
1103
|
+
display_type?: string;
|
|
1104
|
+
mentioned_users?: string[];
|
|
1105
|
+
mentioned_all?: boolean;
|
|
1040
1106
|
};
|
|
1041
1107
|
/** Whether the parent message is from the current user */
|
|
1042
1108
|
isOwnMessage: boolean;
|
|
1043
1109
|
/** Callback when the quote box is clicked */
|
|
1044
1110
|
onClick: (messageId: string) => void;
|
|
1111
|
+
/** I18n Label for attachment-only quoted messages */
|
|
1112
|
+
attachmentLabel?: string;
|
|
1113
|
+
/** I18n Label for quoted messages whose contents are unavailable */
|
|
1114
|
+
unavailableMessageLabel?: string;
|
|
1115
|
+
/** I18n Label for sticker quoted messages */
|
|
1116
|
+
stickerLabel?: string;
|
|
1117
|
+
/** I18n Label for deleted messages */
|
|
1118
|
+
deletedMessageLabel?: string;
|
|
1045
1119
|
};
|
|
1046
1120
|
|
|
1047
1121
|
/* ----------------------------------------------------------
|
|
@@ -1111,6 +1185,10 @@ export type FilePreviewItem = {
|
|
|
1111
1185
|
previewUrl?: string;
|
|
1112
1186
|
/** Upload status */
|
|
1113
1187
|
status: 'pending' | 'uploading' | 'done' | 'error';
|
|
1188
|
+
/** E2EE upload phase when the file is handled by MLS attachment flow */
|
|
1189
|
+
e2eePhase?: 'generating_preview' | 'encrypting' | 'uploading' | 'completing' | 'sending' | 'retrying' | 'failed';
|
|
1190
|
+
/** Upload progress percentage (0-100) */
|
|
1191
|
+
progress?: number;
|
|
1114
1192
|
/** Error message if upload failed */
|
|
1115
1193
|
error?: string;
|
|
1116
1194
|
/** URL returned after successful upload */
|
|
@@ -1200,6 +1278,8 @@ export type AttachmentItem = {
|
|
|
1200
1278
|
og_scrape_url?: string;
|
|
1201
1279
|
image_url?: string;
|
|
1202
1280
|
text?: string;
|
|
1281
|
+
e2ee_manifest?: E2eeAttachmentManifest;
|
|
1282
|
+
e2ee_manifest_missing?: boolean;
|
|
1203
1283
|
};
|
|
1204
1284
|
|
|
1205
1285
|
export type MediaTab = 'members' | 'media' | 'links' | 'files';
|
|
@@ -1272,9 +1352,14 @@ export type ChannelInfoCoverProps = {
|
|
|
1272
1352
|
isTopic?: boolean;
|
|
1273
1353
|
/** Whether the channel is a team channel */
|
|
1274
1354
|
isTeamChannel?: boolean;
|
|
1355
|
+
/** Whether this channel or inherited parent topic is E2EE enabled */
|
|
1356
|
+
isE2ee?: boolean;
|
|
1357
|
+
/** Current encryption epoch, if available */
|
|
1358
|
+
encryptionEpoch?: number;
|
|
1275
1359
|
};
|
|
1276
1360
|
|
|
1277
1361
|
export type ChannelInfoActionsProps = {
|
|
1362
|
+
channel?: Channel;
|
|
1278
1363
|
onSearchClick?: () => void;
|
|
1279
1364
|
onSettingsClick?: () => void;
|
|
1280
1365
|
onLeaveChannel?: () => void;
|
|
@@ -1308,6 +1393,15 @@ export type ChannelInfoActionsProps = {
|
|
|
1308
1393
|
onCreateTopic?: () => void;
|
|
1309
1394
|
createTopicLabel?: string;
|
|
1310
1395
|
topicsEnabled?: boolean;
|
|
1396
|
+
isE2ee?: boolean;
|
|
1397
|
+
encryptionInitialized?: boolean;
|
|
1398
|
+
encryptionEpoch?: number;
|
|
1399
|
+
onRotateKey?: () => void;
|
|
1400
|
+
rotateKeyLabel?: string;
|
|
1401
|
+
rotateKeyDisabled?: boolean;
|
|
1402
|
+
onEnableE2ee?: () => void;
|
|
1403
|
+
enableE2eeLabel?: string;
|
|
1404
|
+
enableE2eeDisabled?: boolean;
|
|
1311
1405
|
};
|
|
1312
1406
|
|
|
1313
1407
|
export type ChannelInfoMember = {
|
|
@@ -1710,6 +1804,7 @@ export type CreateChannelFooterProps = {
|
|
|
1710
1804
|
messageButtonLabel?: string;
|
|
1711
1805
|
nextButtonLabel?: string;
|
|
1712
1806
|
backButtonLabel?: string;
|
|
1807
|
+
e2eeEnabled?: boolean;
|
|
1713
1808
|
};
|
|
1714
1809
|
|
|
1715
1810
|
export type CreateChannelGroupFieldsProps = {
|
|
@@ -1725,12 +1820,27 @@ export type CreateChannelGroupFieldsProps = {
|
|
|
1725
1820
|
groupDescriptionLabel?: string;
|
|
1726
1821
|
groupDescriptionPlaceholder?: string;
|
|
1727
1822
|
groupPublicLabel?: string;
|
|
1823
|
+
e2eeEnabled?: boolean;
|
|
1824
|
+
onE2eeChange?: (enabled: boolean) => void;
|
|
1825
|
+
e2eeLabel?: string;
|
|
1826
|
+
e2eeDescription?: string;
|
|
1827
|
+
e2eeDisabled?: boolean;
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1830
|
+
export type CreateChannelE2eeToggleProps = {
|
|
1831
|
+
enabled: boolean;
|
|
1832
|
+
onChange: (enabled: boolean) => void;
|
|
1833
|
+
disabled?: boolean;
|
|
1834
|
+
label?: string;
|
|
1835
|
+
description?: string;
|
|
1728
1836
|
};
|
|
1729
1837
|
|
|
1730
1838
|
export type CreateChannelModalProps = {
|
|
1731
1839
|
isOpen: boolean;
|
|
1732
1840
|
onClose: () => void;
|
|
1733
1841
|
onSuccess?: (channel: any) => void; // Uses 'any' or 'Channel' based on context
|
|
1842
|
+
/** Recovery coverage policy for newly created E2EE channels. Defaults to member_assisted. */
|
|
1843
|
+
e2eeRecoveryPolicy?: E2eeRecoveryPolicy;
|
|
1734
1844
|
|
|
1735
1845
|
/** Override visual components */
|
|
1736
1846
|
AvatarComponent?: React.ComponentType<AvatarProps>;
|
|
@@ -1744,6 +1854,7 @@ export type CreateChannelModalProps = {
|
|
|
1744
1854
|
placeholder: string;
|
|
1745
1855
|
}>;
|
|
1746
1856
|
SelectedBoxComponent?: React.ComponentType<UserPickerSelectedBoxProps>;
|
|
1857
|
+
E2eeToggleComponent?: React.ComponentType<CreateChannelE2eeToggleProps>;
|
|
1747
1858
|
|
|
1748
1859
|
/** i18n labels */
|
|
1749
1860
|
title?: string;
|
|
@@ -1763,6 +1874,9 @@ export type CreateChannelModalProps = {
|
|
|
1763
1874
|
nextButtonLabel?: string;
|
|
1764
1875
|
backButtonLabel?: string;
|
|
1765
1876
|
emptyStateLabel?: string;
|
|
1877
|
+
e2eeLabel?: string;
|
|
1878
|
+
e2eeDescription?: string;
|
|
1879
|
+
e2eeUnavailableLabel?: string;
|
|
1766
1880
|
|
|
1767
1881
|
/** File upload configuration for group channel images */
|
|
1768
1882
|
imageAccept?: string;
|
|
@@ -22,7 +22,7 @@ const AVATAR_GRADIENTS: readonly [string, string][] = [
|
|
|
22
22
|
] as const;
|
|
23
23
|
|
|
24
24
|
/** Neutral fallback when no name is available. */
|
|
25
|
-
const FALLBACK_GRADIENT = 'linear-gradient(135deg, #6B7280 0%, #
|
|
25
|
+
const FALLBACK_GRADIENT = 'linear-gradient(135deg, #6B7280 0%, #545f71 100%)';
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Simple djb2-variant string hash → non-negative integer.
|
package/src/utils.ts
CHANGED
|
@@ -22,12 +22,15 @@ export function removeAccents(str: string): string {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Format a Date or date-string to a short time string (HH:MM).
|
|
25
|
+
* Format a Date or date-string to a short time string (HH:MM, 24-hour).
|
|
26
|
+
* Matches Telegram's compact time display.
|
|
26
27
|
*/
|
|
27
28
|
export function formatTime(date: Date | string | undefined): string {
|
|
28
29
|
if (!date) return '';
|
|
29
30
|
const d = date instanceof Date ? date : new Date(date);
|
|
30
|
-
|
|
31
|
+
const hours = String(d.getHours()).padStart(2, '0');
|
|
32
|
+
const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
33
|
+
return `${hours}:${minutes}`;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -113,11 +116,11 @@ export function getMessageUserId(message: FormatMessageResponse): string {
|
|
|
113
116
|
*/
|
|
114
117
|
export function replaceMentionsForPreview(
|
|
115
118
|
text: string,
|
|
116
|
-
message: FormatMessageResponse | { mentioned_users?:
|
|
119
|
+
message: FormatMessageResponse | { mentioned_users?: any[]; mentioned_all?: boolean },
|
|
117
120
|
userMap: Record<string, string>,
|
|
118
121
|
renderWrapper?: (userId: string, name: string) => string,
|
|
119
122
|
): string {
|
|
120
|
-
const mentionedUsers:
|
|
123
|
+
const mentionedUsers: any[] = (message as any).mentioned_users ?? [];
|
|
121
124
|
const mentionedAll: boolean = (message as any).mentioned_all ?? false;
|
|
122
125
|
|
|
123
126
|
// If no mentions, nothing to replace
|
|
@@ -127,9 +130,12 @@ export function replaceMentionsForPreview(
|
|
|
127
130
|
|
|
128
131
|
const replacements: { pattern: string; label: string }[] = [];
|
|
129
132
|
|
|
130
|
-
for (const
|
|
133
|
+
for (const userItem of mentionedUsers) {
|
|
134
|
+
if (!userItem) continue;
|
|
135
|
+
const userId = typeof userItem === 'string' ? userItem : userItem.id;
|
|
131
136
|
if (!userId) continue;
|
|
132
|
-
const
|
|
137
|
+
const itemObjName = typeof userItem === 'object' ? userItem.name : undefined;
|
|
138
|
+
const name = userMap[userId] ?? itemObjName ?? userId;
|
|
133
139
|
replacements.push({
|
|
134
140
|
pattern: `@${userId}`,
|
|
135
141
|
label: renderWrapper ? renderWrapper(userId, name) : `@${name}`,
|
|
@@ -161,13 +167,17 @@ export function replaceMentionsForPreview(
|
|
|
161
167
|
*/
|
|
162
168
|
export function buildUserMap(channelState: any, extraUsers?: Record<string, any>): Record<string, string> {
|
|
163
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
|
+
};
|
|
164
176
|
|
|
165
177
|
// 1. Fallback: Global user cache from client state
|
|
166
178
|
if (extraUsers && typeof extraUsers === 'object') {
|
|
167
179
|
for (const [id, user] of Object.entries<any>(extraUsers)) {
|
|
168
|
-
|
|
169
|
-
map[id] = user.name;
|
|
170
|
-
}
|
|
180
|
+
setDisplayName(id, user?.name);
|
|
171
181
|
}
|
|
172
182
|
}
|
|
173
183
|
|
|
@@ -176,7 +186,7 @@ export function buildUserMap(channelState: any, extraUsers?: Record<string, any>
|
|
|
176
186
|
if (members && typeof members === 'object') {
|
|
177
187
|
for (const [id, member] of Object.entries<any>(members)) {
|
|
178
188
|
const name = member?.user?.name || member?.user_id || id;
|
|
179
|
-
|
|
189
|
+
setDisplayName(id, name);
|
|
180
190
|
}
|
|
181
191
|
}
|
|
182
192
|
|
|
@@ -185,9 +195,7 @@ export function buildUserMap(channelState: any, extraUsers?: Record<string, any>
|
|
|
185
195
|
if (Array.isArray(messages)) {
|
|
186
196
|
messages.forEach((msg: any) => {
|
|
187
197
|
const u = msg.user;
|
|
188
|
-
|
|
189
|
-
map[u.id] = u.name;
|
|
190
|
-
}
|
|
198
|
+
setDisplayName(u?.id, u?.name);
|
|
191
199
|
});
|
|
192
200
|
}
|
|
193
201
|
|
|
@@ -195,9 +203,7 @@ export function buildUserMap(channelState: any, extraUsers?: Record<string, any>
|
|
|
195
203
|
const watchers = channelState?.watchers;
|
|
196
204
|
if (watchers && typeof watchers === 'object') {
|
|
197
205
|
for (const [id, user] of Object.entries<any>(watchers)) {
|
|
198
|
-
|
|
199
|
-
map[id] = user.name;
|
|
200
|
-
}
|
|
206
|
+
setDisplayName(id, user?.name);
|
|
201
207
|
}
|
|
202
208
|
}
|
|
203
209
|
|
|
@@ -332,6 +338,8 @@ export function getLastMessagePreview(
|
|
|
332
338
|
videoMessageLabel?: React.ReactNode;
|
|
333
339
|
voiceRecordingMessageLabel?: React.ReactNode;
|
|
334
340
|
fileMessageLabel?: React.ReactNode;
|
|
341
|
+
encryptedMessageLabel?: React.ReactNode;
|
|
342
|
+
encryptedMessageUnavailableLabel?: React.ReactNode;
|
|
335
343
|
systemMessageTranslations?: SystemMessageTranslations;
|
|
336
344
|
signalMessageTranslations?: SignalMessageTranslations;
|
|
337
345
|
},
|
|
@@ -344,6 +352,7 @@ export function getLastMessagePreview(
|
|
|
344
352
|
const msgType = lastMsg.type || 'regular';
|
|
345
353
|
const isDeleted = isDeletedDisplayMessage(lastMsg);
|
|
346
354
|
const rawText = lastMsg.text ?? '';
|
|
355
|
+
const isEncrypted = lastMsg.content_type === 'mls' || Boolean((lastMsg as any).mls_ciphertext);
|
|
347
356
|
|
|
348
357
|
const client = (channel as any).getClient?.() || (channel as any).client;
|
|
349
358
|
const userMap = buildUserMap(channel.state, client?.state?.users);
|
|
@@ -362,7 +371,8 @@ export function getLastMessagePreview(
|
|
|
362
371
|
}
|
|
363
372
|
|
|
364
373
|
const userId = lastMsg.user_id || '';
|
|
365
|
-
const
|
|
374
|
+
const currentUser = userId && userId === myUserId ? client?.user : undefined;
|
|
375
|
+
const senderName = currentUser?.name || lastMsg.user?.name || (userId && userMap[userId]) || userId || '';
|
|
366
376
|
|
|
367
377
|
// Display 'Sticker' if message is a sticker
|
|
368
378
|
const isSticker = msgType === 'sticker' || (lastMsg as any).sticker_url;
|
|
@@ -398,13 +408,23 @@ export function getLastMessagePreview(
|
|
|
398
408
|
}
|
|
399
409
|
}
|
|
400
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
|
+
}
|
|
401
417
|
|
|
402
418
|
// Format mentions if necessary
|
|
403
419
|
const lastMsgRecord = lastMsg as any;
|
|
404
420
|
const mentionedUsers = lastMsgRecord.mentioned_users as string[] | undefined;
|
|
405
421
|
const mentionedAll = lastMsgRecord.mentioned_all as boolean | undefined;
|
|
406
422
|
|
|
407
|
-
if (
|
|
423
|
+
if (
|
|
424
|
+
typeof displayText === 'string' &&
|
|
425
|
+
displayText &&
|
|
426
|
+
(mentionedAll || (mentionedUsers && mentionedUsers.length > 0))
|
|
427
|
+
) {
|
|
408
428
|
displayText = replaceMentionsForPreview(displayText, lastMsg as any, userMap);
|
|
409
429
|
}
|
|
410
430
|
|