@droppii-org/chat-sdk 0.8.1 → 0.8.3

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 (191) hide show
  1. package/dist/components/canned-response/cannedTrigger.d.ts +2 -0
  2. package/dist/components/canned-response/cannedTrigger.d.ts.map +1 -0
  3. package/dist/components/canned-response/cannedTrigger.js +3 -0
  4. package/dist/components/canned-response/insertCannedContent.js +2 -2
  5. package/dist/components/conversation/ConversationByGroupItem.js +1 -1
  6. package/dist/components/conversation/ConversationBySessionItem.js +4 -4
  7. package/dist/components/conversation/DeskConversationList.d.ts.map +1 -1
  8. package/dist/components/conversation/DeskConversationList.js +6 -4
  9. package/dist/components/conversation/folderEmptyState.d.ts.map +1 -1
  10. package/dist/components/conversation/folderEmptyState.js +2 -0
  11. package/dist/components/conversation/internalNotePreview.d.ts +1 -1
  12. package/dist/components/conversation/internalNotePreview.d.ts.map +1 -1
  13. package/dist/components/conversation/internalNotePreview.js +22 -3
  14. package/dist/components/conversation/listHeading.d.ts +1 -1
  15. package/dist/components/conversation/listHeading.d.ts.map +1 -1
  16. package/dist/components/conversation/listHeading.js +7 -1
  17. package/dist/components/conversation/rowTime.d.ts +2 -2
  18. package/dist/components/conversation/rowTime.d.ts.map +1 -1
  19. package/dist/components/conversation/rowTime.js +2 -4
  20. package/dist/components/conversation-detail/ConversationDetailInput.d.ts.map +1 -1
  21. package/dist/components/conversation-detail/ConversationDetailInput.js +8 -3
  22. package/dist/components/conversation-detail/ConversationMessageList.d.ts.map +1 -1
  23. package/dist/components/conversation-detail/ConversationMessageList.js +6 -3
  24. package/dist/components/conversation-detail/bot/BotMenuSheet.d.ts.map +1 -1
  25. package/dist/components/conversation-detail/bot/BotMenuSheet.js +1 -1
  26. package/dist/components/conversation-inbox/ConversationInboxList.d.ts.map +1 -1
  27. package/dist/components/conversation-inbox/ConversationInboxList.js +4 -0
  28. package/dist/components/conversation-inbox/InboxBootStatus.d.ts.map +1 -1
  29. package/dist/components/conversation-inbox/InboxBootStatus.js +10 -2
  30. package/dist/components/group/CreateGroupForm.d.ts.map +1 -1
  31. package/dist/components/group/CreateGroupForm.js +2 -1
  32. package/dist/components/group/CreateGroupFormCrmView.d.ts.map +1 -1
  33. package/dist/components/group/CreateGroupFormCrmView.js +1 -1
  34. package/dist/components/group/GroupMemberPickerCrm.d.ts.map +1 -1
  35. package/dist/components/group/GroupMemberPickerCrm.js +1 -1
  36. package/dist/components/media-collection/FileCollection.d.ts.map +1 -1
  37. package/dist/components/media-collection/FileCollection.js +4 -3
  38. package/dist/components/media-collection/ImageCollection.d.ts.map +1 -1
  39. package/dist/components/media-collection/ImageCollection.js +2 -1
  40. package/dist/components/media-collection/LinkCollection.d.ts.map +1 -1
  41. package/dist/components/media-collection/LinkCollection.js +14 -26
  42. package/dist/components/media-collection/VideoCollection.d.ts.map +1 -1
  43. package/dist/components/media-collection/VideoCollection.js +2 -1
  44. package/dist/components/media-resources/FileResourceList.js +2 -2
  45. package/dist/components/media-resources/ResourceScrollContainer.d.ts.map +1 -1
  46. package/dist/components/media-resources/ResourceScrollContainer.js +34 -2
  47. package/dist/components/message/GroupMembersDrawer.d.ts.map +1 -1
  48. package/dist/components/message/GroupMembersDrawer.js +2 -3
  49. package/dist/components/message/MessageList.d.ts.map +1 -1
  50. package/dist/components/message/MessageList.js +15 -4
  51. package/dist/components/message/footer/ActionBar.d.ts.map +1 -1
  52. package/dist/components/message/footer/ActionBar.js +10 -44
  53. package/dist/components/message/footer/CannedResponsePlugin.d.ts.map +1 -1
  54. package/dist/components/message/footer/CannedResponsePlugin.js +3 -2
  55. package/dist/components/message/footer/EnterHandler.d.ts.map +1 -1
  56. package/dist/components/message/footer/EnterHandler.js +10 -47
  57. package/dist/components/message/footer/FilePreview.d.ts.map +1 -1
  58. package/dist/components/message/footer/FilePreview.js +2 -1
  59. package/dist/components/message/footer/LinkInputPreview.d.ts +1 -1
  60. package/dist/components/message/footer/LinkInputPreview.d.ts.map +1 -1
  61. package/dist/components/message/footer/LinkInputPreview.js +8 -1
  62. package/dist/components/message/footer/MediaUploadButtons.d.ts.map +1 -1
  63. package/dist/components/message/footer/MediaUploadButtons.js +4 -7
  64. package/dist/components/message/footer/index.d.ts.map +1 -1
  65. package/dist/components/message/footer/index.js +57 -16
  66. package/dist/components/message/footer/mention/useMentionOptions.d.ts.map +1 -1
  67. package/dist/components/message/footer/mention/useMentionOptions.js +2 -6
  68. package/dist/components/message/footer/resetEditorAfterSend.d.ts +7 -0
  69. package/dist/components/message/footer/resetEditorAfterSend.d.ts.map +1 -0
  70. package/dist/components/message/footer/resetEditorAfterSend.js +62 -0
  71. package/dist/components/message/groupMemberMenuCrm.d.ts +4 -4
  72. package/dist/components/message/groupMemberMenuCrm.d.ts.map +1 -1
  73. package/dist/components/message/groupMemberMenuCrm.js +73 -60
  74. package/dist/components/message/item/FileMessage.js +2 -2
  75. package/dist/components/message/item/SystemLogMessage.d.ts.map +1 -1
  76. package/dist/components/message/item/SystemLogMessage.js +5 -2
  77. package/dist/components/search-conversation/SearchConversationAsGroups.js +1 -1
  78. package/dist/components/search-conversation/SearchConversationAsMessages.js +1 -1
  79. package/dist/components/search-conversation/SearchConversationMyInbox.js +1 -1
  80. package/dist/components/session/SessionFolderList.d.ts +4 -3
  81. package/dist/components/session/SessionFolderList.d.ts.map +1 -1
  82. package/dist/components/session/SessionFolderList.js +14 -5
  83. package/dist/components/thread/GroupInfoBodyCrm.d.ts.map +1 -1
  84. package/dist/components/thread/GroupInfoBodyCrm.js +2 -2
  85. package/dist/components/thread/GroupThreadInfo.d.ts.map +1 -1
  86. package/dist/components/thread/GroupThreadInfo.js +2 -6
  87. package/dist/components/thread/ManualAssignPopover.d.ts.map +1 -1
  88. package/dist/components/thread/ManualAssignPopover.js +6 -8
  89. package/dist/components/thread/SessionDetailCard.d.ts.map +1 -1
  90. package/dist/components/thread/SessionDetailCard.js +3 -3
  91. package/dist/components/thread/SessionSection.d.ts.map +1 -1
  92. package/dist/components/thread/SessionSection.js +3 -9
  93. package/dist/components/thread/SessionTagPicker.d.ts +3 -1
  94. package/dist/components/thread/SessionTagPicker.d.ts.map +1 -1
  95. package/dist/components/thread/SessionTagPicker.js +2 -2
  96. package/dist/components/thread/sessionDateFormat.d.ts +2 -0
  97. package/dist/components/thread/sessionDateFormat.d.ts.map +1 -1
  98. package/dist/components/thread/sessionDateFormat.js +6 -0
  99. package/dist/constants/dateFormat.d.ts +2 -0
  100. package/dist/constants/dateFormat.d.ts.map +1 -0
  101. package/dist/constants/dateFormat.js +2 -0
  102. package/dist/hooks/common/useFetchUrlMetadata.d.ts +1 -0
  103. package/dist/hooks/common/useFetchUrlMetadata.d.ts.map +1 -1
  104. package/dist/hooks/conversation/useBotFlow.d.ts.map +1 -1
  105. package/dist/hooks/conversation/useBotFlow.js +64 -12
  106. package/dist/hooks/conversation/useCloseBotSession.d.ts +2 -0
  107. package/dist/hooks/conversation/useCloseBotSession.d.ts.map +1 -1
  108. package/dist/hooks/conversation/useCloseBotSession.js +17 -1
  109. package/dist/hooks/global/useGlobalEvent.d.ts.map +1 -1
  110. package/dist/hooks/global/useGlobalEvent.js +148 -16
  111. package/dist/hooks/global/useLoginHealth.d.ts +3 -0
  112. package/dist/hooks/global/useLoginHealth.d.ts.map +1 -0
  113. package/dist/hooks/global/useLoginHealth.js +55 -0
  114. package/dist/hooks/group/useCreateGroup.d.ts.map +1 -1
  115. package/dist/hooks/group/useCreateGroup.js +75 -6
  116. package/dist/hooks/init/useChatBootPhase.d.ts +2 -0
  117. package/dist/hooks/init/useChatBootPhase.d.ts.map +1 -1
  118. package/dist/hooks/init/useChatBootPhase.js +11 -6
  119. package/dist/hooks/message/useMessage.d.ts.map +1 -1
  120. package/dist/hooks/message/useMessage.js +97 -28
  121. package/dist/hooks/message/usePinnedMessages.d.ts.map +1 -1
  122. package/dist/hooks/message/usePinnedMessages.js +13 -2
  123. package/dist/hooks/message/useRevokeMessage.d.ts.map +1 -1
  124. package/dist/hooks/message/useRevokeMessage.js +25 -0
  125. package/dist/hooks/message/useSendMessage.d.ts +15 -12
  126. package/dist/hooks/message/useSendMessage.d.ts.map +1 -1
  127. package/dist/hooks/message/useSendMessage.js +199 -133
  128. package/dist/hooks/search/useSearchMessage.d.ts.map +1 -1
  129. package/dist/hooks/search/useSearchMessage.js +57 -3
  130. package/dist/hooks/session/useConversationSessionState.d.ts.map +1 -1
  131. package/dist/hooks/session/useConversationSessionState.js +4 -1
  132. package/dist/hooks/session/useGetSession.d.ts.map +1 -1
  133. package/dist/hooks/session/useGetSession.js +2 -0
  134. package/dist/hooks/session/useGetSessionList.d.ts +235 -0
  135. package/dist/hooks/session/useGetSessionList.d.ts.map +1 -0
  136. package/dist/hooks/session/useGetSessionList.js +23 -0
  137. package/dist/hooks/session/useSessionLatestPreview.d.ts.map +1 -1
  138. package/dist/hooks/session/useSessionLatestPreview.js +34 -4
  139. package/dist/hooks/user/useAuth.d.ts +1 -1
  140. package/dist/hooks/user/useAuth.d.ts.map +1 -1
  141. package/dist/hooks/user/useAuth.js +31 -15
  142. package/dist/index.d.ts +1 -0
  143. package/dist/index.d.ts.map +1 -1
  144. package/dist/index.js +1 -0
  145. package/dist/locales/vi/boot.json +5 -1
  146. package/dist/locales/vi/common.json +35 -15
  147. package/dist/locales/vi/conversation-inbox.json +2 -1
  148. package/dist/screens/conversation-detail/index.d.ts.map +1 -1
  149. package/dist/screens/conversation-detail/index.js +6 -0
  150. package/dist/screens/desk-message/DeskBootCard.d.ts.map +1 -1
  151. package/dist/screens/desk-message/DeskBootCard.js +6 -4
  152. package/dist/store/boot.d.ts +5 -0
  153. package/dist/store/boot.d.ts.map +1 -1
  154. package/dist/store/boot.js +11 -1
  155. package/dist/store/conversation.d.ts.map +1 -1
  156. package/dist/store/conversation.js +5 -1
  157. package/dist/store/session.d.ts +1 -13
  158. package/dist/store/session.d.ts.map +1 -1
  159. package/dist/store/session.js +10 -9
  160. package/dist/store/sessionFolder.d.ts +2 -0
  161. package/dist/store/sessionFolder.d.ts.map +1 -1
  162. package/dist/store/sessionFolder.js +3 -1
  163. package/dist/store/type.d.ts +7 -1
  164. package/dist/store/type.d.ts.map +1 -1
  165. package/dist/styles/global.css +1 -1
  166. package/dist/types/chat.d.ts +1 -1
  167. package/dist/types/chat.d.ts.map +1 -1
  168. package/dist/utils/common.d.ts.map +1 -1
  169. package/dist/utils/common.js +4 -1
  170. package/dist/utils/download.d.ts +10 -0
  171. package/dist/utils/download.d.ts.map +1 -1
  172. package/dist/utils/download.js +51 -0
  173. package/dist/utils/fileValidation.d.ts +7 -0
  174. package/dist/utils/fileValidation.d.ts.map +1 -1
  175. package/dist/utils/fileValidation.js +37 -13
  176. package/dist/utils/latestMessagePreviewable.d.ts +2 -0
  177. package/dist/utils/latestMessagePreviewable.d.ts.map +1 -0
  178. package/dist/utils/latestMessagePreviewable.js +18 -0
  179. package/dist/utils/mentionMembers.d.ts +3 -0
  180. package/dist/utils/mentionMembers.d.ts.map +1 -0
  181. package/dist/utils/mentionMembers.js +29 -0
  182. package/dist/utils/messageLog.d.ts +2 -2
  183. package/dist/utils/messageLog.d.ts.map +1 -1
  184. package/dist/utils/messageLog.js +129 -40
  185. package/dist/utils/notifiableMessage.d.ts +7 -0
  186. package/dist/utils/notifiableMessage.d.ts.map +1 -0
  187. package/dist/utils/notifiableMessage.js +26 -0
  188. package/dist/utils/withSdkTimeout.d.ts +11 -0
  189. package/dist/utils/withSdkTimeout.d.ts.map +1 -0
  190. package/dist/utils/withSdkTimeout.js +30 -0
  191. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
