@ethora/chat-component-rn 26.5.5 → 26.5.7

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 (149) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/lib/commonjs/components/MainComponents/ChatHeader.js +7 -1
  3. package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
  4. package/lib/commonjs/components/MainComponents/ChatRoom.js +8 -2
  5. package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
  6. package/lib/commonjs/components/MainComponents/ChatWrapper.js +2 -1
  7. package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
  8. package/lib/commonjs/components/MainComponents/MediaMessage.js +11 -5
  9. package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
  10. package/lib/commonjs/components/MainComponents/MessageList.js +10 -2
  11. package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
  12. package/lib/commonjs/components/MessageBubble/Message.js +60 -7
  13. package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
  14. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +35 -16
  15. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  16. package/lib/commonjs/components/Modals/ModalWrapper/ModalWrapper.js +11 -2
  17. package/lib/commonjs/components/Modals/ModalWrapper/ModalWrapper.js.map +1 -1
  18. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +1 -1
  19. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  20. package/lib/commonjs/components/styled/MediaFilePreview.js +96 -5
  21. package/lib/commonjs/components/styled/MediaFilePreview.js.map +1 -1
  22. package/lib/commonjs/components/styled/MessageImage.js +1 -2
  23. package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
  24. package/lib/commonjs/components/styled/SendInput.js +4 -2
  25. package/lib/commonjs/components/styled/SendInput.js.map +1 -1
  26. package/lib/commonjs/components/styled/VideoMessage.js +2 -10
  27. package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
  28. package/lib/commonjs/context/xmppProvider.js +71 -2
  29. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  30. package/lib/commonjs/helpers/mimeToExtension.js +131 -0
  31. package/lib/commonjs/helpers/mimeToExtension.js.map +1 -0
  32. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js +73 -1
  33. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  34. package/lib/commonjs/hooks/useChatWrapperInit.js +22 -10
  35. package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
  36. package/lib/commonjs/hooks/useEventHandlers.js +9 -1
  37. package/lib/commonjs/hooks/useEventHandlers.js.map +1 -1
  38. package/lib/commonjs/hooks/useMessageHeapState.js +6 -2
  39. package/lib/commonjs/hooks/useMessageHeapState.js.map +1 -1
  40. package/lib/commonjs/hooks/useSendMessage.js +48 -4
  41. package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
  42. package/lib/commonjs/networking/api-requests/auth.api.js +7 -1
  43. package/lib/commonjs/networking/api-requests/auth.api.js.map +1 -1
  44. package/lib/commonjs/networking/xmppClient.js +87 -1
  45. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  46. package/lib/commonjs/roomStore/roomHeapSlice.js +26 -8
  47. package/lib/commonjs/roomStore/roomHeapSlice.js.map +1 -1
  48. package/lib/module/components/MainComponents/ChatHeader.js +7 -1
  49. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  50. package/lib/module/components/MainComponents/ChatRoom.js +8 -2
  51. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  52. package/lib/module/components/MainComponents/ChatWrapper.js +2 -1
  53. package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
  54. package/lib/module/components/MainComponents/MediaMessage.js +11 -5
  55. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  56. package/lib/module/components/MainComponents/MessageList.js +11 -3
  57. package/lib/module/components/MainComponents/MessageList.js.map +1 -1
  58. package/lib/module/components/MessageBubble/Message.js +60 -7
  59. package/lib/module/components/MessageBubble/Message.js.map +1 -1
  60. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -17
  61. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  62. package/lib/module/components/Modals/ModalWrapper/ModalWrapper.js +11 -2
  63. package/lib/module/components/Modals/ModalWrapper/ModalWrapper.js.map +1 -1
  64. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +1 -1
  65. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  66. package/lib/module/components/styled/MediaFilePreview.js +97 -6
  67. package/lib/module/components/styled/MediaFilePreview.js.map +1 -1
  68. package/lib/module/components/styled/MessageImage.js +1 -2
  69. package/lib/module/components/styled/MessageImage.js.map +1 -1
  70. package/lib/module/components/styled/SendInput.js +4 -2
  71. package/lib/module/components/styled/SendInput.js.map +1 -1
  72. package/lib/module/components/styled/VideoMessage.js +2 -10
  73. package/lib/module/components/styled/VideoMessage.js.map +1 -1
  74. package/lib/module/context/xmppProvider.js +73 -3
  75. package/lib/module/context/xmppProvider.js.map +1 -1
  76. package/lib/module/helpers/mimeToExtension.js +124 -0
  77. package/lib/module/helpers/mimeToExtension.js.map +1 -0
  78. package/lib/module/helpers/resolveInitBeforeLoadUser.js +71 -0
  79. package/lib/module/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  80. package/lib/module/hooks/useChatWrapperInit.js +22 -10
  81. package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
  82. package/lib/module/hooks/useEventHandlers.js +9 -1
  83. package/lib/module/hooks/useEventHandlers.js.map +1 -1
  84. package/lib/module/hooks/useMessageHeapState.js +6 -2
  85. package/lib/module/hooks/useMessageHeapState.js.map +1 -1
  86. package/lib/module/hooks/useSendMessage.js +49 -5
  87. package/lib/module/hooks/useSendMessage.js.map +1 -1
  88. package/lib/module/networking/api-requests/auth.api.js +7 -1
  89. package/lib/module/networking/api-requests/auth.api.js.map +1 -1
  90. package/lib/module/networking/xmppClient.js +87 -1
  91. package/lib/module/networking/xmppClient.js.map +1 -1
  92. package/lib/module/roomStore/roomHeapSlice.js +24 -7
  93. package/lib/module/roomStore/roomHeapSlice.js.map +1 -1
  94. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  95. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  96. package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
  97. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  98. package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
  99. package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
  100. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  101. package/lib/typescript/components/Modals/ModalWrapper/ModalWrapper.d.ts +6 -0
  102. package/lib/typescript/components/Modals/ModalWrapper/ModalWrapper.d.ts.map +1 -1
  103. package/lib/typescript/components/styled/MediaFilePreview.d.ts.map +1 -1
  104. package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
  105. package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
  106. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  107. package/lib/typescript/helpers/mimeToExtension.d.ts +37 -0
  108. package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -0
  109. package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts +18 -0
  110. package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts.map +1 -1
  111. package/lib/typescript/hooks/useChatWrapperInit.d.ts.map +1 -1
  112. package/lib/typescript/hooks/useEventHandlers.d.ts +6 -0
  113. package/lib/typescript/hooks/useEventHandlers.d.ts.map +1 -1
  114. package/lib/typescript/hooks/useMessageHeapState.d.ts +2 -0
  115. package/lib/typescript/hooks/useMessageHeapState.d.ts.map +1 -1
  116. package/lib/typescript/hooks/useSendMessage.d.ts +2 -1
  117. package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
  118. package/lib/typescript/networking/api-requests/auth.api.d.ts.map +1 -1
  119. package/lib/typescript/networking/xmppClient.d.ts +25 -1
  120. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  121. package/lib/typescript/roomStore/roomHeapSlice.d.ts +22 -1
  122. package/lib/typescript/roomStore/roomHeapSlice.d.ts.map +1 -1
  123. package/lib/typescript/types/types.d.ts +6 -0
  124. package/lib/typescript/types/types.d.ts.map +1 -1
  125. package/package.json +1 -1
  126. package/src/components/MainComponents/ChatHeader.tsx +5 -1
  127. package/src/components/MainComponents/ChatRoom.tsx +6 -1
  128. package/src/components/MainComponents/ChatWrapper.tsx +1 -0
  129. package/src/components/MainComponents/MediaMessage.tsx +15 -8
  130. package/src/components/MainComponents/MessageList.tsx +10 -3
  131. package/src/components/MessageBubble/Message.tsx +64 -10
  132. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +30 -12
  133. package/src/components/Modals/ModalWrapper/ModalWrapper.tsx +22 -1
  134. package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +1 -1
  135. package/src/components/styled/MediaFilePreview.tsx +91 -5
  136. package/src/components/styled/MessageImage.tsx +1 -2
  137. package/src/components/styled/SendInput.tsx +3 -0
  138. package/src/components/styled/VideoMessage.tsx +3 -10
  139. package/src/context/xmppProvider.tsx +90 -2
  140. package/src/helpers/mimeToExtension.ts +135 -0
  141. package/src/helpers/resolveInitBeforeLoadUser.ts +79 -0
  142. package/src/hooks/useChatWrapperInit.ts +51 -46
  143. package/src/hooks/useEventHandlers.tsx +24 -1
  144. package/src/hooks/useMessageHeapState.tsx +6 -2
  145. package/src/hooks/useSendMessage.tsx +75 -4
  146. package/src/networking/api-requests/auth.api.ts +5 -1
  147. package/src/networking/xmppClient.ts +95 -1
  148. package/src/roomStore/roomHeapSlice.ts +47 -4
  149. package/src/types/types.ts +10 -0
