@droppii-org/chat-mobile 0.2.79 → 0.2.81

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 (187) hide show
  1. package/lib/module/components/messages/BubbleTail.js +31 -0
  2. package/lib/module/components/messages/BubbleTail.js.map +1 -0
  3. package/lib/module/components/messages/BubbleTailRow.js +46 -0
  4. package/lib/module/components/messages/BubbleTailRow.js.map +1 -0
  5. package/lib/module/components/messages/bubbleTail.utils.js +19 -0
  6. package/lib/module/components/messages/bubbleTail.utils.js.map +1 -0
  7. package/lib/module/components/messages/fileMessage/index.js +72 -65
  8. package/lib/module/components/messages/fileMessage/index.js.map +1 -1
  9. package/lib/module/components/messages/imageMessage/index.js +23 -17
  10. package/lib/module/components/messages/imageMessage/index.js.map +1 -1
  11. package/lib/module/components/messages/linkMessage/index.js +11 -23
  12. package/lib/module/components/messages/linkMessage/index.js.map +1 -1
  13. package/lib/module/components/messages/mergedMessage/index.js +14 -7
  14. package/lib/module/components/messages/mergedMessage/index.js.map +1 -1
  15. package/lib/module/components/messages/quotedMessage/index.js +11 -23
  16. package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
  17. package/lib/module/components/messages/revokedMessage/index.js +14 -7
  18. package/lib/module/components/messages/revokedMessage/index.js.map +1 -1
  19. package/lib/module/components/messages/stickerMessage/index.js +13 -7
  20. package/lib/module/components/messages/stickerMessage/index.js.map +1 -1
  21. package/lib/module/components/messages/textMessage/index.js +11 -26
  22. package/lib/module/components/messages/textMessage/index.js.map +1 -1
  23. package/lib/module/components/messages/types.js.map +1 -1
  24. package/lib/module/components/messages/videoMessage/index.js +16 -8
  25. package/lib/module/components/messages/videoMessage/index.js.map +1 -1
  26. package/lib/module/config/api-endpoints.js +2 -1
  27. package/lib/module/config/api-endpoints.js.map +1 -1
  28. package/lib/module/hooks/contacts/useSearchContacts.js +50 -0
  29. package/lib/module/hooks/contacts/useSearchContacts.js.map +1 -0
  30. package/lib/module/hooks/groups/useDismissGroup.js +2 -1
  31. package/lib/module/hooks/groups/useDismissGroup.js.map +1 -1
  32. package/lib/module/hooks/query-keys.js +4 -0
  33. package/lib/module/hooks/query-keys.js.map +1 -1
  34. package/lib/module/screens/SearchConversation/ContactResultItem.js +119 -0
  35. package/lib/module/screens/SearchConversation/ContactResultItem.js.map +1 -0
  36. package/lib/module/screens/SearchConversation/SearchAllResult.js +26 -20
  37. package/lib/module/screens/SearchConversation/SearchAllResult.js.map +1 -1
  38. package/lib/module/screens/SearchConversation/SearchCommunityResult.js +79 -0
  39. package/lib/module/screens/SearchConversation/SearchCommunityResult.js.map +1 -0
  40. package/lib/module/screens/SearchConversation/SearchContactsResult.js +117 -0
  41. package/lib/module/screens/SearchConversation/SearchContactsResult.js.map +1 -0
  42. package/lib/module/screens/SearchConversation/SearchEmpty.js +2 -0
  43. package/lib/module/screens/SearchConversation/SearchEmpty.js.map +1 -1
  44. package/lib/module/screens/SearchConversation/SearchMessagesResult.js +1 -8
  45. package/lib/module/screens/SearchConversation/SearchMessagesResult.js.map +1 -1
  46. package/lib/module/screens/SearchConversation/SearchResultItem.js +2 -0
  47. package/lib/module/screens/SearchConversation/SearchResultItem.js.map +1 -1
  48. package/lib/module/screens/SearchConversation/TabResult.js +24 -7
  49. package/lib/module/screens/SearchConversation/TabResult.js.map +1 -1
  50. package/lib/module/screens/SearchConversation/index.js +6 -2
  51. package/lib/module/screens/SearchConversation/index.js.map +1 -1
  52. package/lib/module/screens/SearchConversation/types.js +1 -0
  53. package/lib/module/screens/SearchConversation/types.js.map +1 -1
  54. package/lib/module/screens/chat-detail/ChatListLegend.js +71 -21
  55. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  56. package/lib/module/screens/chat-detail/legend/LegendChatDay.js +8 -2
  57. package/lib/module/screens/chat-detail/legend/LegendChatDay.js.map +1 -1
  58. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +38 -13
  59. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  60. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js +11 -1
  61. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js.map +1 -1
  62. package/lib/module/services/endpoints.js +2 -1
  63. package/lib/module/services/endpoints.js.map +1 -1
  64. package/lib/module/translation/resources/i18n.js +6 -0
  65. package/lib/module/translation/resources/i18n.js.map +1 -1
  66. package/lib/module/types/chat.js +8 -0
  67. package/lib/module/types/chat.js.map +1 -1
  68. package/lib/module/utils/dateTimeUtils.js +10 -0
  69. package/lib/module/utils/dateTimeUtils.js.map +1 -1
  70. package/lib/module/utils/highlightSearch.js +6 -2
  71. package/lib/module/utils/highlightSearch.js.map +1 -1
  72. package/lib/module/utils/legendListMessage.js +86 -12
  73. package/lib/module/utils/legendListMessage.js.map +1 -1
  74. package/lib/module/utils/message.js +19 -1
  75. package/lib/module/utils/message.js.map +1 -1
  76. package/lib/module/utils/messageUtils.js +16 -2
  77. package/lib/module/utils/messageUtils.js.map +1 -1
  78. package/lib/typescript/src/components/messages/BubbleTail.d.ts +13 -0
  79. package/lib/typescript/src/components/messages/BubbleTail.d.ts.map +1 -0
  80. package/lib/typescript/src/components/messages/BubbleTailRow.d.ts +25 -0
  81. package/lib/typescript/src/components/messages/BubbleTailRow.d.ts.map +1 -0
  82. package/lib/typescript/src/components/messages/bubbleTail.utils.d.ts +11 -0
  83. package/lib/typescript/src/components/messages/bubbleTail.utils.d.ts.map +1 -0
  84. package/lib/typescript/src/components/messages/fileMessage/index.d.ts +1 -1
  85. package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/messages/imageMessage/index.d.ts +1 -1
  87. package/lib/typescript/src/components/messages/imageMessage/index.d.ts.map +1 -1
  88. package/lib/typescript/src/components/messages/linkMessage/index.d.ts +1 -1
  89. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  90. package/lib/typescript/src/components/messages/mergedMessage/index.d.ts +1 -1
  91. package/lib/typescript/src/components/messages/mergedMessage/index.d.ts.map +1 -1
  92. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
  93. package/lib/typescript/src/components/messages/revokedMessage/index.d.ts +1 -1
  94. package/lib/typescript/src/components/messages/revokedMessage/index.d.ts.map +1 -1
  95. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts +1 -1
  96. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts.map +1 -1
  97. package/lib/typescript/src/components/messages/textMessage/index.d.ts +1 -1
  98. package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
  99. package/lib/typescript/src/components/messages/types.d.ts +3 -0
  100. package/lib/typescript/src/components/messages/types.d.ts.map +1 -1
  101. package/lib/typescript/src/components/messages/videoMessage/index.d.ts +1 -1
  102. package/lib/typescript/src/components/messages/videoMessage/index.d.ts.map +1 -1
  103. package/lib/typescript/src/config/api-endpoints.d.ts +1 -0
  104. package/lib/typescript/src/config/api-endpoints.d.ts.map +1 -1
  105. package/lib/typescript/src/hooks/contacts/useSearchContacts.d.ts +221 -0
  106. package/lib/typescript/src/hooks/contacts/useSearchContacts.d.ts.map +1 -0
  107. package/lib/typescript/src/hooks/groups/useDismissGroup.d.ts.map +1 -1
  108. package/lib/typescript/src/hooks/query-keys.d.ts +4 -0
  109. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  110. package/lib/typescript/src/screens/SearchConversation/ContactResultItem.d.ts +9 -0
  111. package/lib/typescript/src/screens/SearchConversation/ContactResultItem.d.ts.map +1 -0
  112. package/lib/typescript/src/screens/SearchConversation/SearchAllResult.d.ts +3 -2
  113. package/lib/typescript/src/screens/SearchConversation/SearchAllResult.d.ts.map +1 -1
  114. package/lib/typescript/src/screens/SearchConversation/SearchCommunityResult.d.ts +7 -0
  115. package/lib/typescript/src/screens/SearchConversation/SearchCommunityResult.d.ts.map +1 -0
  116. package/lib/typescript/src/screens/SearchConversation/SearchContactsResult.d.ts +8 -0
  117. package/lib/typescript/src/screens/SearchConversation/SearchContactsResult.d.ts.map +1 -0
  118. package/lib/typescript/src/screens/SearchConversation/SearchEmpty.d.ts.map +1 -1
  119. package/lib/typescript/src/screens/SearchConversation/SearchMessagesResult.d.ts.map +1 -1
  120. package/lib/typescript/src/screens/SearchConversation/SearchResultItem.d.ts.map +1 -1
  121. package/lib/typescript/src/screens/SearchConversation/TabResult.d.ts +4 -2
  122. package/lib/typescript/src/screens/SearchConversation/TabResult.d.ts.map +1 -1
  123. package/lib/typescript/src/screens/SearchConversation/index.d.ts +4 -2
  124. package/lib/typescript/src/screens/SearchConversation/index.d.ts.map +1 -1
  125. package/lib/typescript/src/screens/SearchConversation/types.d.ts +2 -1
  126. package/lib/typescript/src/screens/SearchConversation/types.d.ts.map +1 -1
  127. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  128. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts +6 -1
  129. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts.map +1 -1
  130. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +14 -1
  131. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  132. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts.map +1 -1
  133. package/lib/typescript/src/services/endpoints.d.ts +1 -0
  134. package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
  135. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  136. package/lib/typescript/src/types/chat.d.ts +27 -0
  137. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  138. package/lib/typescript/src/utils/dateTimeUtils.d.ts +2 -0
  139. package/lib/typescript/src/utils/dateTimeUtils.d.ts.map +1 -1
  140. package/lib/typescript/src/utils/highlightSearch.d.ts.map +1 -1
  141. package/lib/typescript/src/utils/legendListMessage.d.ts +19 -4
  142. package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -1
  143. package/lib/typescript/src/utils/message.d.ts +17 -0
  144. package/lib/typescript/src/utils/message.d.ts.map +1 -1
  145. package/lib/typescript/src/utils/messageUtils.d.ts +1 -0
  146. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  147. package/package.json +3 -1
  148. package/src/build-ignore.d.ts +2 -0
  149. package/src/components/messages/BubbleTail.tsx +35 -0
  150. package/src/components/messages/BubbleTailRow.tsx +64 -0
  151. package/src/components/messages/bubbleTail.utils.ts +21 -0
  152. package/src/components/messages/fileMessage/index.tsx +90 -83
  153. package/src/components/messages/imageMessage/index.tsx +26 -23
  154. package/src/components/messages/linkMessage/index.tsx +6 -18
  155. package/src/components/messages/mergedMessage/index.tsx +27 -15
  156. package/src/components/messages/quotedMessage/index.tsx +7 -19
  157. package/src/components/messages/revokedMessage/index.tsx +23 -14
  158. package/src/components/messages/stickerMessage/index.tsx +16 -11
  159. package/src/components/messages/textMessage/index.tsx +6 -22
  160. package/src/components/messages/types.ts +3 -0
  161. package/src/components/messages/videoMessage/index.tsx +13 -10
  162. package/src/config/api-endpoints.ts +2 -0
  163. package/src/hooks/contacts/useSearchContacts.ts +66 -0
  164. package/src/hooks/groups/useDismissGroup.tsx +1 -0
  165. package/src/hooks/query-keys.ts +6 -0
  166. package/src/screens/SearchConversation/ContactResultItem.tsx +128 -0
  167. package/src/screens/SearchConversation/SearchAllResult.tsx +38 -24
  168. package/src/screens/SearchConversation/SearchCommunityResult.tsx +96 -0
  169. package/src/screens/SearchConversation/SearchContactsResult.tsx +155 -0
  170. package/src/screens/SearchConversation/SearchEmpty.tsx +2 -0
  171. package/src/screens/SearchConversation/SearchMessagesResult.tsx +1 -6
  172. package/src/screens/SearchConversation/SearchResultItem.tsx +6 -1
  173. package/src/screens/SearchConversation/TabResult.tsx +40 -5
  174. package/src/screens/SearchConversation/index.tsx +10 -2
  175. package/src/screens/SearchConversation/types.tsx +1 -0
  176. package/src/screens/chat-detail/ChatListLegend.tsx +97 -28
  177. package/src/screens/chat-detail/legend/LegendChatDay.tsx +25 -15
  178. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +62 -12
  179. package/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx +22 -4
  180. package/src/services/endpoints.ts +1 -0
  181. package/src/translation/resources/i18n.ts +6 -0
  182. package/src/types/chat.ts +30 -0
  183. package/src/utils/dateTimeUtils.ts +15 -0
  184. package/src/utils/highlightSearch.ts +12 -2
  185. package/src/utils/legendListMessage.ts +123 -14
  186. package/src/utils/message.ts +19 -1
  187. package/src/utils/messageUtils.ts +23 -9