- import { useEffect } from "react";
1
+ import { useEffect, useRef } from "react";
2
2
  import { useLatest } from "ahooks";
3
3
  import { DChatSDK } from "../../constants/sdk";
4
4
  import { CbEvents, GroupVisibility, MessageType, SessionType, } from "@droppii-org/wasm-client-sdk";
5
- import { BusinessNotificationType, ConnectStatus, CustomType, SyncStatus, } from "../../types/chat";
5
+ import { BusinessNotificationType, ConnectStatus, CustomType, SessionStatus, SyncStatus, } from "../../types/chat";
6
6
  import { pushNewMessage, updateOneMessage } from "../../hooks/message/useMessage";
7
7
  import { fetchPinnedMessages } from "../../hooks/message/usePinnedMessages";
8
8
  import { applyRevokedMessage } from "../../hooks/message/useRevokeMessage";
@@ -22,11 +22,17 @@ import { ignoreInternalLatestMessage } from "../../utils/common";
22
22
  import { invalidateSessionQueries } from "../../utils/queryHelpers";
23
23
  import { refreshPublicGroupLists } from "../../hooks/group/useJoinPublicGroup";
24
24
  import emitter from "../../utils/events";
25
+ import { withSdkTimeout } from "../../utils/withSdkTimeout";
26
+ import { isCoreLoggedIn, useLoginHealthProbe } from "./useLoginHealth";
25
27
  const notPushType = [MessageType.TypingMessage, MessageType.RevokeMessage];
