@ethora/chat-component-rn 26.6.11 → 26.6.13

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 (231) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -2
  3. package/lib/commonjs/components/AuthForms/Login.js +2 -1
  4. package/lib/commonjs/components/AuthForms/Login.js.map +1 -1
  5. package/lib/commonjs/components/MainComponents/ChatHeader.js +4 -1
  6. package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/ChatWrapper.js +14 -13
  8. package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
  9. package/lib/commonjs/components/MainComponents/LoginWrapper.js +2 -1
  10. package/lib/commonjs/components/MainComponents/LoginWrapper.js.map +1 -1
  11. package/lib/commonjs/components/MainComponents/MediaMessage.js +18 -7
  12. package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
  13. package/lib/commonjs/components/MessageBubble/InteractionsOverlay.js +1 -1
  14. package/lib/commonjs/components/MessageBubble/InteractionsOverlay.js.map +1 -1
  15. package/lib/commonjs/components/MessageBubble/Message.js +1 -1
  16. package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
  17. package/lib/commonjs/components/MessageBubble/MessageInteractions.js +1 -1
  18. package/lib/commonjs/components/MessageBubble/MessageInteractions.js.map +1 -1
  19. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +4 -1
  20. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  21. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +15 -11
  22. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  23. package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js +1 -1
  24. package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -1
  25. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js +2 -1
  26. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  27. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js +5 -1
  28. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js.map +1 -1
  29. package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js +10 -2
  30. package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
  31. package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js +4 -1
  32. package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
  33. package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js +4 -1
  34. package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
  35. package/lib/commonjs/components/VideoCalls/VideoCallSession.js +6 -3
  36. package/lib/commonjs/components/VideoCalls/VideoCallSession.js.map +1 -1
  37. package/lib/commonjs/components/styled/AudioMessage.js +2 -1
  38. package/lib/commonjs/components/styled/AudioMessage.js.map +1 -1
  39. package/lib/commonjs/components/styled/MediaFilePreview.js +1 -1
  40. package/lib/commonjs/components/styled/MediaFilePreview.js.map +1 -1
  41. package/lib/commonjs/components/styled/MessageImage.js +34 -8
  42. package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
  43. package/lib/commonjs/components/styled/VideoMessage.js +36 -64
  44. package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
  45. package/lib/commonjs/context/xmppProvider.js +44 -18
  46. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  47. package/lib/commonjs/helpers/initXmppRooms.js +5 -1
  48. package/lib/commonjs/helpers/initXmppRooms.js.map +1 -1
  49. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js +126 -12
  50. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  51. package/lib/commonjs/helpers/secureFileUrl.js +101 -0
  52. package/lib/commonjs/helpers/secureFileUrl.js.map +1 -0
  53. package/lib/commonjs/hooks/useChatWrapperInit.js +3 -3
  54. package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
  55. package/lib/commonjs/hooks/useFileToken.js +10 -0
  56. package/lib/commonjs/hooks/useFileToken.js.map +1 -0
  57. package/lib/commonjs/hooks/useSendMessage.js +23 -13
  58. package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
  59. package/lib/commonjs/main.js +25 -0
  60. package/lib/commonjs/main.js.map +1 -1
  61. package/lib/commonjs/networking/api-requests/auth.api.js +99 -54
  62. package/lib/commonjs/networking/api-requests/auth.api.js.map +1 -1
  63. package/lib/commonjs/networking/api-requests/roomMembers.api.js +1 -1
  64. package/lib/commonjs/networking/api-requests/roomMembers.api.js.map +1 -1
  65. package/lib/commonjs/networking/api-requests/rooms.api.js +9 -9
  66. package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
  67. package/lib/commonjs/networking/api-requests/user.api.js +9 -9
  68. package/lib/commonjs/networking/api-requests/user.api.js.map +1 -1
  69. package/lib/commonjs/networking/apiClient.js +62 -112
  70. package/lib/commonjs/networking/apiClient.js.map +1 -1
  71. package/lib/commonjs/networking/authRefresh.js +323 -0
  72. package/lib/commonjs/networking/authRefresh.js.map +1 -0
  73. package/lib/commonjs/networking/xmppClient.js +33 -5
  74. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  75. package/lib/commonjs/roomStore/chatSettingsSlice.js +20 -2
  76. package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
  77. package/lib/module/components/AuthForms/Login.js +2 -1
  78. package/lib/module/components/AuthForms/Login.js.map +1 -1
  79. package/lib/module/components/MainComponents/ChatHeader.js +4 -1
  80. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  81. package/lib/module/components/MainComponents/ChatWrapper.js +14 -13
  82. package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
  83. package/lib/module/components/MainComponents/LoginWrapper.js +2 -1
  84. package/lib/module/components/MainComponents/LoginWrapper.js.map +1 -1
  85. package/lib/module/components/MainComponents/MediaMessage.js +18 -7
  86. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  87. package/lib/module/components/MessageBubble/InteractionsOverlay.js +1 -1
  88. package/lib/module/components/MessageBubble/InteractionsOverlay.js.map +1 -1
  89. package/lib/module/components/MessageBubble/Message.js +1 -1
  90. package/lib/module/components/MessageBubble/Message.js.map +1 -1
  91. package/lib/module/components/MessageBubble/MessageInteractions.js +1 -1
  92. package/lib/module/components/MessageBubble/MessageInteractions.js.map +1 -1
  93. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +4 -1
  94. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  95. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +15 -11
  96. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  97. package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js +1 -1
  98. package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -1
  99. package/lib/module/components/Modals/FilePreviewModal/PdfView.js +2 -1
  100. package/lib/module/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  101. package/lib/module/components/RoomComponents/ChatRoomItem.js +5 -1
  102. package/lib/module/components/RoomComponents/ChatRoomItem.js.map +1 -1
  103. package/lib/module/components/RoomComponents/styled/LastMessageFile.js +10 -2
  104. package/lib/module/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
  105. package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js +4 -1
  106. package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
  107. package/lib/module/components/RoomComponents/styled/LastMessageVideo.js +4 -1
  108. package/lib/module/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
  109. package/lib/module/components/VideoCalls/VideoCallSession.js +6 -3
  110. package/lib/module/components/VideoCalls/VideoCallSession.js.map +1 -1
  111. package/lib/module/components/styled/AudioMessage.js +2 -1
  112. package/lib/module/components/styled/AudioMessage.js.map +1 -1
  113. package/lib/module/components/styled/MediaFilePreview.js +1 -1
  114. package/lib/module/components/styled/MediaFilePreview.js.map +1 -1
  115. package/lib/module/components/styled/MessageImage.js +34 -8
  116. package/lib/module/components/styled/MessageImage.js.map +1 -1
  117. package/lib/module/components/styled/VideoMessage.js +36 -64
  118. package/lib/module/components/styled/VideoMessage.js.map +1 -1
  119. package/lib/module/context/xmppProvider.js +43 -18
  120. package/lib/module/context/xmppProvider.js.map +1 -1
  121. package/lib/module/helpers/initXmppRooms.js +5 -1
  122. package/lib/module/helpers/initXmppRooms.js.map +1 -1
  123. package/lib/module/helpers/resolveInitBeforeLoadUser.js +126 -11
  124. package/lib/module/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  125. package/lib/module/helpers/secureFileUrl.js +91 -0
  126. package/lib/module/helpers/secureFileUrl.js.map +1 -0
  127. package/lib/module/hooks/useChatWrapperInit.js +3 -3
  128. package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
  129. package/lib/module/hooks/useFileToken.js +5 -0
  130. package/lib/module/hooks/useFileToken.js.map +1 -0
  131. package/lib/module/hooks/useSendMessage.js +24 -14
  132. package/lib/module/hooks/useSendMessage.js.map +1 -1
  133. package/lib/module/main.js +12 -1
  134. package/lib/module/main.js.map +1 -1
  135. package/lib/module/networking/api-requests/auth.api.js +96 -53
  136. package/lib/module/networking/api-requests/auth.api.js.map +1 -1
  137. package/lib/module/networking/api-requests/roomMembers.api.js +1 -1
  138. package/lib/module/networking/api-requests/roomMembers.api.js.map +1 -1
  139. package/lib/module/networking/api-requests/rooms.api.js +9 -9
  140. package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
  141. package/lib/module/networking/api-requests/user.api.js +8 -7
  142. package/lib/module/networking/api-requests/user.api.js.map +1 -1
  143. package/lib/module/networking/apiClient.js +61 -113
  144. package/lib/module/networking/apiClient.js.map +1 -1
  145. package/lib/module/networking/authRefresh.js +312 -0
  146. package/lib/module/networking/authRefresh.js.map +1 -0
  147. package/lib/module/networking/xmppClient.js +33 -5
  148. package/lib/module/networking/xmppClient.js.map +1 -1
  149. package/lib/module/roomStore/chatSettingsSlice.js +20 -2
  150. package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
  151. package/lib/typescript/components/AuthForms/Login.d.ts.map +1 -1
  152. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  153. package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
  154. package/lib/typescript/components/MainComponents/LoginWrapper.d.ts.map +1 -1
  155. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  156. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  157. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  158. package/lib/typescript/components/Modals/FilePreviewModal/PdfView.d.ts.map +1 -1
  159. package/lib/typescript/components/RoomComponents/ChatRoomItem.d.ts.map +1 -1
  160. package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts.map +1 -1
  161. package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts.map +1 -1
  162. package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts.map +1 -1
  163. package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts.map +1 -1
  164. package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
  165. package/lib/typescript/components/styled/MessageImage.d.ts +8 -0
  166. package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
  167. package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
  168. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  169. package/lib/typescript/helpers/initXmppRooms.d.ts.map +1 -1
  170. package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts.map +1 -1
  171. package/lib/typescript/helpers/secureFileUrl.d.ts +7 -0
  172. package/lib/typescript/helpers/secureFileUrl.d.ts.map +1 -0
  173. package/lib/typescript/hooks/useCustomTypingIndicator.d.ts +16 -4
  174. package/lib/typescript/hooks/useCustomTypingIndicator.d.ts.map +1 -1
  175. package/lib/typescript/hooks/useFileToken.d.ts +2 -0
  176. package/lib/typescript/hooks/useFileToken.d.ts.map +1 -0
  177. package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
  178. package/lib/typescript/main.d.ts +6 -1
  179. package/lib/typescript/main.d.ts.map +1 -1
  180. package/lib/typescript/networking/api-requests/auth.api.d.ts +3 -27
  181. package/lib/typescript/networking/api-requests/auth.api.d.ts.map +1 -1
  182. package/lib/typescript/networking/api-requests/user.api.d.ts.map +1 -1
  183. package/lib/typescript/networking/apiClient.d.ts +12 -6
  184. package/lib/typescript/networking/apiClient.d.ts.map +1 -1
  185. package/lib/typescript/networking/authRefresh.d.ts +92 -0
  186. package/lib/typescript/networking/authRefresh.d.ts.map +1 -0
  187. package/lib/typescript/networking/xmppClient.d.ts +6 -0
  188. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  189. package/lib/typescript/roomStore/chatSettingsSlice.d.ts +16 -0
  190. package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
  191. package/lib/typescript/types/types.d.ts +17 -0
  192. package/lib/typescript/types/types.d.ts.map +1 -1
  193. package/package.json +29 -28
  194. package/src/components/AuthForms/Login.tsx +1 -0
  195. package/src/components/MainComponents/ChatHeader.tsx +4 -1
  196. package/src/components/MainComponents/ChatWrapper.tsx +14 -19
  197. package/src/components/MainComponents/LoginWrapper.tsx +1 -0
  198. package/src/components/MainComponents/MediaMessage.tsx +15 -5
  199. package/src/components/MessageBubble/InteractionsOverlay.tsx +1 -1
  200. package/src/components/MessageBubble/Message.tsx +1 -1
  201. package/src/components/MessageBubble/MessageInteractions.tsx +1 -1
  202. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +4 -1
  203. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +14 -10
  204. package/src/components/Modals/FilePreviewModal/GViewWebView.tsx +1 -1
  205. package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -1
  206. package/src/components/RoomComponents/ChatRoomItem.tsx +9 -1
  207. package/src/components/RoomComponents/styled/LastMessageFile.tsx +14 -2
  208. package/src/components/RoomComponents/styled/LastMessagePhoto.tsx +4 -1
  209. package/src/components/RoomComponents/styled/LastMessageVideo.tsx +4 -1
  210. package/src/components/VideoCalls/VideoCallSession.tsx +8 -3
  211. package/src/components/styled/AudioMessage.tsx +2 -1
  212. package/src/components/styled/MediaFilePreview.tsx +1 -1
  213. package/src/components/styled/MessageImage.tsx +45 -6
  214. package/src/components/styled/VideoMessage.tsx +36 -60
  215. package/src/context/xmppProvider.tsx +45 -20
  216. package/src/helpers/initXmppRooms.tsx +5 -1
  217. package/src/helpers/resolveInitBeforeLoadUser.ts +123 -11
  218. package/src/helpers/secureFileUrl.ts +103 -0
  219. package/src/hooks/useChatWrapperInit.ts +3 -3
  220. package/src/hooks/useFileToken.ts +7 -0
  221. package/src/hooks/useSendMessage.tsx +28 -13
  222. package/src/main.ts +22 -1
  223. package/src/networking/api-requests/auth.api.ts +125 -52
  224. package/src/networking/api-requests/roomMembers.api.ts +1 -1
  225. package/src/networking/api-requests/rooms.api.ts +9 -9
  226. package/src/networking/api-requests/user.api.ts +8 -7
  227. package/src/networking/apiClient.ts +68 -123
  228. package/src/networking/authRefresh.ts +435 -0
  229. package/src/networking/xmppClient.ts +37 -5
  230. package/src/roomStore/chatSettingsSlice.ts +28 -3
  231. package/src/types/types.ts +17 -0