package/src/types/chat.ts CHANGED
@@ -341,3 +341,33 @@ export interface ISearchMessageItem {
341
341
  chatLog: MessageItem;
342
342
  conversation: ConversationItem;
343
343
  }
344
+
345
+ export enum ContactCategory {
346
+ Customer = 'CUSTOMER',
347
+ Pdp = 'PDP_BOT',
348
+ Group = 'GROUP',
349
+ Seller = 'USER',
350
+ }
351
+
352
+ export type IContactSearchRequest = {
353
+ applicationType: DChatApplicationType;
354
+ searchTerm: string;
355
+ page: number;
356
+ pageSize: number;
357
+ type?: ContactCategory;
358
+ };
359
+
360
+ export interface IContactSearchItem {
361
+ id: string;
362
+ type: ContactCategory;
363
+ targetUserId?: string;
364
+ targetCustomerId?: string;
365
+ targetGroupId?: string;
366
+ targetBotId?: string;
367
+ targetName: string;
368
+ targetAvatar?: string;
369
+ applicationType: DChatApplicationType;
370
+ lastOnlineTime?: string;
371
+ memberCount?: number;
372
+ description?: string;
373
+ }
@@ -2,6 +2,12 @@
2
2
  * Date and time formatting utilities
3
3
  */
4
4
 
