@droppii-org/chat-mobile 0.2.78 → 0.2.80

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 (140) 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/core/useChatListener.js +11 -0
  27. package/lib/module/core/useChatListener.js.map +1 -1
  28. package/lib/module/hooks/groups/useDismissGroup.js +2 -1
  29. package/lib/module/hooks/groups/useDismissGroup.js.map +1 -1
  30. package/lib/module/hooks/groups/useGroupAccessGuard.js +145 -0
  31. package/lib/module/hooks/groups/useGroupAccessGuard.js.map +1 -0
  32. package/lib/module/screens/ChatInfo/ChatInfoDescriptionRow.js +4 -1
  33. package/lib/module/screens/ChatInfo/ChatInfoDescriptionRow.js.map +1 -1
  34. package/lib/module/screens/chat-detail/ChatDetail.js +12 -10
  35. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  36. package/lib/module/screens/chat-detail/ChatDetailHeader.js +3 -9
  37. package/lib/module/screens/chat-detail/ChatDetailHeader.js.map +1 -1
  38. package/lib/module/screens/chat-detail/ChatListLegend.js +71 -21
  39. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  40. package/lib/module/screens/chat-detail/legend/LegendChatDay.js +8 -2
  41. package/lib/module/screens/chat-detail/legend/LegendChatDay.js.map +1 -1
  42. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +38 -13
  43. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  44. package/lib/module/screens/inbox/CommunityTab.js +5 -2
  45. package/lib/module/screens/inbox/CommunityTab.js.map +1 -1
  46. package/lib/module/screens/inbox/MessagesTab.js +2 -2
  47. package/lib/module/screens/inbox/MessagesTab.js.map +1 -1
  48. package/lib/module/store/conversation.js +18 -20
  49. package/lib/module/store/conversation.js.map +1 -1
  50. package/lib/module/translation/resources/i18n.js +4 -1
  51. package/lib/module/translation/resources/i18n.js.map +1 -1
  52. package/lib/module/utils/conversation.js +4 -1
  53. package/lib/module/utils/conversation.js.map +1 -1
  54. package/lib/module/utils/legendListMessage.js +86 -12
  55. package/lib/module/utils/legendListMessage.js.map +1 -1
  56. package/lib/module/utils/message.js +19 -1
  57. package/lib/module/utils/message.js.map +1 -1
  58. package/lib/module/utils/messageUtils.js +7 -1
  59. package/lib/module/utils/messageUtils.js.map +1 -1
  60. package/lib/typescript/src/components/messages/BubbleTail.d.ts +13 -0
  61. package/lib/typescript/src/components/messages/BubbleTail.d.ts.map +1 -0
  62. package/lib/typescript/src/components/messages/BubbleTailRow.d.ts +25 -0
  63. package/lib/typescript/src/components/messages/BubbleTailRow.d.ts.map +1 -0
  64. package/lib/typescript/src/components/messages/bubbleTail.utils.d.ts +11 -0
  65. package/lib/typescript/src/components/messages/bubbleTail.utils.d.ts.map +1 -0
  66. package/lib/typescript/src/components/messages/fileMessage/index.d.ts +1 -1
  67. package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
  68. package/lib/typescript/src/components/messages/imageMessage/index.d.ts +1 -1
  69. package/lib/typescript/src/components/messages/imageMessage/index.d.ts.map +1 -1
  70. package/lib/typescript/src/components/messages/linkMessage/index.d.ts +1 -1
  71. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  72. package/lib/typescript/src/components/messages/mergedMessage/index.d.ts +1 -1
  73. package/lib/typescript/src/components/messages/mergedMessage/index.d.ts.map +1 -1
  74. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
  75. package/lib/typescript/src/components/messages/revokedMessage/index.d.ts +1 -1
  76. package/lib/typescript/src/components/messages/revokedMessage/index.d.ts.map +1 -1
  77. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts +1 -1
  78. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts.map +1 -1
  79. package/lib/typescript/src/components/messages/textMessage/index.d.ts +1 -1
  80. package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
  81. package/lib/typescript/src/components/messages/types.d.ts +3 -0
  82. package/lib/typescript/src/components/messages/types.d.ts.map +1 -1
  83. package/lib/typescript/src/components/messages/videoMessage/index.d.ts +1 -1
  84. package/lib/typescript/src/components/messages/videoMessage/index.d.ts.map +1 -1
  85. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  86. package/lib/typescript/src/hooks/groups/useDismissGroup.d.ts.map +1 -1
  87. package/lib/typescript/src/hooks/groups/useGroupAccessGuard.d.ts +10 -0
  88. package/lib/typescript/src/hooks/groups/useGroupAccessGuard.d.ts.map +1 -0
  89. package/lib/typescript/src/screens/ChatInfo/ChatInfoDescriptionRow.d.ts.map +1 -1
  90. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  91. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts +1 -1
  92. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
  93. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  94. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts +6 -1
  95. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts.map +1 -1
  96. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +14 -1
  97. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  98. package/lib/typescript/src/screens/chat-detail/types.d.ts +0 -1
  99. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  100. package/lib/typescript/src/store/conversation.d.ts.map +1 -1
  101. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  102. package/lib/typescript/src/utils/conversation.d.ts.map +1 -1
  103. package/lib/typescript/src/utils/legendListMessage.d.ts +19 -4
  104. package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -1
  105. package/lib/typescript/src/utils/message.d.ts +17 -0
  106. package/lib/typescript/src/utils/message.d.ts.map +1 -1
  107. package/lib/typescript/src/utils/messageUtils.d.ts +1 -0
  108. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  109. package/package.json +1 -1
  110. package/src/components/messages/BubbleTail.tsx +35 -0
  111. package/src/components/messages/BubbleTailRow.tsx +64 -0
  112. package/src/components/messages/bubbleTail.utils.ts +21 -0
  113. package/src/components/messages/fileMessage/index.tsx +90 -83
  114. package/src/components/messages/imageMessage/index.tsx +26 -23
  115. package/src/components/messages/linkMessage/index.tsx +6 -18
  116. package/src/components/messages/mergedMessage/index.tsx +27 -15
  117. package/src/components/messages/quotedMessage/index.tsx +7 -19
  118. package/src/components/messages/revokedMessage/index.tsx +23 -14
  119. package/src/components/messages/stickerMessage/index.tsx +16 -11
  120. package/src/components/messages/textMessage/index.tsx +6 -22
  121. package/src/components/messages/types.ts +3 -0
  122. package/src/components/messages/videoMessage/index.tsx +13 -10
  123. package/src/core/useChatListener.ts +14 -0
  124. package/src/hooks/groups/useDismissGroup.tsx +1 -0
  125. package/src/hooks/groups/useGroupAccessGuard.ts +182 -0
  126. package/src/screens/ChatInfo/ChatInfoDescriptionRow.tsx +5 -1
  127. package/src/screens/chat-detail/ChatDetail.tsx +11 -13
  128. package/src/screens/chat-detail/ChatDetailHeader.tsx +2 -6
  129. package/src/screens/chat-detail/ChatListLegend.tsx +97 -28
  130. package/src/screens/chat-detail/legend/LegendChatDay.tsx +25 -15
  131. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +62 -12
  132. package/src/screens/chat-detail/types.ts +0 -1
  133. package/src/screens/inbox/CommunityTab.tsx +2 -2
  134. package/src/screens/inbox/MessagesTab.tsx +1 -1
  135. package/src/store/conversation.ts +14 -18
  136. package/src/translation/resources/i18n.ts +4 -0
  137. package/src/utils/conversation.ts +6 -3
  138. package/src/utils/legendListMessage.ts +123 -14
  139. package/src/utils/message.ts +19 -1
  140. package/src/utils/messageUtils.ts +9 -1
@@ -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 {
@@ -232,7 +240,7 @@ export const getMessagePreview = (message?: MessageItem): string => {
232
240
  case MessageType.FileMessage: {
233
241
  const fileName = message?.fileElem?.fileName;
234
242
  if (fileName) {
235
- return `[File] ${fileName}`;
243
+ return `[File] ${truncateMiddle(fileName, 20)}`;
236
244
  }
237
245
  return '[File]';
238
246
  }