@@ -0,0 +1,103 @@
1
+ import { store } from '../roomStore';
2
+ import { getCurrentBaseURL } from '../networking/apiClient';
3
+ import { refreshAuthTokens } from '../networking/authRefresh';
4
+
5
+ const SECURE_FILES_HOST_PREFIX = 'secure-files.';
6
+ const SECURE_MEDIA_PATH_PREFIX = '/secure-media/';
7
+
8
+ const FILE_TOKEN_PARAM = 'ft';
9
+
10
+ export const resolveFileUrl = (url?: string | null): string => {
11
+ if (!url) {return '';}
12
+ // Any scheme at all — http(s), data, file, content, blob.
13
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url)) {return url;}
14
+ if (!url.startsWith('/')) {return url;}
15
+
16
+ const base = getCurrentBaseURL().replace(/\/+$/, '');
17
+ return base ? `${base}${url}` : url;
18
+ };
19
+
20
+ export const isSecureFileUrl = (url?: string | null): boolean => {
21
+ if (!url) {return false;}
22
+
23
+ const path = url.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/i, '');
24
+ if (path.startsWith(SECURE_MEDIA_PATH_PREFIX)) {return true;}
25
+
26
+ const match = /^https?:\/\/(?:[^@/]+@)?([^:/?#]+)/i.exec(url);
27
+ if (!match) {return false;}
28
+ return match[1].toLowerCase().startsWith(SECURE_FILES_HOST_PREFIX);
29
+ };
30
+
31
+ export const appendFileToken = (
32
+ url: string | null | undefined,
33
+ fileToken: string | null | undefined
34
+ ): string => {
35
+ if (!url) {return '';}
36
+
37
+ const resolved = resolveFileUrl(url);
38
+ if (!fileToken || !isSecureFileUrl(resolved)) {return resolved;}
39
+ url = resolved;
40
+
41
+ const hashIndex = url.indexOf('#');
42
+ const hash = hashIndex === -1 ? '' : url.slice(hashIndex);
43
+ const withoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);
44
+
45
+ const queryIndex = withoutHash.indexOf('?');
46
+ const base = queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex);
47
+ const query = queryIndex === -1 ? '' : withoutHash.slice(queryIndex + 1);
48
+
49
+ const params = query
50
+ .split('&')
51
+ .filter(Boolean)
52
+ .filter((pair) => pair.split('=')[0] !== FILE_TOKEN_PARAM);
53
+
54
+ params.push(`${FILE_TOKEN_PARAM}=${encodeURIComponent(fileToken)}`);
55
+
56
+ return `${base}?${params.join('&')}${hash}`;
57
+ };
58
+
59
+ export const withFileToken = (url?: string | null): string =>
60
+ appendFileToken(
61
+ url,
62
+ store.getState().chatSettingStore?.user?.fileToken || ''
63
+ );
64
+
65
+ const RECOVERY_COOLDOWN_MS = 30_000;
66
+ let recoveryInFlight: Promise<boolean> | null = null;
67
+ let lastRecoveryAt = 0;
68
+
69
+ export const requestFileTokenRecovery = (): Promise<boolean> => {
70
+ if (recoveryInFlight) {return recoveryInFlight;}
71
+
72
+ const now = Date.now();
73
+ if (now - lastRecoveryAt < RECOVERY_COOLDOWN_MS) {
74
+ return Promise.resolve(false);
75
+ }
76
+ lastRecoveryAt = now;
77
+
78
+ const state = store.getState();
79
+ const refreshConfig = state.chatSettingStore?.config?.refreshTokens;
80
+ const user = state.chatSettingStore?.user;
81
+
82
+ if (!refreshConfig?.enabled) {return Promise.resolve(false);}
83
+
84
+ const run = async (): Promise<boolean> => {
85
+ if (!refreshConfig.refreshFunction && !user?.refreshToken) {return false;}
86
+
87
+ const tokens = await refreshAuthTokens();
88
+ return Boolean(tokens.fileToken);
89
+ };
90
+
91
+ recoveryInFlight = run()
92
+ .catch(() => false)
93
+ .finally(() => {
94
+ recoveryInFlight = null;
95
+ });
96
+
97
+ return recoveryInFlight;
98
+ };
99
+
100
+ export const __resetFileTokenRecoveryForTests = (): void => {
101
+ recoveryInFlight = null;
102
+ lastRecoveryAt = 0;
103
+ };
@@ -10,7 +10,7 @@ import { chatAutoEnterer } from '../helpers/chatAutoEnterer';
10
10
  import { initRoomsPresence } from '../helpers/initRoomsPresence';