5
+ import dayjs from 'dayjs';
6
+ import relativeTime from 'dayjs/plugin/relativeTime';
7
+ import 'dayjs/locale/vi';
8
+
9
+ dayjs.extend(relativeTime);
10
+
5
11
  export const formatDuration = (durationMs: number): string => {
6
12
  // Convert milliseconds to seconds
7
13
  const totalSeconds = Math.floor(durationMs / 1000);
@@ -46,3 +52,12 @@ export const formatTimestamp = (ts: number | string | null): string => {
46
52
  return '';
47
53
  }
48
54
  };
55
+
56
+ export const formatLastOnlineTime = (ts: number | string | null): string => {
57
+ if (!ts) return '';
58
+
59
+ const date = dayjs(ts).locale('vi');
60
+ if (!date.isValid()) return '';
61
+
62
+ return date.fromNow();
63
+ };
@@ -4,6 +4,16 @@ export interface HighlightResult {
4
4
  after: string;
5
5
  }
6
6
 
7
+ // Strips Vietnamese diacritics one base-character-at-a-time (NFD decomposition +
8
+ // combining-mark removal, plus đ/Đ which NFD doesn't decompose) so the result stays
9
+ // the same length and index-aligned with the input
10
+ const stripDiacritics = (input: string): string =>
11
+ input
12
+ .normalize('NFD')
13
+ .replace(/[\u0300-\u036f]/g, '')
14
+ .replace(/đ/g, 'd')
15
+ .replace(/Đ/g, 'D');
16
+
7
17
  export const highlightSearch = (
8
18
  text: string,
9
19
  keyword: string,
@@ -11,8 +21,8 @@ export const highlightSearch = (
11
21
  ): HighlightResult | null => {
12
22
  if (!keyword) return null;
13
23
 
14
- const lowerText = text.toLowerCase();
15
- const lowerKeyword = keyword.toLowerCase();
24
+ const lowerText = stripDiacritics(text.toLowerCase());
25
+ const lowerKeyword = stripDiacritics(keyword.toLowerCase());
16
26
  const index = lowerText.indexOf(lowerKeyword);
17
27
 
18
28
  if (index === -1) return null;
@@ -1,4 +1,6 @@
1
1
  import { MessageType, type MessageItem } from '@droppii/openim-rn-client-sdk';
2
+ import { GROUP_NOTIFICATION_TYPES } from './messageUtils';
3
+ import { getMessageRenderId, getMessageTime } from './message';
2
4
 
3
5
  /**
4
6
  * Extract message text based on content type
@@ -39,41 +41,148 @@ export const getMessageText = (message: MessageItem): string => {
39
41
  }
40
42
  };
41
43
 
44
+ /** Where a message sits inside its run of consecutive same-sender messages. */
45
+ export type GroupPosition = 'first' | 'middle' | 'last' | 'solo';
46
+
42
47
  export interface PrecomputedMessageData {
43
48
  messageId: string;
44
49
  dayStart: boolean;
50
+ /**
51
+ * More than GROUP_TIME_GAP has passed since the previous message. The list
52
+ * marks these with a time label: the cluster break is real but, unlike a day
53
+ * change, nothing else on screen would show why the run stopped.
54
+ */
55
+ timeBreak: boolean;
45
56
  createdAt: number;
57
+ groupPosition: GroupPosition;
58
+ }
59
+
60
+ /** Two messages further apart than this never share a cluster. */
61
+ const GROUP_TIME_GAP = 300_000; // 5 minutes
62
+
63
+ /** Virtual sender for bot conversations — see getGroupKey. */
64
+ const BOT_GROUP_KEY = '__bot__';
65
+
66
+ const startOfDay = (timestamp: number): number => {
67
+ const date = new Date(timestamp);
68
+ date.setHours(0, 0, 0, 0);
69
+ return date.getTime();
70
+ };
71
+
72
+ /**
73
+ * System banners (group created/renamed/member changes) and anything without a
74
+ * sender are not user-authored, so they never join a cluster and always break
75
+ * the runs on both sides of them.
76
+ */
77
+ const isSystemMessage = (message: MessageItem): boolean =>
78
+ GROUP_NOTIFICATION_TYPES.includes(message.contentType) || !message.sendID;
79
+
80
+ /**
81
+ * Identity two messages must share to cluster together. In a bot conversation
82
+ * every incoming message is treated as one virtual sender: replies come from
83
+ * the bot and from human agents under different sendIDs, but read as a single
84
+ * continuous voice.
85
+ */
86
+ const getGroupKey = (
87
+ message: MessageItem,
88
+ isBotConversation: boolean,
89
+ currentUserId?: string
90
+ ): string => {
91
+ const isOutgoing = message.sendID === currentUserId;
92
+ if (isBotConversation && !isOutgoing) return BOT_GROUP_KEY;
93
+ return message.sendID;
94
+ };
95
+
96
+ export interface PrecomputeOptions {
97
+ isBotConversation?: boolean;
98
+ currentUserId?: string;
46
99
  }
47
100
 
48
101
  /**
49
- * Precompute all message rendering data in single pass O(n)
50
- * Eliminates redundant Date creation and type resolution during render
102
+ * Precompute per-message render data (day dividers + cluster position) so
103
+ * renderItem stays allocation-free.
51
104
  *
52
- * Returns: Map of messageId -> {dayStart, messageType, createdAt}
105
+ * Runs in two passes: day starts first, because cluster boundaries depend on
106
+ * them — a divider between two messages separates them however close in time
107
+ * they are.
53
108
  */
54
109
  export const precomputeLegendListData = (
55
- messages: MessageItem[]
110
+ messages: MessageItem[],
111
+ { isBotConversation = false, currentUserId }: PrecomputeOptions = {}
56
112
  ): Map<string, PrecomputedMessageData> => {
57
113
  const dataMap = new Map<string, PrecomputedMessageData>();
114
+
115
+ // Pass 1 — identity, timestamp, day divider.
116
+ const dayStarts: boolean[] = [];
117
+ const ids: string[] = [];
118
+ // Resolved timestamps, reused by pass 2. Shared with the list's sort key so
119
+ // cluster boundaries are computed over the same ordering that gets rendered.
120
+ const timestamps: number[] = [];
58
121
  let lastDay: number | null = null;
59
122
 
60
123
  messages.forEach((message, index) => {
61
- // Get message ID - use timestamp as fallback for newly sent messages
62
- const messageId =
63
- message.clientMsgID ||
64
- message.serverMsgID ||
65
- `temp-${message.sendTime || message.createTime}-${index}`;
124
+ const messageId = getMessageRenderId(message, index);
125
+
126
+ // A message still in flight can reach us with no usable timestamp at all.
127
+ // Left as 0 it lands on the epoch, which reads as a different calendar day
128
+ // from its neighbours — that inserts a bogus divider AND splits the cluster,
129
+ // stranding a tail on the message before it. Anchor it to the previous
130
+ // message instead, which is where it belongs in both senses.
131
+ const rawCreatedAt = getMessageTime(message);
132
+ const createdAt =
133
+ rawCreatedAt > 0 ? rawCreatedAt : (timestamps[index - 1] ?? rawCreatedAt);
134
+ const day = startOfDay(createdAt);
66
135
 
67
- const createdAt = message.sendTime || message.createTime;
68
- const day = new Date(createdAt).getDate();
136
+ const prevTimestamp = timestamps[index - 1];
137
+
138
+ ids[index] = messageId;
139
+ timestamps[index] = createdAt;
140
+ dayStarts[index] = lastDay !== day;
141
+ lastDay = day;
69
142
 
70
143
  dataMap.set(messageId, {
71
144
  messageId,
72
- dayStart: lastDay !== day,
145
+ dayStart: dayStarts[index]!,
146
+ timeBreak:
147
+ prevTimestamp !== undefined &&
148
+ createdAt - prevTimestamp > GROUP_TIME_GAP,
73
149
  createdAt,
150
+ groupPosition: 'solo',
74
151
  });
152
+ });
75
153
 
76
- lastDay = day;
154
+ // Pass 2 — cluster position, now that every day divider is known.
155
+ messages.forEach((message, index) => {
156
+ const entry = dataMap.get(ids[index]!);
157
+ if (!entry) return;
158
+ if (isSystemMessage(message)) return; // stays 'solo'
159
+
160
+ const key = getGroupKey(message, isBotConversation, currentUserId);
161
+ const prev = messages[index - 1];
162
+ const next = messages[index + 1];
163
+
164
+ const joinsPrev =
165
+ !!prev &&
166
+ !isSystemMessage(prev) &&
167
+ getGroupKey(prev, isBotConversation, currentUserId) === key &&
168
+ timestamps[index]! - timestamps[index - 1]! <= GROUP_TIME_GAP &&
169
+ // A divider above THIS message separates it from the one before it.
170
+ !dayStarts[index];
171
+
172
+ const joinsNext =
173
+ !!next &&
174
+ !isSystemMessage(next) &&
175
+ getGroupKey(next, isBotConversation, currentUserId) === key &&
176
+ timestamps[index + 1]! - timestamps[index]! <= GROUP_TIME_GAP &&
177
+ !dayStarts[index + 1];
178
+
179
+ entry.groupPosition = joinsPrev
180
+ ? joinsNext
181
+ ? 'middle'
182
+ : 'last'
183
+ : joinsNext
184
+ ? 'first'
185
+ : 'solo';
77
186
  });
78
187
 
79
188
  return dataMap;
@@ -90,7 +199,7 @@ export const computeDayStarts = (
90
199
  let lastDay: number | null = null;
91
200
 
92
201
  messages.forEach((message) => {
93
- const day = new Date(message.sendTime || message.createTime).getDate();
202
+ const day = startOfDay(getMessageTime(message));
94
203
  const messageId = message.clientMsgID || message.serverMsgID || '';
95
204
  dayStartMap.set(messageId, lastDay !== day);
96
205
  lastDay = day;
@@ -6,12 +6,30 @@ import {
6
6
  } from '@droppii/openim-rn-client-sdk';
7
7
  import { getConversationID } from './conversation';
8
8
 
9
- const getMessageTime = (message: MessageItem) =>
9
+ /**
10
+ * Canonical timestamp for a message. `sendTime` is 0 until the SDK stamps an
11
+ * outgoing message, so `createTime` covers the in-flight window. This is the
12
+ * key the list is sorted by — anything reasoning about message order must use
13
+ * it too, or it will disagree with the order actually rendered.
14
+ */
15
+ export const getMessageTime = (message: MessageItem) =>
10
16
  message.sendTime || message.createTime || 0;
11
17
 
12
18
  const getMessageKey = (message: MessageItem) =>
13
19
  message.clientMsgID || message.serverMsgID;
14
20
 
21
+ /**
22
+ * Stable id for a message inside a rendered list.
23
+ *
24
+ * A message that has not round-tripped through the server yet may carry
25
+ * neither id, so position and timestamp stand in. Every consumer that looks a
26
+ * message up by id must derive it the same way — the precompute map is keyed
27
+ * on this, so a divergent formula silently misses and the message renders
28
+ * without its day divider or cluster position.
29
+ */
30
+ export const getMessageRenderId = (message: MessageItem, index: number) =>
31
+ getMessageKey(message) || `temp-${getMessageTime(message)}-${index}`;
32
+
15
33
  export const mergeMessages = (
16
34
  current: MessageItem[],
17
35
  incoming: MessageItem[]
@@ -92,6 +92,14 @@ export const GROUP_NOTIFICATION_TEXT: Partial<
92
92
  },
93
93
  };
94
94
 
95
+ // The content types above, as a plain list. Both the chat list (to decide
96
+ // whether a message renders as a system banner) and the grouping precompute
97
+ // (system messages never join a sender cluster) need this, so it is derived
98
+ // once here rather than in each consumer.
99
+ export const GROUP_NOTIFICATION_TYPES = Object.keys(
100
+ GROUP_NOTIFICATION_TEXT
101
+ ).map(Number) as MessageType[];
102
+
95
103
  const parseNotificationDetail = (message: MessageItem): any => {
96
104
  if (!message.notificationElem?.detail) return undefined;
97
105
  try {
@@ -434,15 +442,21 @@ export const getSenderName = (
434
442
  currentUserId?: string,
435
443
  conversation?: ConversationItem
436
444
  ): string => {
437
- const sender =
438
- currentUserId && message.sendID === currentUserId
439
- ? 'Bạn'
440
- : message?.senderNickname ||
441
- getNotificationOpUserNickname(message) ||
442
- (conversation?.peerType === PeerType.Bot
443
- ? conversation?.showName || ''
444
- : undefined);
445
- return sender || '';
445
+ const isCurrentUser = currentUserId && message.sendID === currentUserId;
446
+
447
+ if (isCurrentUser) return 'Bạn';
448
+
449
+ switch (conversation?.peerType) {
450
+ case PeerType.Bot:
451
+ case PeerType.Customer:
452
+ case PeerType.User:
453
+ return '';
454
+
455
+ default:
456
+ return (
457
+ message?.senderNickname || getNotificationOpUserNickname(message) || ''
458
+ );
459
+ }
446
460
  };
447
461
 
448
462
  export interface GetLastMessageTextParams {