28
+ const LOGIN_TIMEOUT_MS = 30000;
26
29
  export const useGlobalEvent = () => {
27
30
  const { user } = useChatContext();
28
31
  const onNavigate = useChatNavigate();
29
32
  const userRef = useLatest(user);
33
+ // One login at a time: boot, token refresh, the health probe and relogin all share it.
34
+ // Keyed by credential: the same user and token share a flight, a new one chains after it.
35
+ const loginInFlight = useRef(null);
30
36
  const queryClient = useQueryClient();
31
37
  const { updateConnectStatus, updateSyncStatus, getSelfUserInfo } = useChatContext();
32
38
  const updateConversationList = useConversationStore((state) => state.updateConversationList);
@@ -164,7 +170,20 @@ export const useGlobalEvent = () => {
164
170
  const inputStatusChangedHandler = ({ data, }) => {
165
171
  handleInputStatusChanged(data);
166
172
  };
173
+ // Token rotation (refresh timer, storage sync) must not lift a kick: re-logging in would
174
+ // kick the other session back. Only a different user, a logout or a reload starts over.
175
+ const isKickedForCurrentCredentials = () => {
176
+ const { isKicked, kickedUserID } = useBootStore.getState();
177
+ if (!isKicked)
178
+ return false;
179
+ if (!kickedUserID)
180
+ return true;
181
+ return kickedUserID === useAuthStore.getState().userID;
182
+ };
167
183
  const userTokenHandler = () => {
184
+ // A kicked account's token is dead; refreshing it would re-login and restart the kick loop.
185
+ if (isKickedForCurrentCredentials())
186
+ return;
168
187
  refetchChatToken(undefined, {
169
188
  onSettled(data) {
170
189
  var _a;
@@ -183,42 +202,129 @@ export const useGlobalEvent = () => {
183
202
  const initStore = () => {
184
203
  loadConversationList();
185
204
  };
186
- const tryLogin = async () => {
205
+ // Resolves true when this run ended with a usable core session.
206
+ const runLoginOnce = (run) => {
207
+ const { userID, chatToken } = useAuthStore.getState();
208
+ const key = `${userID}:${chatToken}`;
209
+ const running = loginInFlight.current;
210
+ if ((running === null || running === void 0 ? void 0 : running.key) === key)
211
+ return running.promise;
212
+ const start = running
213
+ ? // A different credential must not be dropped, but the running flight may already
214
+ // have logged core in: logging in again would only hit "repeated login".
215
+ running.promise
216
+ .catch(() => false)
217
+ .then((prevOk) => runChained(run, running.userID, prevOk))
218
+ : run();
219
+ const flight = { key, userID, promise: start };
220
+ flight.promise = start.finally(() => {
221
+ if (loginInFlight.current === flight)
222
+ loginInFlight.current = null;
223
+ });
224
+ loginInFlight.current = flight;
225
+ return flight.promise;
226
+ };
227
+ const runChained = async (run, prevUserID, prevOk) => {
228
+ if (isKickedForCurrentCredentials())
229
+ return false;
230
+ const { userID } = useAuthStore.getState();
231
+ if (!(await isCoreLoggedIn()))
232
+ return run();
233
+ // Same user already logged in by the previous flight: nothing to redo.
234
+ if (prevUserID === userID && prevOk)
235
+ return true;
236
+ // Another user, or the previous login failed with the old token: start over with the new one.
237
+ return reloginUnguarded();
238
+ };
239
+ const getCoreUserID = async () => {
240
+ try {
241
+ return (await withSdkTimeout(DChatSDK.getLoginUserID(), "getLoginUserID")).data;
242
+ }
243
+ catch (_a) {
244
+ return undefined;
245
+ }
246
+ };
247
+ const tryLogin = async (isRetry = false) => {
248
+ var _a;
249
+ if (isKickedForCurrentCredentials())
250
+ return false;
187
251
  const { userID, chatToken, platformID, wsAddress } = useAuthStore.getState();
188
252
  try {
189
- await DChatSDK.login({
253
+ await withSdkTimeout(DChatSDK.login({
190
254
  userID,
191
255
  token: chatToken,
192
256
  platformID,
193
257
  apiAddr: `${apiAddress}/chat-service`,
194
258
  wsAddr: `${wsAddress}/chat-service/ws`,
195
- });
259
+ }), "login", LOGIN_TIMEOUT_MS);
196
260
  initStore();
261
+ return true;
197
262
  }
198
263
  catch (error) {
199
- if (error.errCode === 10102) {
200
- updateConnectStatus(ConnectStatus.Connected);
201
- updateSyncStatus(SyncStatus.Success);
202
- // Already logged in (e.g. ChatProvider remounted): sync events may have fired while
203
- // nobody listened, so stop waiting for them rather than risk a boot screen that never ends.
204
- useBootStore.getState().markConnected();
205
- useBootStore.getState().markFirstSyncDone();
264
+ const errCode = (_a = error.errCode) !== null && _a !== void 0 ? _a : error.code;
265
+ if (errCode === 10102) {
266
+ // "Already logged in" is only true if core agrees; a stale tab can hold a dead session.
267
+ if (!isRetry && !(await isCoreLoggedIn())) {
268
+ return reloginUnguarded();
269
+ }
270
+ // Core may hold another user's session (shared storage); only trust it for this user.
271
+ if (!isRetry && (await getCoreUserID()) !== userID) {
272
+ return reloginUnguarded();
273
+ }
274
+ // Sync events may have fired while nobody listened (e.g. ChatProvider remounted); trust
275
+ // only what the boot store itself saw: connect, and the first sync finishing.
276
+ const boot = useBootStore.getState();
277
+ if (boot.hasConnected) {
278
+ updateConnectStatus(ConnectStatus.Connected);
279
+ if (boot.isFirstSyncDone)
280
+ updateSyncStatus(SyncStatus.Success);
281
+ }
206
282
  getSelfUserInfo();
207
283
  initStore();
208
- return;
284
+ return true;
209
285
  }
210
286
  useBootStore.getState().markFailed("engine");
211
287
  console.error(error);
288
+ return false;
289
+ }
290
+ };
291
+ // Drops a dead core session and logs in again with the current chat token; an
292
+ // expired token comes back through OnUserTokenExpired and is refreshed there.
293
+ const reloginUnguarded = async () => {
294
+ if (isKickedForCurrentCredentials())
295
+ return false;
296
+ try {
297
+ await withSdkTimeout(DChatSDK.logout(), "logout");
212
298
  }
299
+ catch (error) {
300
+ console.warn("logout before relogin", error);
301
+ }
302
+ return tryLogin(true);
303
+ };
304
+ const relogin = async () => {
305
+ await runLoginOnce(reloginUnguarded);
306
+ };
307
+ // The account signed in elsewhere and the token is now dead. Core logs itself
308
+ // out on a kick, so do not log out here or log back in: that would kick the
309
+ // other session in turn and loop. A reload starts a fresh sign-in.
310
+ const kickedOfflineHandler = () => {
311
+ updateConnectStatus(ConnectStatus.Disconnected);
312
+ useBootStore.getState().markKicked(useAuthStore.getState().userID);
213
313
  };
314
+ useLoginHealthProbe(relogin);
214
315
  const loginCheck = async () => {
215
316
  const chatToken = useAuthStore.getState().chatToken;
216
317
  const userID = useAuthStore.getState().userID;
217
318
  if (!chatToken || !userID) {
218
319
  return;
219
320
  }
321
+ // A kicked user stays blocked; only a different user starts a fresh boot.
322
+ if (isKickedForCurrentCredentials())
323
+ return;
324
+ if (useBootStore.getState().isKicked)
325
+ useBootStore.getState().reset();
220
326
  useBootStore.getState().startBoot(userID);
221
- tryLogin();
327
+ void runLoginOnce(() => tryLogin());
222
328
  };
223
329
  const connectingHandler = () => {
224
330
  useBootStore.getState().markConnecting();
@@ -282,12 +388,36 @@ export const useGlobalEvent = () => {
282
388
  queryKey: [QUERY_KEYS.GET_BOT_DETAIL, botId],
283
389
  });
284
390
  };
391
+ // A closed session resets the bot flow server-side, so the open thread's
392
+ // cached step and menu are stale until refetched.
393
+ const handleSessionClosed = (conversationId) => {
394
+ const conversation = useConversationStore.getState().conversationData;
395
+ if (!conversation)
396
+ return;
397
+ const openId = conversation.conversationID;
398
+ if (conversationId && conversationId !== openId)
399
+ return;
400
+ // Keep the in-flight refetch an action started; cancelling it would
401
+ // leave that action waiting on a result that never comes.
402
+ void queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_CONVERSATION_DETAIL, openId] }, { cancelRefetch: false });
403
+ void queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_BOT_MAIN_MENU, openId] }, { cancelRefetch: false });
404
+ };
285
405
  const businessNotificationHandler = ({ data, }) => {
286
406
  console.log(data.key, "business notification");
287
407
  switch (data.key) {
288
- case BusinessNotificationType.SESSION_STATE_UPDATED:
289
- updateSession(JSON.parse(data.data || "{}"));
408
+ case BusinessNotificationType.SESSION_STATE_UPDATED: {
409
+ const session = JSON.parse(data.data || "{}");
410
+ updateSession(session);
411
+ if (session.status === SessionStatus.COMPLETED) {
412
+ handleSessionClosed(session.conversationId);
413
+ }
414
+ break;
415
+ }
416
+ case BusinessNotificationType.SESSION_CLOSED: {
417
+ const closed = JSON.parse(data.data || "{}");
418
+ handleSessionClosed(closed.conversationId);
290
419
  break;
420
+ }
291
421
  case BusinessNotificationType.SESSION_CREATED:
292
422
  updateSession(JSON.parse(data.data || "{}"));
293
423
  queryClient.invalidateQueries({
@@ -446,6 +576,7 @@ export const useGlobalEvent = () => {
446
576
  //account
447
577
  DChatSDK.on(CbEvents.OnUserTokenExpired, userTokenHandler);
448
578
  DChatSDK.on(CbEvents.OnUserTokenInvalid, userTokenHandler);
579
+ DChatSDK.on(CbEvents.OnKickedOffline, kickedOfflineHandler);
449
580
  DChatSDK.on(CbEvents.OnConnecting, connectingHandler);
450
581
  DChatSDK.on(CbEvents.OnConnectSuccess, connectSuccessHandler);
451
582
  DChatSDK.on(CbEvents.OnConnectFailed, connectFailedHandler);
@@ -481,6 +612,7 @@ export const useGlobalEvent = () => {
481
612
  //account
482
613
  DChatSDK.off(CbEvents.OnUserTokenExpired, userTokenHandler);
483
614
  DChatSDK.off(CbEvents.OnUserTokenInvalid, userTokenHandler);
615
+ DChatSDK.off(CbEvents.OnKickedOffline, kickedOfflineHandler);
484
616
  DChatSDK.off(CbEvents.OnConnecting, connectingHandler);
485
617
  DChatSDK.off(CbEvents.OnConnectSuccess, connectSuccessHandler);
486
618
  DChatSDK.off(CbEvents.OnConnectFailed, connectFailedHandler);
@@ -0,0 +1,3 @@
1
+ export declare const isCoreLoggedIn: () => Promise<boolean>;
2
+ export declare const useLoginHealthProbe: (relogin: () => Promise<void>) => void;
3
+ //# sourceMappingURL=useLoginHealth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLoginHealth.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useLoginHealth.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,cAAc,QAAa,OAAO,CAAC,OAAO,CAQtD,CAAC;AAKF,eAAO,MAAM,mBAAmB,GAAI,SAAS,MAAM,OAAO,CAAC,IAAI,CAAC,SA6B/D,CAAC"}
@@ -0,0 +1,55 @@
1
+ import { useEffect } from "react";
2
+ import { useLatest } from "ahooks";
3
+ import { LoginStatus } from "@droppii-org/wasm-client-sdk";
4
+ import { DChatSDK } from "../../constants/sdk";
5
+ import useAuthStore from "../../store/auth";
6
+ import useBootStore from "../../store/boot";
7
+ import { withSdkTimeout } from "../../utils/withSdkTimeout";
8
+ // A failed or hung status read counts as not logged in: either way the tab cannot talk to core.
9
+ export const isCoreLoggedIn = async () => {
10
+ try {
11
+ const { data } = await withSdkTimeout(DChatSDK.getLoginStatus(), "getLoginStatus");
12
+ return data === LoginStatus.Logged || data === LoginStatus.Logging;
13
+ }
14
+ catch (error) {
15
+ console.warn("getLoginStatus", error);
16
+ return false;
17
+ }
18
+ };
19
+ // A desk tab left open for hours can come back with core logged out while the UI still
20
+ // shows "connected". Checking when the tab is shown again or the network returns repairs
21
+ // it without a reload.
22
+ export const useLoginHealthProbe = (relogin) => {
23
+ const reloginRef = useLatest(relogin);
24
+ useEffect(() => {
25
+ let probing = false;
26
+ const probe = async () => {
27
+ var _a;
28
+ if (probing || document.visibilityState === "hidden")
29
+ return;
30
+ const { userID, apiAddress, chatToken } = useAuthStore.getState();
31
+ if (!userID || !apiAddress || !chatToken || !DChatSDK)
32
+ return;
33
+ // Boot owns the first login; after a kick the token is dead and a relogin
34
+ // would only kick the other session again.
35
+ const boot = useBootStore.getState();
36
+ const isBooting = !boot.isFirstSyncDone && !boot.failedStep;
37
+ if (boot.isKicked || isBooting)
38
+ return;
39
+ probing = true;
40
+ try {
41
+ if (!(await isCoreLoggedIn()))
42
+ await ((_a = reloginRef.current) === null || _a === void 0 ? void 0 : _a.call(reloginRef));
43
+ }
44
+ finally {
45
+ probing = false;
46
+ }
47
+ };
48
+ document.addEventListener("visibilitychange", probe);
49
+ window.addEventListener("online", probe);
50
+ return () => {
51
+ document.removeEventListener("visibilitychange", probe);
52
+ window.removeEventListener("online", probe);
53
+ };
54
+ }, [reloginRef]);
55
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"useCreateGroup.d.ts","sourceRoot":"","sources":["../../../src/hooks/group/useCreateGroup.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAEjE,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAqCD,eAAO,MAAM,cAAc;6DAQ2B,iBAAiB;CA2DtE,CAAC"}
1
+ {"version":3,"file":"useCreateGroup.d.ts","sourceRoot":"","sources":["../../../src/hooks/group/useCreateGroup.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAEjE,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAyGD,eAAO,MAAM,cAAc;6DAS2B,iBAAiB;CAkFtE,CAAC"}
@@ -6,11 +6,58 @@ import { useTranslation } from "react-i18next";
6
6
  import { GroupType, GroupVerificationType, GroupVisibility, SessionType } from "@droppii-org/wasm-client-sdk";
7
7
  import { DChatSDK } from "../../constants/sdk";
8
8
  import useAuthStore from "../../store/auth";
9
+ import { useChatContext } from "../../context/ChatContext";
9
10
  import { useDeskThreadNavigation } from "../../hooks/conversation/useDeskThreadNavigation";
10
11
  import { generateDefaultGroupName } from "../../utils/groupName";
12
+ import { SdkTimeoutError, withSdkTimeout } from "../../utils/withSdkTimeout";
11
13
  import { QUERY_KEYS } from "../../services/query";
12
14
  const GET_CONVERSATION_RETRY_COUNT = 4;
13
15
  const GET_CONVERSATION_RETRY_DELAY_MS = 500;
16
+ // A create that timed out may still have gone through. Only a group that did not exist
17
+ // before the call and was created since it started (minus clock skew) can be the result.
18
+ const CREATE_TIME_SKEW_MS = 5000;
19
+ const listJoinedGroups = async () => {
20
+ const { data } = await withSdkTimeout(DChatSDK.getJoinedGroupList(), "getJoinedGroupList");
21
+ return data !== null && data !== void 0 ? data : [];
22
+ };
23
+ // Full member IDs when core can list them; null means unknown, so the head count decides.
24
+ const hasExpectedMembers = async (groupID, expectedUserIDs) => {
25
+ try {
26
+ const { data } = await withSdkTimeout(DChatSDK.getGroupMemberList({
27
+ groupID,
28
+ filter: 0,
29
+ offset: 0,
30
+ count: expectedUserIDs.length + 1,
31
+ }), "getGroupMemberList");
32
+ if (!data)
33
+ return null;
34
+ const actual = new Set(data.map((member) => member.userID));
35
+ return (actual.size === expectedUserIDs.length &&
36
+ expectedUserIDs.every((userID) => actual.has(userID)));
37
+ }
38
+ catch (_a) {
39
+ return null;
40
+ }
41
+ };
42
+ const findRecentlyCreatedGroup = async ({ name, ownerUserID, memberUserIDs, since, knownGroupIDs, }) => {
43
+ try {
44
+ const expectedUserIDs = [...new Set([ownerUserID, ...memberUserIDs])];
45
+ const candidates = (await listJoinedGroups()).filter((group) => !knownGroupIDs.has(group.groupID) &&
46
+ group.groupName === name &&
47
+ group.ownerUserID === ownerUserID &&
48
+ group.memberCount === expectedUserIDs.length &&
49
+ group.createTime >= since - CREATE_TIME_SKEW_MS);
50
+ for (const group of candidates) {
51
+ if ((await hasExpectedMembers(group.groupID, expectedUserIDs)) !== false)
52
+ return group;
53
+ }
54
+ return undefined;
55
+ }
56
+ catch (error) {
57
+ console.warn("find recently created group", error);
58
+ return undefined;
59
+ }
60
+ };
14
61
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
62
  // createGroup()'s own response succeeding doesn't mean the local WASM store
16
63
  // has the new conversation yet — the WS event that inserts it locally can
@@ -21,10 +68,10 @@ const getOneConversationWithRetry = async (groupID) => {
21
68
  let lastError;
22
69
  for (let attempt = 0; attempt < GET_CONVERSATION_RETRY_COUNT; attempt++) {
23
70
  try {
24
- return await DChatSDK.getOneConversation({
71
+ return await withSdkTimeout(DChatSDK.getOneConversation({
25
72
  sourceID: groupID,
26
73
  sessionType: SessionType.Group,
27
- });
74
+ }), "getOneConversation");
28
75
  }
29
76
  catch (error) {
30
77
  lastError = error;
@@ -46,13 +93,14 @@ export const useCreateGroup = () => {
46
93
  const { openThread } = useDeskThreadNavigation();
47
94
  const queryClient = useQueryClient();
48
95
  const userID = useAuthStore((state) => state.userID);
96
+ const { user } = useChatContext();
49
97
  const createGroup = useCallback(async ({ groupName, isPublic, members, faceURL }) => {
50
98
  var _a, _b;
51
99
  const trimmedName = groupName.trim();
52
100
  const [firstMember, ...restMembers] = members;
53
101
  const resolvedName = trimmedName ||
54
102
  generateDefaultGroupName({
55
- creatorName: tCommon("you"),
103
+ creatorName: (user === null || user === void 0 ? void 0 : user.nickname) || tCommon("you"),
56
104
  firstMemberName: firstMember === null || firstMember === void 0 ? void 0 : firstMember.displayName,
57
105
  remainingCount: restMembers.length,
58
106
  });
@@ -60,7 +108,13 @@ export const useCreateGroup = () => {
60
108
  // group image (DROPPII-30928).
61
109
  const resolvedFaceURL = faceURL || "";
62
110
  try {
63
- const { data: group } = await DChatSDK.createGroup({
111
+ // Without a snapshot a pre-existing twin cannot be told apart, so the fallback stays off.
112
+ const knownGroupIDs = await listJoinedGroups()
113
+ .then((groups) => new Set(groups.map((group) => group.groupID)))
114
+ .catch(() => null);
115
+ const startedAt = Date.now();
116
+ // A stalled core call must end in the error toast, not a dialog stuck on "loading".
117
+ const { groupID } = await withSdkTimeout(DChatSDK.createGroup({
64
118
  memberUserIDs: members.map((member) => member.userID),
65
119
  ownerUserID: userID,
66
120
  groupInfo: {
@@ -71,8 +125,23 @@ export const useCreateGroup = () => {
71
125
  ex: JSON.stringify({ isPublic }),
72
126
  visibility: isPublic ? GroupVisibility.Public : GroupVisibility.Private,
73
127
  },
128
+ }), "createGroup").then(({ data }) => data, async (error) => {
129
+ if (!(error instanceof SdkTimeoutError))
130
+ throw error;
131
+ const created = knownGroupIDs
132
+ ? await findRecentlyCreatedGroup({
133
+ name: resolvedName,
134
+ ownerUserID: userID,
135
+ memberUserIDs: members.map((member) => member.userID),
136
+ since: startedAt,
137
+ knownGroupIDs,
138
+ })
139
+ : undefined;
140
+ if (!created)
141
+ throw error;
142
+ return created;
74
143
  });
75
- const { data: conversation } = await getOneConversationWithRetry(group.groupID);
144
+ const { data: conversation } = await getOneConversationWithRetry(groupID);
76
145
  message.success(t("create_group.create_group_success"));
77
146
  void queryClient.invalidateQueries({
78
147
  queryKey: [QUERY_KEYS.GET_CONVERSATION_GROUPS],
@@ -87,6 +156,6 @@ export const useCreateGroup = () => {
87
156
  message.error(errorMessage);
88
157
  throw error;
89
158
  }
90
- }, [openThread, t, tCommon, userID, queryClient]);
159
+ }, [openThread, t, tCommon, userID, user === null || user === void 0 ? void 0 : user.nickname, queryClient]);
91
160
  return { createGroup };
92
161
  };
@@ -5,6 +5,8 @@ export interface ChatBootState {
5
5
  phase: ChatBootPhase;
6
6
  /** Step that failed when `phase` is "error". */
7
7
  failedStep: ChatBootFailedStep | null;
8
+ /** The failure is a sign-in from another device, not a network problem. */
9
+ isKicked: boolean;
8
10
  /** 0-100, meaningful in the "sync" phase. */
9
11
  syncProgress: number;
10
12
  /** The current step has run longer than the caller's `slowAfterMs`. */
@@ -1 +1 @@
1
- {"version":3,"file":"useChatBootPhase.d.ts","sourceRoot":"","sources":["../../../src/hooks/init/useChatBootPhase.ts"],"names":[],"mappings":"AAIA,OAAqB,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAE9E,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,aAAa,CAAC;IACrB,gDAAgD;IAChD,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACtC,6CAA6C;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,MAAM,EAAE,OAAO,CAAC;IAChB,oFAAoF;IACpF,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,eAAO,MAAM,oBAAoB,MAAM,CAAC;AACxC,oFAAoF;AACpF,eAAO,MAAM,aAAa,QAAS,CAAC;AAEpC,UAAU,qBAAqB;IAC7B,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACvC;AAED,eAAO,MAAM,oBAAoB,GAAI,+DAKlC,qBAAqB,KAAG,aAO1B,CAAC;AAEF,iGAAiG;AACjG,eAAO,MAAM,gBAAgB,GAAI,kBAE9B;IACD,WAAW,EAAE,MAAM,CAAC;CACrB,KAAG,aAwDH,CAAC"}
1
+ {"version":3,"file":"useChatBootPhase.d.ts","sourceRoot":"","sources":["../../../src/hooks/init/useChatBootPhase.ts"],"names":[],"mappings":"AAIA,OAAqB,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAE9E,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,aAAa,CAAC;IACrB,gDAAgD;IAChD,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACtC,2EAA2E;IAC3E,QAAQ,EAAE,OAAO,CAAC;IAClB,6CAA6C;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,MAAM,EAAE,OAAO,CAAC;IAChB,oFAAoF;IACpF,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,eAAO,MAAM,oBAAoB,MAAM,CAAC;AACxC,oFAAoF;AACpF,eAAO,MAAM,aAAa,QAAS,CAAC;AAEpC,UAAU,qBAAqB;IAC7B,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACvC;AAED,eAAO,MAAM,oBAAoB,GAAI,+DAKlC,qBAAqB,KAAG,aAO1B,CAAC;AAEF,iGAAiG;AACjG,eAAO,MAAM,gBAAgB,GAAI,kBAE9B;IACD,WAAW,EAAE,MAAM,CAAC;CACrB,KAAG,aA6DH,CAAC"}
@@ -22,13 +22,17 @@ export const useChatBootPhase = ({ slowAfterMs, }) => {
22
22
  const hasConnected = useBootStore((state) => state.hasConnected);
23
23
  const isFirstSyncDone = useBootStore((state) => state.isFirstSyncDone);
24
24
  const failedStep = useBootStore((state) => state.failedStep);
25
+ const isKicked = useBootStore((state) => state.isKicked);
25
26
  const syncProgress = useBootStore((state) => state.syncProgress);
26
- const basePhase = resolveChatBootPhase({
27
- connectStatus,
28
- hasConnected,
29
- isFirstSyncDone,
30
- failedStep,
31
- });
27
+ // A kick is final even if a later connect event flips the connect status back.
28
+ const basePhase = isKicked
29
+ ? "error"
30
+ : resolveChatBootPhase({
31
+ connectStatus,
32
+ hasConnected,
33
+ isFirstSyncDone,
34
+ failedStep,
35
+ });
32
36
  // Sync progress restarts the slow/stall timers: a moving sync is not stuck.
33
37
  const stepKey = basePhase === "sync" ? `sync:${syncProgress}` : basePhase;
34
38
  const [isDelayOver, setIsDelayOver] = useState(false);
@@ -65,6 +69,7 @@ export const useChatBootPhase = ({ slowAfterMs, }) => {
65
69
  return {
66
70
  phase,
67
71
  failedStep: isStalled ? "connect" : phase === "error" ? failedStep : null,
72
+ isKicked: phase === "error" && isKicked,
68
73
  syncProgress,
69
74
  isSlow: slowStepKey === stepKey,
70
75
  isStatusVisible: isDelayOver || phase === "error",
@@ -1 +1 @@
1
- {"version":3,"file":"useMessage.d.ts","sourceRoot":"","sources":["../../../src/hooks/message/useMessage.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,WAAW,EACX,WAAW,EAEZ,MAAM,8BAA8B,CAAC;AAkBtC,eAAO,MAAM,kBAAkB,eAkB9B,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS,WAAW,EACpB,OAAO,OAAO,EACd,eAAe;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5E,eAAe,MAAM,KACpB,OA4BF,CAAC;AAEF,eAAO,MAAM,UAAU,GACrB,iBAAiB,MAAM,EACvB,oBAAoB,MAAM,EAC1B,iBAAc,EACd,uBAAqB,EAGrB,yBAAyB,MAAM,IAAI;;;;;qBAed,WAAW,EAAE;;;;;;qBAAb,WAAW,EAAE;;;;;;;uCAwBlB,MAAM;CAmYvB,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,SACpB,CAAC;AAChC,eAAO,MAAM,gBAAgB,GAAI,SAAS,WAAW,SACpB,CAAC;AAClC,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,SACf,CAAC;AAEtC,eAAO,MAAM,kBAAkB,GAC7B,aAAa,WAAW,EAAE,EAC1B,SAAS,WAAW,EACpB,WAAW,MAAM,GAAG,MAAM,EAC1B,YAAY,CAAC,SAAS,EAAE,WAAW,KAAK,OAAO,KAC9C,WAAW,GAAG,SAqBhB,CAAC;AAMF,eAAO,MAAM,oBAAoB,GAC/B,aAAa,WAAW,EAAE,EAC1B,SAAS,WAAW,EACpB,WAAW,MAAM,GAAG,MAAM,KACzB,WAAW,GAAG,SAQhB,CAAC;AAmBF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,WAAW,EAAE,EACtB,SAAS,WAAW,EAAE,kBAgBvB,CAAC"}
1
+ {"version":3,"file":"useMessage.d.ts","sourceRoot":"","sources":["../../../src/hooks/message/useMessage.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,WAAW,EAEX,WAAW,EAEZ,MAAM,8BAA8B,CAAC;AAkBtC,eAAO,MAAM,kBAAkB,eAkB9B,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS,WAAW,EACpB,OAAO,OAAO,EACd,eAAe;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5E,eAAe,MAAM,KACpB,OA4BF,CAAC;AAEF,eAAO,MAAM,UAAU,GACrB,iBAAiB,MAAM,EACvB,oBAAoB,MAAM,EAC1B,iBAAc,EACd,uBAAqB,EAGrB,yBAAyB,MAAM,IAAI;;;;;qBAed,WAAW,EAAE;;;;;;qBAAb,WAAW,EAAE;;;;;;;uCA6BlB,MAAM;CAscvB,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,SACpB,CAAC;AAChC,eAAO,MAAM,gBAAgB,GAAI,SAAS,WAAW,SACpB,CAAC;AAClC,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,SACf,CAAC;AAEtC,eAAO,MAAM,kBAAkB,GAC7B,aAAa,WAAW,EAAE,EAC1B,SAAS,WAAW,EACpB,WAAW,MAAM,GAAG,MAAM,EAC1B,YAAY,CAAC,SAAS,EAAE,WAAW,KAAK,OAAO,KAC9C,WAAW,GAAG,SAqBhB,CAAC;AAMF,eAAO,MAAM,oBAAoB,GAC/B,aAAa,WAAW,EAAE,EAC1B,SAAS,WAAW,EACpB,WAAW,MAAM,GAAG,MAAM,KACzB,WAAW,GAAG,SAQhB,CAAC;AAkCF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,WAAW,EAAE,EACtB,SAAS,WAAW,EAAE,kBAgBvB,CAAC"}