11
11
  import { updatedChatLastTimestamps } from '../helpers/updatedChatLastTimestamps';
12
12
  import { updateMessagesTillLast } from '../helpers/updateMessagesTillLast';
13
- import { refresh } from '../networking/apiClient';
13
+ import { refreshAuthTokensQuietly } from '../networking/authRefresh';
14
14
  import { setLangSource, setConfig } from '../roomStore/chatSettingsSlice';
15
15
  import { setIsLoading } from '../roomStore/roomsSlice';
16
16
  import { useRoomState } from './useRoomState';
@@ -179,7 +179,7 @@ const useChatWrapperInit = ({
179
179
  }
180
180
 
181
181
  {
182
- config?.refreshTokens?.enabled && refresh();
182
+ config?.refreshTokens?.enabled && refreshAuthTokensQuietly();
183
183
  }
184
184
  } catch (error) {
185
185
  console.error(' Error during initialization', error);
@@ -233,7 +233,7 @@ const useChatWrapperInit = ({
233
233
  setConnectionLost(false);
234
234
  }
235
235
  {
236
- config?.refreshTokens?.enabled && refresh();
236
+ config?.refreshTokens?.enabled && refreshAuthTokensQuietly();
237
237
  }
238
238
  }
239
239
  }
@@ -0,0 +1,7 @@
1
+ import { useSelector } from 'react-redux';
2
+ import { RootState } from '../roomStore';
3
+
4
+ export const useFileToken = (): string =>
5
+ useSelector(
6
+ (state: RootState) => state.chatSettingStore.user?.fileToken || ''
7
+ );
@@ -9,7 +9,7 @@ import {
9
9
  clearMessageFailure,
10
10
  markMessageFailed,
11
11
  } from '../roomStore/roomHeapSlice';