@@ -0,0 +1,135 @@
1
+ // MIME → file extension table for download/preview filename fallbacks.
2
+ // Centralized so FilePreviewModal, MediaMessage, and any future media
3
+ // surface share one source of truth. Keep additions sorted by category.
4
+
5
+ const MIME_TO_EXT: Record<string, string> = {
6
+ // images
7
+ 'image/jpeg': '.jpg',
8
+ 'image/jpg': '.jpg',
9
+ 'image/png': '.png',
10
+ 'image/gif': '.gif',
11
+ 'image/webp': '.webp',
12
+ 'image/heic': '.heic',
13
+ 'image/heif': '.heif',
14
+ 'image/bmp': '.bmp',
15
+ 'image/svg+xml': '.svg',
16
+ 'image/tiff': '.tiff',
17
+
18
+ // video
19
+ 'video/mp4': '.mp4',
20
+ 'video/quicktime': '.mov',
21
+ 'video/x-m4v': '.m4v',
22
+ 'video/webm': '.webm',
23
+ 'video/3gpp': '.3gp',
24
+ 'video/x-matroska': '.mkv',
25
+ 'video/mpeg': '.mpeg',
26
+
27
+ // audio
28
+ 'audio/mpeg': '.mp3',
29
+ 'audio/mp3': '.mp3',
30
+ 'audio/mp4': '.m4a',
31
+ 'audio/x-m4a': '.m4a',
32
+ 'audio/wav': '.wav',
33
+ 'audio/x-wav': '.wav',
34
+ 'audio/aac': '.aac',
35
+ 'audio/ogg': '.ogg',
36
+ 'audio/flac': '.flac',
37
+ 'audio/webm': '.weba',
38
+
39
+ // documents
40
+ 'application/pdf': '.pdf',
41
+ 'application/msword': '.doc',
42
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
43
+ '.docx',
44
+ 'application/vnd.ms-excel': '.xls',
45
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
46
+ 'application/vnd.ms-powerpoint': '.ppt',
47
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
48
+ '.pptx',
49
+ 'text/plain': '.txt',
50
+ 'text/csv': '.csv',
51
+ 'text/html': '.html',
52
+ 'application/json': '.json',
53
+ 'application/xml': '.xml',
54
+ 'text/xml': '.xml',
55
+ 'application/rtf': '.rtf',
56
+
57
+ // archives
58
+ 'application/zip': '.zip',
59
+ 'application/x-rar-compressed': '.rar',
60
+ 'application/x-7z-compressed': '.7z',
61
+ 'application/gzip': '.gz',
62
+ 'application/x-tar': '.tar',
63
+ };
64
+
65
+ const VALID_EXT_RE = /\.[a-z0-9]{1,8}$/i;
66
+
67
+ /**
68
+ * Returns the conventional extension (including the leading dot) for a
69
+ * MIME type, or `.bin` as a last-resort fallback so callers never end up
70
+ * with an extension-less filename.
71
+ */
72
+ export function getExtensionForMime(mime: string | undefined | null): string {
73
+ if (!mime) {return '.bin';}
74
+ const normalized = mime.toLowerCase().split(';')[0]!.trim();
75
+ return MIME_TO_EXT[normalized] || '.bin';
76
+ }
77
+
78
+ /**
79
+ * Parses a URL or file path and returns the last segment, URL-decoded,
80
+ * stripped of query/hash. Returns `''` when nothing usable is present.
81
+ */
82
+ export function filenameFromUrl(url: string | undefined | null): string {
83
+ if (!url) {return '';}
84
+ try {
85
+ // Strip query + hash without depending on a real URL parser (some RN
86
+ // URLs from CDNs do not parse cleanly).
87
+ const noQuery = url.split('?')[0]!.split('#')[0]!;
88
+ const last = noQuery.split('/').pop() || '';
89
+ return decodeURIComponent(last);
90
+ } catch {
91
+ return '';
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Ensures `fileName` ends with a valid extension. Strategy:
97
+ * 1. If the input already has an extension, return it unchanged.
98
+ * 2. Otherwise, derive the extension from `mime` and append it.
99
+ * 3. If `mime` is unknown, append `.bin`.
100
+ *
101
+ * Empty/falsy filenames get a `media_<timestamp>` base before the
102
+ * extension is appended so callers always get a usable filename.
103
+ */
104
+ export function ensureFilenameHasExtension(
105
+ fileName: string | undefined | null,
106
+ mime: string | undefined | null
107
+ ): string {
108
+ const base =
109
+ (fileName && fileName.trim()) || `media_${Date.now()}`;
110
+ if (VALID_EXT_RE.test(base)) {return base;}
111
+ return `${base}${getExtensionForMime(mime)}`;
112
+ }
113
+
114
+ /**
115
+ * Best-effort filename derivation for a message bubble:
116
+ * - server-provided `fileName`
117
+ * - server-provided `originalName`
118
+ * - URL pathname
119
+ * - generic `media_<timestamp>` so we never render an empty label
120
+ *
121
+ * Always returns a string with a valid extension.
122
+ */
123
+ export function deriveDisplayFilename(opts: {
124
+ fileName?: string | null;
125
+ originalName?: string | null;
126
+ url?: string | null;
127
+ mime?: string | null;
128
+ }): string {
129
+ const candidate =
130
+ (opts.fileName && opts.fileName.trim()) ||
131
+ (opts.originalName && opts.originalName.trim()) ||
132
+ filenameFromUrl(opts.url) ||
133
+ '';
134
+ return ensureFilenameHasExtension(candidate, opts.mime);
135
+ }
@@ -237,4 +237,83 @@ export const applyResolvedUserToStore = (user?: User | null) => {
237
237
  store.dispatch(setUser(user));
238
238
  };
239
239
 
240
+ /**
241
+ * Re-mints a User with a fresh xmppPassword for the XMPP credentials-
242
+ * provider path (bug #17). Unlike `resolveInitBeforeLoadUser` this
243
+ * ALWAYS hydrates via the chosen auth path even if the cached user
244
+ * already had xmppCredentials — that's the whole point, the old ones
245
+ * are stale.
246
+ *
247
+ * Selection order:
248
+ * 1. `jwtLogin` — re-exchange the JWT via `/users/client`.
249
+ * 2. Anything else (`userLogin` / `customLogin` / store / persisted)
250
+ * — `tryHydrateViaMy` on the current user, which calls
251
+ * `/users/my` and, on 401, refreshes the access token via
252
+ * `/users/login/refresh` and retries.
253
+ *
254
+ * Returns `null` only when nothing usable is available — the caller
255
+ * (xmppProvider) falls back to the cached creds in that case.
256
+ */
257
+ export const refreshUserCredentialsForXmpp = async (
258
+ config?: IConfig,
259
+ signal?: AbortSignal
260
+ ): Promise<User | null> => {
261
+ if (signal?.aborted) {return null;}
262
+
263
+ // Priority 1: jwtLogin — same path as `resolveInitBeforeLoadUser`
264
+ // priority 3. We always re-exchange because the original JWT may
265
+ // itself have expired (and the consumer is expected to swap
266
+ // `config.jwtLogin.token` for a fresh one before then).
267
+ if (config?.jwtLogin?.enabled && config?.jwtLogin?.token) {
268
+ try {
269
+ const fresh = await loginViaJwt(config.jwtLogin.token);
270
+ const normalized = normalizeUserForXmpp(fresh);
271
+ if (normalized && hasXmppCredentials(normalized)) {return normalized;}
272
+ } catch (error) {
273
+ if (signal?.aborted || isAbortError(error)) {return null;}
274
+ // fall through to /users/my path
275
+ }
276
+ }
277
+
278
+ const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/users/my';
279
+
280
+ // Priority 2: redux store user (covers userLogin + customLogin +
281
+ // anything previously persisted into chatSettingStore.user). The
282
+ // user must have at least a refreshToken for the refresh chain to
283
+ // produce something useful.
284
+ const currentUser = store.getState().chatSettingStore.user;
285
+ if (currentUser?.token || currentUser?.refreshToken) {
286
+ const hydrated = await tryHydrateViaMy(
287
+ currentUser as User,
288
+ myEndpoint,
289
+ signal
290
+ ).catch(() => null);
291
+ if (hydrated && hasXmppCredentials(hydrated)) {return hydrated;}
292
+ }
293
+
294
+ // Priority 3: persisted user in AsyncStorage — defensive; the store
295
+ // should already mirror this, but if the redux state got cleared
296
+ // mid-session (logout race) this gives us one last shot.
297
+ try {
298
+ const storedUser = await asyncLocalStorage<User>(
299
+ localStorageConstants.ETHORA_USER
300
+ ).get();
301
+ if (
302
+ storedUser &&
303
+ (storedUser.token || storedUser.refreshToken || storedUser.xmppPassword)
304
+ ) {
305
+ const hydrated = await tryHydrateViaMy(
306
+ storedUser,
307
+ myEndpoint,
308
+ signal
309
+ ).catch(() => null);
310
+ if (hydrated && hasXmppCredentials(hydrated)) {return hydrated;}
311
+ }
312
+ } catch {
313
+ // ignore storage errors
314
+ }
315
+
316
+ return null;
317
+ };
318
+
240
319
  export default resolveInitBeforeLoadUser;
@@ -119,10 +119,8 @@ const useChatWrapperInit = ({
119
119
  user.xmppUsername || user?.defaultWallet?.walletAddress,
120
120
  user?.xmppPassword,
121
121
  config?.xmppSettings
122
- ).then((client) => {
123
- console.log(' Client initialized', client?.status);
124
- return client;
125
- });
122
+ );
123
+ console.log(' Client initialized', newClient?.status);
126
124
 
127
125
  if (roomsList && Object.keys(roomsList).length > 0) {
128
126
  console.log(
@@ -156,27 +154,29 @@ const useChatWrapperInit = ({
156
154
  await newClient.getRoomsStanza();
157
155
  }
158
156
  }
159
- await newClient
160
- .getChatsPrivateStoreRequestStanza()
161
- .then(
162
- async (
163
- roomTimestampObject: any
164
- ) => {
165
- console.log(
166
- 'Got chats private store',
167
- roomTimestampObject?.length || 0
168
- );
169
- updatedChatLastTimestamps(roomTimestampObject, dispatch);
170
- if (!hasSyncedHistoryRef.current) {
171
- console.log('Updating messages till last');
172
- await updateMessagesTillLast(rooms, newClient);
173
- hasSyncedHistoryRef.current = true;
174
- }
175
- setClient(newClient);
176
- setConnectionLost(false);
177
- console.log('Initialization complete');
178
- }
157
+ try {
158
+ const roomTimestampObject: any =
159
+ await newClient.getChatsPrivateStoreRequestStanza();
160
+ console.log(
161
+ 'Got chats private store',
162
+ roomTimestampObject?.length || 0
179
163
  );
164
+ if (roomTimestampObject) {
165
+ updatedChatLastTimestamps(roomTimestampObject, dispatch);
166
+ }
167
+ if (!hasSyncedHistoryRef.current) {
168
+ console.log('Updating messages till last');
169
+ await updateMessagesTillLast(rooms, newClient);
170
+ hasSyncedHistoryRef.current = true;
171
+ }
172
+ setClient(newClient);
173
+ setConnectionLost(false);
174
+ console.log('Initialization complete');
175
+ } catch (err) {
176
+ console.warn('privateStore/timestamps failed', err);
177
+ setClient(newClient);
178
+ setConnectionLost(false);
179
+ }
180
180
 
181
181
  {
182
182
  config?.refreshTokens?.enabled && refresh();
@@ -204,29 +204,34 @@ const useChatWrapperInit = ({
204
204
  }
205
205
  setInited(true);
206
206
  console.log(' Getting chats private store for existing client');
207
- await client
208
- .getChatsPrivateStoreRequestStanza()
209
- .then(
210
- async (
211
- roomTimestampObject: any
212
- ) => {
213
- console.log(
214
- 'Got chats private store for existing client',
215
- roomTimestampObject?.length || 0
216
- );
217
- updatedChatLastTimestamps(roomTimestampObject, dispatch);
218
- if (!hasSyncedHistoryRef.current) {
219
- console.log(
220
- 'Updating messages till last for existing client'
221
- );
222
- await updateMessagesTillLast(rooms, client);
223
- hasSyncedHistoryRef.current = true;
224
- }
225
- setClient(client);
226
- setConnectionLost(false);
227
- console.log(' Initialization complete for existing client');
228
- }
207
+ try {
208
+ const roomTimestampObject: any =
209
+ await client.getChatsPrivateStoreRequestStanza();
210
+ console.log(
211
+ 'Got chats private store for existing client',
212
+ roomTimestampObject?.length || 0
229
213
  );
214
+ if (roomTimestampObject) {
215
+ updatedChatLastTimestamps(roomTimestampObject, dispatch);
216
+ }
217
+ if (!hasSyncedHistoryRef.current) {
218
+ console.log(
219
+ 'Updating messages till last for existing client'
220
+ );
221
+ await updateMessagesTillLast(rooms, client);
222
+ hasSyncedHistoryRef.current = true;
223
+ }
224
+ setClient(client);
225
+ setConnectionLost(false);
226
+ console.log(' Initialization complete for existing client');
227
+ } catch (err) {
228
+ console.warn(
229
+ 'privateStore/timestamps failed (existing client)',
230
+ err
231
+ );
232
+ setClient(client);
233
+ setConnectionLost(false);
234
+ }
230
235
  {
231
236
  config?.refreshTokens?.enabled && refresh();
232
237
  }
@@ -23,10 +23,17 @@ interface MessageEditedEvent {
23
23
  user: any;
24
24
  }
25
25
 
26
+ interface MessageRetryEvent {
27
+ messageId: string;
28
+ roomJID: string;
29
+ messageType: 'text' | 'media';
30
+ }
31
+
26
32
  export interface EventHandlersHook {
27
33
  handleMessageSent: (event: MessageSentEvent) => Promise<void>;
28
34
  handleMessageFailed: (event: MessageFailedEvent) => void;
29
35
  handleMessageEdited: (event: MessageEditedEvent) => void;
36
+ handleMessageRetry: (event: MessageRetryEvent) => void;
30
37
  }
31
38
 
32
39
  export const useEventHandlers = (config?: IConfig): EventHandlersHook => {
@@ -77,5 +84,21 @@ export const useEventHandlers = (config?: IConfig): EventHandlersHook => {
77
84
  [config]
78
85
  );
79
86
 
80
- return { handleMessageSent, handleMessageFailed, handleMessageEdited };
87
+ const handleMessageRetry = useCallback(
88
+ (event: MessageRetryEvent) => {
89
+ try {
90
+ config?.eventHandlers?.onMessageRetry?.(event);
91
+ } catch (error) {
92
+ console.error('Error in message retry handler:', error);
93
+ }
94
+ },
95
+ [config]
96
+ );
97
+
98
+ return {
99
+ handleMessageSent,
100
+ handleMessageFailed,
101
+ handleMessageEdited,
102
+ handleMessageRetry,
103
+ };
81
104
  };
@@ -3,9 +3,13 @@ import { RootState } from '../roomStore';
3
3
 
4
4
  export const useMessageHeapState = () => {
5
5
  const queue = useSelector(
6
- (state: RootState) => state.roomHeapSlice.messageHeap
6
+ (state: RootState) => state.roomHeapSlice?.messageHeap
7
+ );
8
+ const failedMessages = useSelector(
9
+ (state: RootState) => state.roomHeapSlice?.failedMessages
7
10
  );
8
11
  const idSet = new Set(queue?.map((m) => m.id) ?? []);
12
+ const failedIdSet = new Set(Object.keys(failedMessages || {}));
9
13
 
10
- return { queue, idSet };
14
+ return { queue, idSet, failedMessages, failedIdSet };
11
15
  };
@@ -3,7 +3,11 @@ import { v4 as uuidv4 } from 'uuid';
3
3
  import { useXmppClient } from '../context/xmppProvider';
4
4
  import { useDispatch, useSelector } from 'react-redux';
5
5
  import { addRoomMessage, setEditAction } from '../roomStore/roomsSlice';
6
- import { addMessageToHeap } from '../roomStore/roomHeapSlice';
6
+ import {
7
+ addMessageToHeap,
8
+ clearMessageFailure,
9
+ markMessageFailed,
10
+ } from '../roomStore/roomHeapSlice';
7
11
  import { uploadFile } from '../networking/api-requests/auth.api';
8
12
  import { RootState } from '../roomStore';
9
13
  import { useEventHandlers } from './useEventHandlers';
@@ -38,8 +42,13 @@ export const useSendMessage = (_configOverride?: IConfig) => {
38
42
  handleMessageSent,
39
43
  handleMessageFailed,
40
44
  handleMessageEdited,
45
+ handleMessageRetry,
41
46
  } = useEventHandlers(config);
42
47
 
48
+ const failedMessages = useSelector(
49
+ (state: RootState) => state.roomHeapSlice?.failedMessages || {}
50
+ );
51
+
43
52
  const sendMessage = useCallback(
44
53
  async (
45
54
  message: string,
@@ -140,6 +149,17 @@ export const useSendMessage = (_configOverride?: IConfig) => {
140
149
  messageType: 'text',
141
150
  });
142
151
  } catch (error) {
152
+ dispatch(
153
+ markMessageFailed({
154
+ kind: 'text',
155
+ id: optimisticId,
156
+ roomJID: activeRoomJID,
157
+ body: message,
158
+ isReply,
159
+ isChecked,
160
+ mainMessage,
161
+ })
162
+ );
143
163
  handleMessageFailed({
144
164
  message,
145
165
  roomJID: activeRoomJID,
@@ -158,11 +178,12 @@ export const useSendMessage = (_configOverride?: IConfig) => {
158
178
  activeRoomJID: string,
159
179
  isReply: boolean = false,
160
180
  isChecked: boolean = false,
161
- mainMessage: string = ''
181
+ mainMessage: string = '',
182
+ existingId?: string
162
183
  ) => {
163
184
  client?.onCriticalSend?.(activeRoomJID);
164
185
 
165
- const id = `send-media-message:${uuidv4()}`;
186
+ const id = existingId || `send-media-message:${uuidv4()}`;
166
187
  const optimisticTimestamp = Date.now();
167
188
  const optimisticDate = new Date(optimisticTimestamp).toISOString();
168
189
  const selfId =
@@ -171,7 +192,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
171
192
  `${user?.firstName || ''} ${user?.lastName || ''}`.trim() || selfId;
172
193
  const fileSizeStr = data?.size != null ? String(data.size) : '';
173
194
 
174
- if (!config?.disableSentLogic) {
195
+ if (!config?.disableSentLogic && !existingId) {
175
196
  dispatch(
176
197
  addRoomMessage({
177
198
  roomJID: activeRoomJID,
@@ -290,6 +311,18 @@ export const useSendMessage = (_configOverride?: IConfig) => {
290
311
  requestUrl: error?.config?.url,
291
312
  axiosMessage: error?.message,
292
313
  });
314
+ dispatch(
315
+ markMessageFailed({
316
+ kind: 'media',
317
+ id,
318
+ roomJID: activeRoomJID,
319
+ data,
320
+ type,
321
+ isReply,
322
+ isChecked,
323
+ mainMessage,
324
+ })
325
+ );
293
326
  handleMessageFailed({
294
327
  message: 'media',
295
328
  roomJID: activeRoomJID,
@@ -301,6 +334,43 @@ export const useSendMessage = (_configOverride?: IConfig) => {
301
334
  [client, config, user, dispatch, handleMessageSent, handleMessageFailed]
302
335
  );
303
336
 
337
+ // Retry a previously-failed send by replaying the saved payload from
338
+ // the heap. Clears the failure flag first so the bubble flips back to
339
+ // "sending..."; if the retry fails again `markMessageFailed` re-arms
340
+ // it. No-op when the id isn't in the failed map.
341
+ const retryMessage = useCallback(
342
+ async (failedId: string) => {
343
+ const payload = failedMessages[failedId];
344
+ if (!payload) {return;}
345
+ dispatch(clearMessageFailure(failedId));
346
+ handleMessageRetry({
347
+ messageId: failedId,
348
+ roomJID: payload.roomJID,
349
+ messageType: payload.kind,
350
+ });
351
+ if (payload.kind === 'text') {
352
+ await sendMessage(
353
+ payload.body,
354
+ payload.roomJID,
355
+ payload.isReply,
356
+ payload.isChecked,
357
+ payload.mainMessage
358
+ );
359
+ } else {
360
+ await sendMedia(
361
+ payload.data,
362
+ payload.type,
363
+ payload.roomJID,
364
+ payload.isReply,
365
+ payload.isChecked,
366
+ payload.mainMessage,
367
+ failedId
368
+ );
369
+ }
370
+ },
371
+ [failedMessages, dispatch, handleMessageRetry, sendMessage, sendMedia]
372
+ );
373
+
304
374
  // ChatRoom/ThreadWrapper consume this as the "edit branch" of send.
305
375
  // It mirrors the edit path inside `sendMessage` so callers can wire it
306
376
  // up directly when `editAction.isEdit` is true.
@@ -355,6 +425,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
355
425
  sendMessage,
356
426
  sendMedia,
357
427
  sendEditMessage,
428
+ retryMessage,
358
429
  isLastMessageFromUserAndProcessing,
359
430
  };
360
431
  };
@@ -134,7 +134,11 @@ export function uploadFile(formData: FormData) {
134
134
  headers: {
135
135
  Authorization: token,
136
136
  Accept: '*/*',
137
- 'Content-Type': 'multipart/form-data',
137
+ 'Content-Type': null,
138
+ },
139
+ transformRequest: (data: any, headers: any) => {
140
+ if (headers?.delete) {headers.delete('Content-Type');}
141
+ return data;
138
142
  },
139
143
  });
140
144
  }