@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.
Files changed (72) hide show
  1. package/README.md +144 -0
  2. package/dist/index.cjs +5087 -11279
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +632 -152
  5. package/dist/index.css.map +1 -1
  6. package/dist/index.d.mts +273 -9
  7. package/dist/index.d.ts +273 -9
  8. package/dist/index.mjs +5085 -11295
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +2 -2
  11. package/src/components/Channel.tsx +0 -3
  12. package/src/components/ChannelActions.tsx +6 -1
  13. package/src/components/ChannelHeader.tsx +8 -32
  14. package/src/components/ChannelInfo/AddMemberModal.tsx +7 -1
  15. package/src/components/ChannelInfo/ChannelInfo.tsx +82 -2
  16. package/src/components/ChannelInfo/EditChannelModal.tsx +2 -2
  17. package/src/components/ChannelInfo/MediaGridItem.tsx +215 -78
  18. package/src/components/ChannelInfo/useChannelInfoTabs.tsx +170 -129
  19. package/src/components/ChannelList.tsx +72 -13
  20. package/src/components/CreateChannelModal.tsx +131 -12
  21. package/src/components/FilesPreview.tsx +8 -12
  22. package/src/components/FlatTopicGroupItem.tsx +27 -16
  23. package/src/components/ForwardMessageModal.tsx +11 -3
  24. package/src/components/MediaLightbox.tsx +444 -304
  25. package/src/components/MessageActionsBox.tsx +2 -0
  26. package/src/components/MessageInput.tsx +41 -12
  27. package/src/components/MessageItem.tsx +70 -25
  28. package/src/components/MessageQuickReactions.tsx +131 -128
  29. package/src/components/MessageReactions.tsx +47 -2
  30. package/src/components/MessageRenderers.tsx +1030 -433
  31. package/src/components/PinnedMessages.tsx +40 -12
  32. package/src/components/QuotedMessagePreview.tsx +99 -8
  33. package/src/components/RecoveryPin/RecoveryPin.tsx +279 -0
  34. package/src/components/RecoveryPin/index.ts +19 -0
  35. package/src/components/TopicList.tsx +20 -5
  36. package/src/components/TypingIndicator.tsx +3 -3
  37. package/src/components/UserPicker.tsx +26 -25
  38. package/src/components/VirtualMessageList.tsx +345 -125
  39. package/src/context/ChatProvider.tsx +27 -1
  40. package/src/hooks/useChannelListUpdates.ts +22 -1
  41. package/src/hooks/useChannelMessages.ts +338 -51
  42. package/src/hooks/useChannelRowUpdates.ts +18 -6
  43. package/src/hooks/useChatUser.ts +9 -1
  44. package/src/hooks/useE2eeAttachmentRenderer.ts +204 -0
  45. package/src/hooks/useE2eeFileUpload.ts +38 -0
  46. package/src/hooks/useFileUpload.ts +25 -5
  47. package/src/hooks/useForwardMessage.ts +210 -13
  48. package/src/hooks/useLoadMessages.ts +16 -4
  49. package/src/hooks/useMentions.ts +60 -6
  50. package/src/hooks/useMessageActions.ts +14 -8
  51. package/src/hooks/useMessageSend.ts +64 -12
  52. package/src/hooks/usePendingE2eeSends.ts +29 -0
  53. package/src/hooks/useRecoveryPin.ts +287 -0
  54. package/src/hooks/useScrollToMessage.ts +29 -4
  55. package/src/hooks/useTopicGroupUpdates.ts +49 -11
  56. package/src/index.ts +23 -0
  57. package/src/messageTypeUtils.ts +14 -0
  58. package/src/styles/_channel-info.css +9 -0
  59. package/src/styles/_channel-list.css +37 -14
  60. package/src/styles/_media-lightbox.css +36 -3
  61. package/src/styles/_message-bubble.css +381 -41
  62. package/src/styles/_message-input.css +8 -0
  63. package/src/styles/_message-list.css +67 -10
  64. package/src/styles/_message-quick-reactions.css +101 -59
  65. package/src/styles/_message-reactions.css +18 -32
  66. package/src/styles/_recovery-pin.css +97 -0
  67. package/src/styles/_tokens.css +5 -5
  68. package/src/styles/_typing-indicator.css +23 -13
  69. package/src/styles/index.css +1 -0
  70. package/src/types.ts +115 -1
  71. package/src/utils/avatarColors.ts +1 -1
  72. 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: string;
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%, #9CA3AF 100%)';
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
- return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
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?: string[]; mentioned_all?: boolean },
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: string[] = (message as any).mentioned_users ?? [];
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 userId of mentionedUsers) {
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 name = userMap[userId] ?? userId;
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
- if (user?.name) {
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
- if (name) map[id] = name;
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
- if (u?.id && !map[u.id] && u.name) {
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
- if (!map[id] && user?.name) {
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 senderName = (userId && userMap[userId]) || lastMsg.user?.name || userId || '';
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 (typeof displayText === 'string' && displayText && (mentionedAll || (mentionedUsers && mentionedUsers.length > 0))) {
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