12
- import { uploadFileMultipart } from '../networking/api-requests/auth.api';
12
+ import { uploadFileV2 } from '../networking/api-requests/auth.api';
13
13
  import { enqueueOutboundSend } from '../networking/outboundQueue';
14
14
  import { RootState, store } from '../roomStore';
15
15
  import { getGlobalXmppClient } from '../utils/clientRegistry';
@@ -182,13 +182,34 @@ export const useSendMessage = (_configOverride?: IConfig) => {
182
182
  // PENDING_WATCHDOG_MS, flip the bubble to "failed → tap to
183
183
  // retry". We check at fire-time whether the message is still
184
184
  // pending and not already marked failed by the catch path.
185
- const watchdogTimer = setTimeout(() => {
185
+ // One-shot extension for the case below. Declared outside the
186
+ // timer so the rescheduled run can't extend again.
187
+ let watchdogExtended = false;
188
+ const runWatchdog = () => {
186
189
  const state = reduxStore.getState();
187
190
  const roomMsgs = state.rooms?.rooms?.[activeRoomJID]?.messages || [];
188
191
  const stillPending = roomMsgs.find(
189
192
  (m: any) => m.id === optimisticId && m.pending
190
193
  );
191
194
  const alreadyFailed = state.roomHeapSlice?.failedMessages?.[optimisticId];
195
+
196
+ // Don't call it a failure while the stream is still coming back.
197
+ // A reconnect tears the old client down and builds a new one, and
198
+ // any echo in flight during that window is simply lost — the
199
+ // message may well have been delivered (it has been observed
200
+ // arriving twice on the receiving side). Failing it here shows a
201
+ // red "tap to retry" under a message that actually went through.
202
+ // Give the reconnect one more window to produce an echo, once.
203
+ const streamOnline = getGlobalXmppClient()?.status === 'online';
204
+ if (stillPending && !alreadyFailed && !streamOnline && !watchdogExtended) {
205
+ watchdogExtended = true;
206
+ console.warn(
207
+ `Send watchdog — ${optimisticId} still pending but the stream is not online; extending one window`
208
+ );
209
+ watchdogTimer = setTimeout(runWatchdog, PENDING_WATCHDOG_MS);
210
+ return;
211
+ }
212
+
192
213
  if (stillPending && !alreadyFailed) {
193
214
  console.warn(
194
215
  `Send watchdog fired — message ${optimisticId} stuck pending > ${PENDING_WATCHDOG_MS}ms; marking failed`
@@ -211,7 +232,9 @@ export const useSendMessage = (_configOverride?: IConfig) => {
211
232
  messageType: 'text',
212
233
  });
213
234
  }
214
- }, PENDING_WATCHDOG_MS);
235
+ };
236
+
237
+ let watchdogTimer = setTimeout(runWatchdog, PENDING_WATCHDOG_MS);
215
238
 
216
239
  try {
217
240
  // The single send closure, used both for the immediate send and for
@@ -467,19 +490,11 @@ export const useSendMessage = (_configOverride?: IConfig) => {
467
490
  name: data?.name || data?.fileName || `media_${Date.now()}`,
468
491
  };
469
492
 
470
- // Mirror the web client: a single multipart POST to /files/ with
471
- // the file under the plural "files" field. We send it via fetch
472
- // (not axios) because RN's fetch sets the multipart boundary on the
473
- // Content-Type header correctly. The previous axios fallback omitted
474
- // a valid boundary, so the server mis-parsed one file as many and
475
- // rejected the request with HTTP 413 "TOO_MANY_FILES". The only
476
- // retry is the documented singular-"file" fallback for deployments
477
- // that 500 on the plural field (bug #10).
478
493
  const tryUpload = async () => {
479
494
  const fd = new FormData();
480
495
  fd.append('files', fileBlob as any);
481
496
  try {
482
- return await uploadFileMultipart(fd);
497
+ return await uploadFileV2(fd, activeRoomJID);
483
498
  } catch (err: any) {
484
499
  if (err?.response?.status === 500) {
485
500
  console.warn(
@@ -488,7 +503,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
488
503
  );
489
504
  const fd2 = new FormData();
490
505
  fd2.append('file', fileBlob as any);
491
- return await uploadFileMultipart(fd2);
506
+ return await uploadFileV2(fd2, activeRoomJID);
492
507
  }
493
508
  throw err;
494
509
  }
package/src/main.ts CHANGED
@@ -7,7 +7,10 @@
7
7
  *
8
8
  * <Chat
9
9
  * config={{
10
- * baseUrl: 'https://api.chat.ethora.com/v1',
10
+ * // The API ROOT — the SDK versions every path itself
11
+ * // (`/v1/...`, `/v2/...`). A legacy `.../v1` is accepted and
12
+ * // normalised, so existing integrations keep working.
13
+ * baseUrl: 'https://api.chat.ethora.com',
11
14
  * xmppSettings: { host: 'xmpp.chat.ethora.com' },
12
15
  * jwtLogin: { enabled: true, token: '<paste JWT>' },
13
16
  * initBeforeLoad: true,
@@ -66,3 +69,21 @@ export type {
66
69
  } from './types/types';
67
70
  export type { Iso639_1Codes } from './types/models/language.model';
68
71
  export type { TranslateMode } from './utils/translateModePolicy';
72
+
73
+ // Auth-token rotation. Exposed so hosts can drive a refresh themselves
74
+ // (it is deduped and shares the SDK's lock) and, more importantly, so
75
+ // they can tell a dead session from a transient failure: only
76
+ // `RefreshFatalError` means "log the user out". Every other rejection —
77
+ // network, 5xx, a lost REFRESH_IN_PROGRESS race — must leave the
78
+ // session alone.
79
+ export {
80
+ refreshAuthTokens,
81
+ refreshAuthTokensQuietly,
82
+ RefreshFatalError,
83
+ isRefreshFatalError,
84
+ } from './networking/authRefresh';
85
+ export type {
86
+ RefreshErrorCode,
87
+ RefreshResult,
88
+ RefreshOptions,
89
+ } from './networking/authRefresh';
@@ -18,8 +18,9 @@ export async function loginEmail(email: string, password: string) {
18
18
  user: User;
19
19
  refreshToken: string;
20
20
  token: string;
21
+ fileToken?: string;
21
22
  }>(
22
- '/users/login-with-email',
23
+ '/v1/users/login-with-email',
23
24
  {
24
25
  email,
25
26
  password,
@@ -29,6 +30,16 @@ export async function loginEmail(email: string, password: string) {
29
30
 
30
31
  console.log('loginEmail res', res.data);
31
32
 
33
+ // The fileToken arrives at the TOP level of the login response, next to
34
+ // token/refreshToken — not inside `user`. Folding it in here is what
35
+ // makes secure media (`secure-files.*`, uploaded via /v2/files/secure)
36
+ // render at all: without it every such image 403s and the bubble comes
37
+ // up blank.
38
+ res.data.user = {
39
+ ...res.data.user,
40
+ fileToken: res.data.fileToken || '',
41
+ };
42
+
32
43
  return res;
33
44
  }
34
45
 
@@ -39,7 +50,7 @@ export function loginSocial(
39
50
  authToken: string = 'authToken'
40
51
  ) {
41
52
  return http.post<any>(
42
- '/users/login',
53
+ '/v1/users/login',
43
54
  {
44
55
  idToken,
45
56
  accessToken,
@@ -58,7 +69,7 @@ export function registerSocial(
58
69
  signUpPlan?: string
59
70
  ) {
60
71
  return http.post(
61
- '/users',
72
+ '/v1/users',
62
73
  {
63
74
  idToken,
64
75
  accessToken,
@@ -72,7 +83,7 @@ export function registerSocial(
72
83
 
73
84
  export function checkEmailExist(email: string) {
74
85
  return http.get(
75
- '/users/checkEmail/' + email,
86
+ '/v1/users/checkEmail/' + email,
76
87
 
77
88
  { headers: { Authorization: getCurrentAppToken() } }
78
89
  );
@@ -84,7 +95,8 @@ export async function loginViaJwt(clientToken: string): Promise<User> {
84
95
  user: User;
85
96
  refreshToken: string;
86
97
  token: string;
87
- }>('/users/client', null, { headers: { 'x-custom-token': clientToken } });
98
+ fileToken?: string;
99
+ }>('/v1/users/client', null, { headers: { 'x-custom-token': clientToken } });
88
100
 
89
101
  console.log('✅ loginViaJwt: Received response', {
90
102
  hasUser: !!response.data.user,
@@ -103,6 +115,8 @@ export async function loginViaJwt(clientToken: string): Promise<User> {
103
115
  ...response.data.user,
104
116
  refreshToken: response.data.refreshToken,
105
117
  token: response.data.token,
118
+ // Top-level, same as loginEmail — see the note there.
119
+ fileToken: response.data.fileToken || '',
106
120
  };
107
121
 
108
122
  return user;
@@ -131,7 +145,7 @@ export async function loginViaJwt(clientToken: string): Promise<User> {
131
145
 
132
146
  export function uploadFile(formData: FormData) {
133
147
  const token = store.getState().chatSettingStore?.user?.token ?? '';
134
- return http.post('/files/', formData, {
148
+ return http.post('/v1/files/', formData, {
135
149
  headers: {
136
150
  Authorization: token,
137
151
  Accept: '*/*',
@@ -144,79 +158,91 @@ export function uploadFile(formData: FormData) {
144
158
  });
145
159
  }
146
160
 
147
- const UPLOAD_PATH = '/files/';
148
-
149
- /**
150
- * Multipart upload of a picked file, sent over `XMLHttpRequest`.
151
- *
152
- * The transport matters here, and both obvious alternatives are wrong:
153
- *
154
- * - `fetch`. Expo SDK 54 shipped a WinterCG-compliant fetch and SDK 57
155
- * installs it as the GLOBAL fetch. It accepts only `Blob` / `File` /
156
- * string form parts, so React Native's own `{ uri, type, name }` part
157
- * — which is exactly what an image/document picker hands us — is
158
- * rejected with "Unsupported FormDataPart implementation", surfacing
159
- * to the caller as ERR_NETWORK. A host can set
160
- * `EXPO_PUBLIC_USE_RN_FETCH=1`, but that opts their whole app out of
161
- * Expo's fetch just to satisfy this SDK; not a cost we get to impose
162
- * on a consumer.
163
- * - axios. It forces its own `Content-Type: multipart/form-data`
164
- * WITHOUT a boundary, so the server mis-parses one file as many and
165
- * answers HTTP 413 TOO_MANY_FILES (bug #10 — this is why the upload
166
- * was moved off axios in the first place).
167
- *
168
- * `XMLHttpRequest` avoids both. Expo replaces `fetch`, not `XHR`, so the
169
- * request still goes through React Native's native networking module,
170
- * which understands the `{ uri, type, name }` part, streams the file
171
- * straight from disk (a 50MB video never lands in JS memory) and writes
172
- * its own `Content-Type` with a valid boundary — which is precisely why
173
- * we must NOT set that header ourselves below.
174
- */
175
- export function uploadFileMultipart(formData: FormData): Promise<{ data: any }> {
161
+ const SECURE_UPLOAD_PATH = '/v2/files/secure';
162
+ const LEGACY_UPLOAD_PATH = '/v1/files/';
163
+
164
+ const cloneFormData = (formData: FormData): FormData => {
165
+ const copy = new FormData();
166
+ const source = formData as any;
167
+
168
+ if (Array.isArray(source?._parts)) {
169
+ for (const [key, value] of source._parts) {
170
+ copy.append(key, value);
171
+ }
172
+ return copy;
173
+ }
174
+
175
+ if (typeof source?.forEach === 'function') {
176
+ source.forEach((value: any, key: string) => copy.append(key, value));
177
+ return copy;
178
+ }
179
+
180
+ if (typeof source?.getParts === 'function') {
181
+ for (const part of source.getParts()) {
182
+ // `headers` is derived by getParts() and must not be copied back —
183
+ // RN regenerates it from the value.
184
+ const { fieldName, string, headers: _headers, ...file } = part;
185
+ copy.append(fieldName, string !== undefined ? string : file);
186
+ }
187
+ }
188
+
189
+ return copy;
190
+ };
191
+
192
+ function postMultipart(
193
+ url: string,
194
+ body: FormData,
195
+ path: string
196
+ ): Promise<{ data: any }> {
176
197
  const token = store.getState().chatSettingStore?.user?.token ?? '';
177
- const baseUrl = getCurrentBaseURL();
178
- const url = `${baseUrl.replace(/\/$/, '')}${UPLOAD_PATH}`;
179
198
 
180
199
  pushLog('http', `→ POST ${url} (xhr)`, {
181
- headers: { Authorization: token ? token.slice(0, 16) + '…' : '[empty]', Accept: '*/*' },
200
+ path,
201
+ baseUrl: getCurrentBaseURL() || '[empty]',
202
+ headers: {
203
+ Authorization: token ? token.slice(0, 16) + '…' : '[empty]',
204
+ Accept: '*/*',
205
+ },
182
206
  });
183
207
 
184
208
  return new Promise((resolve, reject) => {
185
209
  const xhr = new XMLHttpRequest();
186
210
 
187
211
  const failNetwork = (reason: string) => {
188
- pushLog('http', `← ERR xhr ${UPLOAD_PATH}`, { reason });
212
+ pushLog('http', `← ERR xhr ${path}`, { reason, url });
189
213
  const err: any = new Error(reason);
190
214
  err.code = 'ERR_NETWORK';
191
- err.config = { url: UPLOAD_PATH };
215
+ err.config = { url: path };
192
216
  reject(err);
193
217
  };
194
218
 
195
219
  xhr.onload = () => {
196
220
  const status = xhr.status;
197
- let body: any = null;
221
+ let responseBody: any = null;
198
222
  try {
199
- body = JSON.parse(xhr.responseText);
223
+ responseBody = JSON.parse(xhr.responseText);
200
224
  } catch {
201
- body = xhr.responseText || null;
225
+ responseBody = xhr.responseText || null;
202
226
  }
203
227
 
204
228
  if (status < 200 || status >= 300) {
205
- pushLog('http', `← ${status} POST ${UPLOAD_PATH} (xhr)`, body);
229
+ pushLog('http', `← ${status} POST ${path} (xhr)`, responseBody);
206
230
  const err: any = new Error(`Upload failed: ${status}`);
207
231
  // Shaped like an axios error so callers' `err.response.status`
208
232
  // checks keep working — notably useSendMessage's retry with the
209
233
  // singular "file" field on 500.
210
- err.response = { status, statusText: xhr.statusText, data: body };
211
- err.config = { url: UPLOAD_PATH };
234
+ err.response = { status, statusText: xhr.statusText, data: responseBody };
235
+ err.config = { url: path };
212
236
  reject(err);
213
237
  return;
214
238
  }
215
239
 
216
- pushLog('http', `← ${status} POST ${UPLOAD_PATH} (xhr)`, {
217
- resultsCount: Array.isArray(body?.results) ? body.results.length : 'n/a',
240
+ pushLog('http', `← ${status} POST ${path} (xhr)`, {
241
+ resultsCount: Array.isArray(responseBody?.results)
242
+ ? responseBody.results.length
243
+ : 'n/a',
218
244
  });
219
- resolve({ data: body });
245
+ resolve({ data: responseBody });
220
246
  };
221
247
 
222
248
  xhr.onerror = () => failNetwork('Network request failed');
@@ -226,8 +252,55 @@ export function uploadFileMultipart(formData: FormData): Promise<{ data: any }>
226
252
  xhr.open('POST', url);
227
253
  xhr.setRequestHeader('Authorization', token);
228
254
  xhr.setRequestHeader('Accept', '*/*');
229
- // Deliberately no Content-Type — see the note above: RN fills it in
230
- // along with the multipart boundary it generated.
231
- xhr.send(formData as any);
255
+ // Deliberately no Content-Type — see the note above.
256
+ xhr.send(body as any);
232
257
  });
233
258
  }
259
+
260
+ const canFallBackToLegacyUpload = (error: unknown): boolean => {
261
+ const status = (error as { response?: { status?: number } })?.response
262
+ ?.status;
263
+ if (typeof status !== 'number') {return true;}
264
+ return status !== 401 && status !== 413;
265
+ };
266
+
267
+ let secureUploadUnavailable = false;
268
+
269
+ export async function uploadFileV2(
270
+ formData: FormData,
271
+ activeRoomJID: string
272
+ ): Promise<{ data: any }> {
273
+ const chatName = (activeRoomJID || '').split('@')[0];
274
+ const apiRoot = getCurrentBaseURL().replace(/\/$/, '');
275
+
276
+ if (!secureUploadUnavailable && chatName) {
277
+ const body = cloneFormData(formData);
278
+ body.append('chatName', chatName);
279
+
280
+ try {
281
+ return await postMultipart(
282
+ `${apiRoot}${SECURE_UPLOAD_PATH}`,
283
+ body,
284
+ SECURE_UPLOAD_PATH
285
+ );
286
+ } catch (error) {
287
+ if (!canFallBackToLegacyUpload(error)) {throw error;}
288
+
289
+ secureUploadUnavailable = true;
290
+ console.warn(
291
+ `[chat] ${SECURE_UPLOAD_PATH} unreachable, falling back to ${LEGACY_UPLOAD_PATH} for this session`,
292
+ error
293
+ );
294
+ }
295
+ }
296
+
297
+ return postMultipart(
298
+ `${apiRoot}${LEGACY_UPLOAD_PATH}`,
299
+ formData,
300
+ LEGACY_UPLOAD_PATH
301
+ );
302
+ }
303
+
304
+ export const __resetSecureUploadLatchForTests = (): void => {
305
+ secureUploadUnavailable = false;
306
+ };
@@ -7,7 +7,7 @@ export async function getUserByXmppUsername(
7
7
  ): Promise<RoomMember | null> {
8
8
  try {
9
9
  const res = await http.get<{ result: RoomMember }>(
10
- `/apps/users/${xmppUsername}`,
10
+ `/v1/apps/users/${xmppUsername}`,
11
11
  {
12
12
  headers: { Authorization: token },
13
13
  }
@@ -181,7 +181,7 @@ export async function getRooms(): Promise<{ items: ApiRoom[] }> {
181
181
 
182
182
  getRoomsInFlightToken = token;
183
183
  getRoomsInFlight = (async () => {
184
- const response = await http.get('/chats/my', {
184
+ const response = await http.get('/v1/chats/my', {
185
185
  headers: { Authorization: token },
186
186
  });
187
187
  lastGetRoomsResponse = response.data;
@@ -206,7 +206,7 @@ export async function getRoomByName(chatName: string): Promise<ApiRoom> {
206
206
  const token = store.getState().chatSettingStore.user.token || '';
207
207
 
208
208
  try {
209
- const response = await http.get(`/chats/my/${chatName}`, {
209
+ const response = await http.get(`/v1/chats/my/${chatName}`, {
210
210
  headers: {
211
211
  Authorization: token,
212
212
  },
@@ -221,7 +221,7 @@ export async function postRoom(data: PostRoom) {
221
221
  const token = store.getState().chatSettingStore.user.token || '';
222
222
 
223
223
  try {
224
- const response = await http.post('/chats', data, {
224
+ const response = await http.post('/v1/chats', data, {
225
225
  headers: {
226
226
  Authorization: token,
227
227
  },
@@ -240,7 +240,7 @@ export async function postPrivateRoom(
240
240
 
241
241
  try {
242
242
  const response = await http.post(
243
- '/chats/private',
243
+ '/v1/chats/private',
244
244
  { username },
245
245
  {
246
246
  headers: {
@@ -260,7 +260,7 @@ export async function postReportRoom(data: PostReportRoom) {
260
260
 
261
261
  try {
262
262
  const response = await http.post(
263
- `/chats/reports/${chatName}`,
263
+ `/v1/chats/reports/${chatName}`,
264
264
  { category, text },
265
265
  {
266
266
  headers: {
@@ -282,7 +282,7 @@ export async function postAddRoomMember(
282
282
 
283
283
  try {
284
284
  const response = await http.post(
285
- `/chats/users-access`,
285
+ `/v1/chats/users-access`,
286
286
  { chatName, members },
287
287
  {
288
288
  headers: {
@@ -301,7 +301,7 @@ export async function deleteRoomMember(data: DeleteRoomMember) {
301
301
  const token = store.getState().chatSettingStore.user.token || '';
302
302
 
303
303
  try {
304
- const response = await http.delete(`/chats/users-access`, {
304
+ const response = await http.delete(`/v1/chats/users-access`, {
305
305
  headers: {
306
306
  Authorization: token,
307
307
  },
@@ -320,7 +320,7 @@ export async function deleteRoom(name: string) {
320
320
  const token = store.getState().chatSettingStore.user.token || '';
321
321
 
322
322
  try {
323
- const response = await http.delete('/chats', {
323
+ const response = await http.delete('/v1/chats', {
324
324
  headers: {
325
325
  Authorization: token,
326
326
  },
@@ -360,7 +360,7 @@ export async function createChatCall(
360
360
 
361
361
  try {
362
362
  await http.post(
363
- `/chats/call/create/${chatName}`,
363
+ `/v1/chats/call/create/${chatName}`,
364
364
  { kind },
365
365
  {
366
366
  headers: {