@ethora/chat-component-rn 26.6.12 → 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 (198) hide show
  1. package/CHANGELOG.md +5 -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 +17 -6
  12. package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
  13. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +4 -1
  14. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  15. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +8 -7
  16. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  17. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js +2 -1
  18. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  19. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js +5 -1
  20. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js.map +1 -1
  21. package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js +10 -2
  22. package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
  23. package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js +4 -1
  24. package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
  25. package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js +4 -1
  26. package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
  27. package/lib/commonjs/components/styled/AudioMessage.js +2 -1
  28. package/lib/commonjs/components/styled/AudioMessage.js.map +1 -1
  29. package/lib/commonjs/components/styled/MessageImage.js +34 -8
  30. package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
  31. package/lib/commonjs/components/styled/VideoMessage.js +7 -1
  32. package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
  33. package/lib/commonjs/context/xmppProvider.js +44 -18
  34. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  35. package/lib/commonjs/helpers/initXmppRooms.js +5 -1
  36. package/lib/commonjs/helpers/initXmppRooms.js.map +1 -1
  37. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js +126 -12
  38. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  39. package/lib/commonjs/helpers/secureFileUrl.js +101 -0
  40. package/lib/commonjs/helpers/secureFileUrl.js.map +1 -0
  41. package/lib/commonjs/hooks/useChatWrapperInit.js +3 -3
  42. package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
  43. package/lib/commonjs/hooks/useFileToken.js +10 -0
  44. package/lib/commonjs/hooks/useFileToken.js.map +1 -0
  45. package/lib/commonjs/hooks/useSendMessage.js +23 -13
  46. package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
  47. package/lib/commonjs/main.js +25 -0
  48. package/lib/commonjs/main.js.map +1 -1
  49. package/lib/commonjs/networking/api-requests/auth.api.js +99 -54
  50. package/lib/commonjs/networking/api-requests/auth.api.js.map +1 -1
  51. package/lib/commonjs/networking/api-requests/roomMembers.api.js +1 -1
  52. package/lib/commonjs/networking/api-requests/roomMembers.api.js.map +1 -1
  53. package/lib/commonjs/networking/api-requests/rooms.api.js +9 -9
  54. package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
  55. package/lib/commonjs/networking/api-requests/user.api.js +9 -9
  56. package/lib/commonjs/networking/api-requests/user.api.js.map +1 -1
  57. package/lib/commonjs/networking/apiClient.js +62 -112
  58. package/lib/commonjs/networking/apiClient.js.map +1 -1
  59. package/lib/commonjs/networking/authRefresh.js +323 -0
  60. package/lib/commonjs/networking/authRefresh.js.map +1 -0
  61. package/lib/commonjs/networking/xmppClient.js +33 -5
  62. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  63. package/lib/commonjs/roomStore/chatSettingsSlice.js +20 -2
  64. package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
  65. package/lib/module/components/AuthForms/Login.js +2 -1
  66. package/lib/module/components/AuthForms/Login.js.map +1 -1
  67. package/lib/module/components/MainComponents/ChatHeader.js +4 -1
  68. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  69. package/lib/module/components/MainComponents/ChatWrapper.js +14 -13
  70. package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
  71. package/lib/module/components/MainComponents/LoginWrapper.js +2 -1
  72. package/lib/module/components/MainComponents/LoginWrapper.js.map +1 -1
  73. package/lib/module/components/MainComponents/MediaMessage.js +17 -6
  74. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  75. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +4 -1
  76. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  77. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +8 -7
  78. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  79. package/lib/module/components/Modals/FilePreviewModal/PdfView.js +2 -1
  80. package/lib/module/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  81. package/lib/module/components/RoomComponents/ChatRoomItem.js +5 -1
  82. package/lib/module/components/RoomComponents/ChatRoomItem.js.map +1 -1
  83. package/lib/module/components/RoomComponents/styled/LastMessageFile.js +10 -2
  84. package/lib/module/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
  85. package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js +4 -1
  86. package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
  87. package/lib/module/components/RoomComponents/styled/LastMessageVideo.js +4 -1
  88. package/lib/module/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
  89. package/lib/module/components/styled/AudioMessage.js +2 -1
  90. package/lib/module/components/styled/AudioMessage.js.map +1 -1
  91. package/lib/module/components/styled/MessageImage.js +34 -8
  92. package/lib/module/components/styled/MessageImage.js.map +1 -1
  93. package/lib/module/components/styled/VideoMessage.js +7 -1
  94. package/lib/module/components/styled/VideoMessage.js.map +1 -1
  95. package/lib/module/context/xmppProvider.js +43 -18
  96. package/lib/module/context/xmppProvider.js.map +1 -1
  97. package/lib/module/helpers/initXmppRooms.js +5 -1
  98. package/lib/module/helpers/initXmppRooms.js.map +1 -1
  99. package/lib/module/helpers/resolveInitBeforeLoadUser.js +126 -11
  100. package/lib/module/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  101. package/lib/module/helpers/secureFileUrl.js +91 -0
  102. package/lib/module/helpers/secureFileUrl.js.map +1 -0
  103. package/lib/module/hooks/useChatWrapperInit.js +3 -3
  104. package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
  105. package/lib/module/hooks/useFileToken.js +5 -0
  106. package/lib/module/hooks/useFileToken.js.map +1 -0
  107. package/lib/module/hooks/useSendMessage.js +24 -14
  108. package/lib/module/hooks/useSendMessage.js.map +1 -1
  109. package/lib/module/main.js +12 -1
  110. package/lib/module/main.js.map +1 -1
  111. package/lib/module/networking/api-requests/auth.api.js +96 -53
  112. package/lib/module/networking/api-requests/auth.api.js.map +1 -1
  113. package/lib/module/networking/api-requests/roomMembers.api.js +1 -1
  114. package/lib/module/networking/api-requests/roomMembers.api.js.map +1 -1
  115. package/lib/module/networking/api-requests/rooms.api.js +9 -9
  116. package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
  117. package/lib/module/networking/api-requests/user.api.js +8 -7
  118. package/lib/module/networking/api-requests/user.api.js.map +1 -1
  119. package/lib/module/networking/apiClient.js +61 -113
  120. package/lib/module/networking/apiClient.js.map +1 -1
  121. package/lib/module/networking/authRefresh.js +312 -0
  122. package/lib/module/networking/authRefresh.js.map +1 -0
  123. package/lib/module/networking/xmppClient.js +33 -5
  124. package/lib/module/networking/xmppClient.js.map +1 -1
  125. package/lib/module/roomStore/chatSettingsSlice.js +20 -2
  126. package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
  127. package/lib/typescript/components/AuthForms/Login.d.ts.map +1 -1
  128. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  129. package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
  130. package/lib/typescript/components/MainComponents/LoginWrapper.d.ts.map +1 -1
  131. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  132. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  133. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  134. package/lib/typescript/components/Modals/FilePreviewModal/PdfView.d.ts.map +1 -1
  135. package/lib/typescript/components/RoomComponents/ChatRoomItem.d.ts.map +1 -1
  136. package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts.map +1 -1
  137. package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts.map +1 -1
  138. package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts.map +1 -1
  139. package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
  140. package/lib/typescript/components/styled/MessageImage.d.ts +8 -0
  141. package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
  142. package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
  143. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  144. package/lib/typescript/helpers/initXmppRooms.d.ts.map +1 -1
  145. package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts.map +1 -1
  146. package/lib/typescript/helpers/secureFileUrl.d.ts +7 -0
  147. package/lib/typescript/helpers/secureFileUrl.d.ts.map +1 -0
  148. package/lib/typescript/hooks/useFileToken.d.ts +2 -0
  149. package/lib/typescript/hooks/useFileToken.d.ts.map +1 -0
  150. package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
  151. package/lib/typescript/main.d.ts +6 -1
  152. package/lib/typescript/main.d.ts.map +1 -1
  153. package/lib/typescript/networking/api-requests/auth.api.d.ts +3 -27
  154. package/lib/typescript/networking/api-requests/auth.api.d.ts.map +1 -1
  155. package/lib/typescript/networking/api-requests/user.api.d.ts.map +1 -1
  156. package/lib/typescript/networking/apiClient.d.ts +12 -6
  157. package/lib/typescript/networking/apiClient.d.ts.map +1 -1
  158. package/lib/typescript/networking/authRefresh.d.ts +92 -0
  159. package/lib/typescript/networking/authRefresh.d.ts.map +1 -0
  160. package/lib/typescript/networking/xmppClient.d.ts +6 -0
  161. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  162. package/lib/typescript/roomStore/chatSettingsSlice.d.ts +16 -0
  163. package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
  164. package/lib/typescript/types/types.d.ts +17 -0
  165. package/lib/typescript/types/types.d.ts.map +1 -1
  166. package/package.json +1 -1
  167. package/src/components/AuthForms/Login.tsx +1 -0
  168. package/src/components/MainComponents/ChatHeader.tsx +4 -1
  169. package/src/components/MainComponents/ChatWrapper.tsx +14 -19
  170. package/src/components/MainComponents/LoginWrapper.tsx +1 -0
  171. package/src/components/MainComponents/MediaMessage.tsx +14 -4
  172. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +4 -1
  173. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +8 -7
  174. package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -1
  175. package/src/components/RoomComponents/ChatRoomItem.tsx +9 -1
  176. package/src/components/RoomComponents/styled/LastMessageFile.tsx +14 -2
  177. package/src/components/RoomComponents/styled/LastMessagePhoto.tsx +4 -1
  178. package/src/components/RoomComponents/styled/LastMessageVideo.tsx +4 -1
  179. package/src/components/styled/AudioMessage.tsx +2 -1
  180. package/src/components/styled/MessageImage.tsx +45 -6
  181. package/src/components/styled/VideoMessage.tsx +10 -1
  182. package/src/context/xmppProvider.tsx +45 -20
  183. package/src/helpers/initXmppRooms.tsx +5 -1
  184. package/src/helpers/resolveInitBeforeLoadUser.ts +123 -11
  185. package/src/helpers/secureFileUrl.ts +103 -0
  186. package/src/hooks/useChatWrapperInit.ts +3 -3
  187. package/src/hooks/useFileToken.ts +7 -0
  188. package/src/hooks/useSendMessage.tsx +28 -13
  189. package/src/main.ts +22 -1
  190. package/src/networking/api-requests/auth.api.ts +125 -52
  191. package/src/networking/api-requests/roomMembers.api.ts +1 -1
  192. package/src/networking/api-requests/rooms.api.ts +9 -9
  193. package/src/networking/api-requests/user.api.ts +8 -7
  194. package/src/networking/apiClient.ts +68 -123
  195. package/src/networking/authRefresh.ts +435 -0
  196. package/src/networking/xmppClient.ts +37 -5
  197. package/src/roomStore/chatSettingsSlice.ts +28 -3
  198. package/src/types/types.ts +17 -0
@@ -28,6 +28,8 @@ import { MODAL_TYPES } from '../../helpers/constants/MODAL_TYPES';
28
28
  import { RoomMenu } from '../MenuRoom/MenuRoom';
29
29
  import { useRoomState } from '../../hooks/useRoomState';
30
30
  import { useChatSettingState } from '../../hooks/useChatSettingState';
31
+ import { useFileToken } from '../../hooks/useFileToken';
32
+ import { appendFileToken } from '../../helpers/secureFileUrl';
31
33
  import { View, StyleSheet, Text, Keyboard } from 'react-native';
32
34
  import { getIconColor } from '../../helpers/getIconColor';
33
35
  import { resolveHeaderHeight } from '../../helpers/headerLayout';
@@ -43,6 +45,7 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
43
45
  currentRoom,
44
46
  handleBackClick,
45
47
  }) => {
48
+ const fileToken = useFileToken();
46
49
  const dispatch = useDispatch();
47
50
  const { client } = useXmppClient();
48
51
 
@@ -152,7 +155,7 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
152
155
  <ProfileImagePlaceholder
153
156
  name={currentRoom?.title || currentRoom?.name}
154
157
  size={40}
155
- icon={currentRoom?.icon}
158
+ icon={appendFileToken(currentRoom?.icon, fileToken)}
156
159
  active={!config?.disableChatInfo?.disableChatHeaderMenu}
157
160
  click={
158
161
  config?.disableChatInfo?.disableChatHeaderMenu
@@ -23,7 +23,7 @@ import {
23
23
  setIsLoading,
24
24
  setLastViewedTimestamp,
25
25
  } from '../../roomStore/roomsSlice';
26
- import {refresh} from '../../networking/apiClient';
26
+ import {refreshAuthTokensQuietly} from '../../networking/authRefresh';
27
27
  import RoomList from './RoomList';
28
28
  import {StyledLoaderWrapper} from '../styled/StyledComponents';
29
29
  import Modal from '../Modals/Modal/Modal';
@@ -170,6 +170,16 @@ const ChatWrapper: FC<ChatWrapperProps> = ({
170
170
  );
171
171
  }
172
172
 
173
+ // Top-up rotation after the client is up. Deduped inside
174
+ // `refreshAuthTokensQuietly`, so it can no longer race the 401
175
+ // interceptor into a second, parallel rotation — which the backend
176
+ // would read as reuse. Never rejects, hence no `.catch` here.
177
+ const ensureFreshTokens = () => {
178
+ if (config?.refreshTokens?.enabled) {
179
+ refreshAuthTokensQuietly();
180
+ }
181
+ };
182
+
173
183
  const initXmmpClient = async () => {
174
184
  // Only sync config to redux if we have one — passing `undefined`
175
185
  // wipes whatever XmppProvider already set up.
@@ -252,14 +262,7 @@ const ChatWrapper: FC<ChatWrapperProps> = ({
252
262
  devPushLog('warn', 'initializeClient failed (legacy path)', err);
253
263
  }
254
264
  setInited(true);
255
- if (config?.refreshTokens?.enabled) {
256
- // refresh() is fire-and-forget but rejects on bad
257
- // refreshToken / network — surface via .catch instead of
258
- // leaving the rejection unhandled (was bug #4, id:2).
259
- refresh().catch((err) =>
260
- console.warn('refresh failed', err)
261
- );
262
- }
265
+ ensureFreshTokens();
263
266
  } else if (storedClient) {
264
267
  devPushLog('rn', 'ChatWrapper: reusing storedClient');
265
268
  setClient(storedClient);
@@ -274,11 +277,7 @@ const ChatWrapper: FC<ChatWrapperProps> = ({
274
277
  .catch((err: unknown) => console.warn('getRoomsStanza failed', err));
275
278
  }
276
279
  setInited(true);
277
- if (config?.refreshTokens?.enabled) {
278
- refresh().catch((err) =>
279
- console.warn('refresh failed', err)
280
- );
281
- }
280
+ ensureFreshTokens();
282
281
  } else if (client) {
283
282
  devPushLog('rn', 'ChatWrapper: reusing provider client');
284
283
  if (!activeRoomJID) {
@@ -295,11 +294,7 @@ const ChatWrapper: FC<ChatWrapperProps> = ({
295
294
  console.warn('getChatsPrivateStoreRequestStanza failed', err)
296
295
  );
297
296
  setInited(true);
298
- if (config?.refreshTokens?.enabled) {
299
- refresh().catch((err) =>
300
- console.warn('refresh failed', err)
301
- );
302
- }
297
+ ensureFreshTokens();
303
298
  }
304
299
 
305
300
  dispatch(setIsLoading({loading: false}));
@@ -36,6 +36,7 @@ const LoginWrapper: React.FC<LoginWrapperProps> = ({...props}) => {
36
36
  ...authData.data.user,
37
37
  token: authData.data.token,
38
38
  refreshToken: authData.data.refreshToken,
39
+ fileToken: (authData.data as any).fileToken || '',
39
40
  };
40
41
  } catch (error) {
41
42
  console.error('Login failed:', error);
@@ -15,6 +15,8 @@ import { deriveDisplayFilename, isLikelyAudio } from '../../helpers/mimeToExtens
15
15
  import { FileIcon, PlayIcon } from '../../assets/icons';
16
16
  import { defaultMediaDims } from '../../helpers/mediaDimensions';
17
17
  import { useChatSettingState } from '../../hooks/useChatSettingState';
18
+ import { useFileToken } from '../../hooks/useFileToken';
19
+ import { appendFileToken } from '../../helpers/secureFileUrl';
18
20
 
19
21
  interface MediaMessageProps {
20
22
  mimeType?: string;
@@ -136,22 +138,26 @@ const PendingMediaMessage: React.FC<{
136
138
 
137
139
  const MediaMessage: React.FC<MediaMessageProps> = ({
138
140
  mimeType,
139
- location,
140
- messageText,
141
+ location: rawLocation,
142
+ messageText: rawMessageText,
141
143
  message,
142
144
  isUser,
143
145
  }) => {
146
+ const fileToken = useFileToken();
147
+ const location = appendFileToken(rawLocation, fileToken);
148
+ const messageText = appendFileToken(rawMessageText, fileToken);
149
+
144
150
  if (mimeType) {
145
151
  const displayName = deriveDisplayFilename({
146
152
  fileName: message?.fileName,
147
153
  originalName: (message as any)?.originalName,
148
- url: location || messageText,
154
+ url: rawLocation || rawMessageText,
149
155
  mime: mimeType,
150
156
  });
151
157
  const isAudioPayload = isLikelyAudio(
152
158
  mimeType,
153
159
  displayName,
154
- location,
160
+ rawLocation,
155
161
  {
156
162
  duration: (message as any)?.duration,
157
163
  waveForm: (message as any)?.waveForm,
@@ -174,7 +180,11 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
174
180
  return (
175
181
  <CustomMessageImage
176
182
  fileName={displayName}
183
+ // Full-size original: opened by the preview modal.
177
184
  fileURL={location || messageText || ''}
185
+ // ...and the thumbnail the bubble actually renders, which is
186
+ // what the web client shows too.
187
+ locationPreview={messageText}
178
188
  mimetype={mimeType}
179
189
  />
180
190
  );
@@ -1,4 +1,6 @@
1
1
  import React, { useEffect, useMemo, useState } from 'react';
2
+ import { useFileToken } from '../../../hooks/useFileToken';
3
+ import { appendFileToken } from '../../../helpers/secureFileUrl';
2
4
  import {
3
5
  CenterContainer,
4
6
  UserInfo,
@@ -69,6 +71,7 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
69
71
  const { client } = useXmppClient();
70
72
  const { user: stateUser, config } = useChatSettingState();
71
73
  const activeRoom = useSelector((state: RootState) => getActiveRoom(state));
74
+ const fileToken = useFileToken();
72
75
 
73
76
  // Pull live member list from the MUC when the modal opens — REST
74
77
  // hydration only fills the *count* (item.participants /
@@ -295,7 +298,7 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
295
298
  <CenterContainer>
296
299
  <ProfileImagePlaceholder
297
300
  name={activeRoom.title || activeRoom.name}
298
- icon={activeRoom.icon}
301
+ icon={appendFileToken(activeRoom.icon, fileToken)}
299
302
  // Two gates control whether the icon is editable:
300
303
  // 1. role — only non-participants could edit before.
301
304
  // 2. NEW: `disableChatInfo.disableIconEdit` — hard kill from
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
+ import { withFileToken } from '../../../helpers/secureFileUrl';
2
3
  import styled from 'styled-components/native';
3
4
  import {
4
5
  CenterContainer,
@@ -154,7 +155,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
154
155
  );
155
156
 
156
157
  const filePath = FileSystem.cacheDirectory + fileName;
157
- const download = await FileSystem.downloadAsync(activeFile.fileURL, filePath);
158
+ const download = await FileSystem.downloadAsync(withFileToken(activeFile.fileURL), filePath);
158
159
 
159
160
  if (download.status === 200) {
160
161
  await MediaLibrary.saveToLibraryAsync(download.uri);
@@ -179,7 +180,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
179
180
  activeFile.mimetype
180
181
  );
181
182
  const filePath = FileSystem.cacheDirectory + fileName;
182
- const download = await FileSystem.downloadAsync(activeFile.fileURL, filePath);
183
+ const download = await FileSystem.downloadAsync(withFileToken(activeFile.fileURL), filePath);
183
184
 
184
185
  if (download.status !== 200) {
185
186
  Alert.alert('Error', 'Failed to save the file.');
@@ -272,7 +273,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
272
273
  <FullScreenImage
273
274
  source={{
274
275
  uri:
275
- activeFile.fileURL ||
276
+ withFileToken(activeFile.fileURL) ||
276
277
  'https://as2.ftcdn.net/v2/jpg/02/51/95/53/1000_F_251955356_FAQH0U1y1TZw3ZcdPGybwUkH90a3VAhb.jpg',
277
278
  }}
278
279
  resizeMode="contain"
@@ -280,7 +281,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
280
281
  />
281
282
  );
282
283
  case activeFile.mimetype.startsWith('video/'):
283
- return <ModalVideo uri={activeFile.fileURL} />;
284
+ return <ModalVideo uri={withFileToken(activeFile.fileURL)} />;
284
285
  case activeFile.mimetype.includes('application/octet-stream'):
285
286
  return (
286
287
  <View
@@ -296,7 +297,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
296
297
  Voice message
297
298
  </Text>
298
299
  <AudioMessage
299
- src={activeFile.fileURL}
300
+ src={withFileToken(activeFile.fileURL)}
300
301
  mimeType={activeFile.mimetype}
301
302
  fileName={activeFile.fileName}
302
303
  originalName={activeFile.originalName}
@@ -336,7 +337,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
336
337
  )}
337
338
  </Text>
338
339
  <AudioMessage
339
- src={activeFile.fileURL}
340
+ src={withFileToken(activeFile.fileURL)}
340
341
  mimeType={activeFile.mimetype}
341
342
  fileName={activeFile.fileName}
342
343
  originalName={activeFile.originalName}
@@ -346,7 +347,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
346
347
  </View>
347
348
  );
348
349
  case activeFile.mimetype === 'application/pdf':
349
- return <PdfViewer pdfUrl={activeFile.fileURL} />;
350
+ return <PdfViewer pdfUrl={withFileToken(activeFile.fileURL)} />;
350
351
  default: {
351
352
  const displayName = ensureFilenameHasExtension(
352
353
  activeFile.fileName,
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useState } from 'react';
2
+ import { withFileToken } from '../../../helpers/secureFileUrl';
2
3
  import { ActivityIndicator, Platform, StyleSheet, Text, View } from 'react-native';
3
4
  import { WebView } from 'react-native-webview';
4
5
  import * as FileSystem from 'expo-file-system/legacy';
@@ -25,7 +26,7 @@ const PdfViewer: React.FC<PdfViewerProps> = ({ pdfUrl }) => {
25
26
  const run = async () => {
26
27
  try {
27
28
  const target = FileSystem.cacheDirectory + `pdf-${Date.now()}.pdf`;
28
- const result = await FileSystem.downloadAsync(pdfUrl, target);
29
+ const result = await FileSystem.downloadAsync(withFileToken(pdfUrl), target);
29
30
  if (cancelled) {return;}
30
31
  setLocalUri(result.uri);
31
32
  } catch (e: any) {
@@ -1,4 +1,6 @@
1
1
  import React, { useMemo } from 'react';
2
+ import { useFileToken } from '../../hooks/useFileToken';
3
+ import { appendFileToken } from '../../helpers/secureFileUrl';
2
4
  import { IConfig, IRoom } from '../../types/types';
3
5
  import { ProfileImagePlaceholder } from '../MainComponents/ProfileImagePlaceholder';
4
6
  import {
@@ -26,6 +28,8 @@ const ChatRoomItem: React.FC<ChatRoomItemProps> = ({
26
28
  isDriver,
27
29
  config,
28
30
  }) => {
31
+ // Room icons can live on the membership-gated secure-files host.
32
+ const fileToken = useFileToken();
29
33
  const displayName = String(chat?.title || chat?.name || '').trim();
30
34
 
31
35
  const lastMessage = useMemo(() => {
@@ -86,7 +90,11 @@ const ChatRoomItem: React.FC<ChatRoomItemProps> = ({
86
90
 
87
91
  return (
88
92
  <ChatItem key={index}>
89
- <ProfileImagePlaceholder name={displayName} icon={chat?.icon} active={false} />
93
+ <ProfileImagePlaceholder
94
+ name={displayName}
95
+ icon={appendFileToken(chat?.icon, fileToken)}
96
+ active={false}
97
+ />
90
98
  <View
91
99
  style={{
92
100
  flexDirection: 'column',
@@ -8,6 +8,12 @@ import {
8
8
  } from './StyledRoomComponents';
9
9
  import { FileIcon } from '../../../assets/icons';
10
10
  import { Image, View } from 'react-native';
11
+ import { useFileToken } from '../../../hooks/useFileToken';
12
+ import {
13
+ appendFileToken,
14
+ isSecureFileUrl,
15
+ requestFileTokenRecovery,
16
+ } from '../../../helpers/secureFileUrl';
11
17
 
12
18
  interface LastMessageFileProps
13
19
  extends Pick<LastMessage, 'user' | 'originalName' | 'locationPreview'> {}
@@ -21,6 +27,7 @@ const LastMessageFile: FC<LastMessageFileProps> = ({
21
27
  locationPreview,
22
28
  }) => {
23
29
  const [imgSrc, setImgSrc] = useState(locationPreview);
30
+ const fileToken = useFileToken();
24
31
 
25
32
  return (
26
33
  <LastRoomMessageContainer>
@@ -34,13 +41,18 @@ const LastMessageFile: FC<LastMessageFileProps> = ({
34
41
  >
35
42
  {imgSrc ? (
36
43
  <Image
37
- source={{ uri: imgSrc }}
44
+ source={{ uri: appendFileToken(imgSrc, fileToken) }}
38
45
  style={{
39
46
  borderRadius: 16,
40
47
  width: 20,
41
48
  height: 20,
42
49
  }}
43
- onError={() => setImgSrc(fallbackImage)}
50
+ onError={() => {
51
+ if (isSecureFileUrl(imgSrc)) {
52
+ requestFileTokenRecovery();
53
+ }
54
+ setImgSrc(fallbackImage);
55
+ }}
44
56
  />
45
57
  ) : (
46
58
  <FileIcon style={{ width: '20px', height: '20px' }} />
@@ -1,4 +1,6 @@
1
1
  import React, { FC } from 'react';
2
+ import { useFileToken } from '../../../hooks/useFileToken';
3
+ import { appendFileToken } from '../../../helpers/secureFileUrl';
2
4
  import styled from 'styled-components/native';
3
5
  import { LastMessage } from '../../../types/types';
4
6
  import {
@@ -30,6 +32,7 @@ const LastMessagePhoto: FC<LastMessagePhotoProps> = ({
30
32
  locationPreview,
31
33
  originalName,
32
34
  }) => {
35
+ const fileToken = useFileToken();
33
36
  return (
34
37
  <LastRoomMessageContainer>
35
38
  <LastRoomMessageName>{user?.name || ''}:</LastRoomMessageName>
@@ -42,7 +45,7 @@ const LastMessagePhoto: FC<LastMessagePhotoProps> = ({
42
45
  >
43
46
  <PhotoContainer>
44
47
  <ShadeWrapper>
45
- <LastMessageImg src={locationPreview} />
48
+ <LastMessageImg src={appendFileToken(locationPreview, fileToken)} />
46
49
  </ShadeWrapper>
47
50
  </PhotoContainer>
48
51
  <LastRoomMessageText>{originalName || 'file'}</LastRoomMessageText>
@@ -1,4 +1,6 @@
1
1
  import React, { FC } from 'react';
2
+ import { useFileToken } from '../../../hooks/useFileToken';
3
+ import { appendFileToken } from '../../../helpers/secureFileUrl';
2
4
  import styled from 'styled-components/native';
3
5
  import { LastMessage } from '../../../types/types';
4
6
  import {
@@ -45,6 +47,7 @@ const LastMessageVideo: FC<LastMessageVideoProps> = ({
45
47
  location,
46
48
  originalName,
47
49
  }) => {
50
+ const fileToken = useFileToken();
48
51
  return (
49
52
  <LastRoomMessageContainer>
50
53
  <LastRoomMessageName>{user?.name || ''}:</LastRoomMessageName>
@@ -57,7 +60,7 @@ const LastMessageVideo: FC<LastMessageVideoProps> = ({
57
60
  >
58
61
  <VideoContainer>
59
62
  <ShadeWrapper>
60
- <Thumbnail src={location} />
63
+ <Thumbnail src={appendFileToken(location, fileToken)} />
61
64
  </ShadeWrapper>
62
65
  <PlayButton><Text>▶</Text></PlayButton>
63
66
  </VideoContainer>
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
+ import { withFileToken } from '../../helpers/secureFileUrl';
2
3
  import {
3
4
  View,
4
5
  TouchableOpacity,
@@ -425,7 +426,7 @@ const AudioMessage = ({
425
426
  const partInfo = await FileSystem.getInfoAsync(part);
426
427
  if (!partInfo.exists) {
427
428
  const download = await withTimeout(
428
- FileSystem.downloadAsync(src, part),
429
+ FileSystem.downloadAsync(withFileToken(src), part),
429
430
  12000,
430
431
  'audio_download_timeout'
431
432
  );
@@ -1,4 +1,9 @@
1
1
  import React, { useEffect, useState } from 'react';
2
+ import {
3
+ isSecureFileUrl,
4
+ requestFileTokenRecovery,
5
+ } from '../../helpers/secureFileUrl';
6
+ import { pushLog } from '../../utils/devLogger';
2
7
  import { Container } from './StyledInputComponents/MediaComponents';
3
8
  import { useDispatch } from 'react-redux';
4
9
  import {
@@ -14,11 +19,23 @@ import {
14
19
  } from '../../helpers/mediaDimensions';
15
20
 
16
21
  interface CustomMessageImageProps {
22
+ /** Full-size file. Opened in the preview modal, NOT rendered inline. */
17
23
  fileURL: string;
18
24
  fileName: string;
19
25
  mimetype: string;
26
+ /**
27
+ * Server-generated thumbnail — what the bubble actually renders, same
28
+ * as the web client. The full-size original can be many megabytes and,
29
+ * for secure uploads, is served from a different (membership-gated)
30
+ * route; rendering it inline is what left the bubbles blank.
31
+ */
32
+ locationPreview?: string;
20
33
  }
21
34
 
35
+ /** Keep the personal file token out of the logs. */
36
+ const maskToken = (url?: string) =>
37
+ (url || '').replace(/([?&]ft=)[^&]*/i, '$1<redacted>');
38
+
22
39
  const FALLBACK_IMAGE =
23
40
  'https://as2.ftcdn.net/v2/jpg/02/51/95/53/1000_F_251955356_FAQH0U1y1TZw3ZcdPGybwUkH90a3VAhb.jpg';
24
41
 
@@ -26,7 +43,12 @@ const CustomMessageImage: React.FC<CustomMessageImageProps> = ({
26
43
  fileURL,
27
44
  fileName,
28
45
  mimetype,
46
+ locationPreview,
29
47
  }) => {
48
+ // Preview first, original only as a fallback — mirrors the web client,
49
+ // where the <img> src is the preview and `fileURL` is reserved for the
50
+ // modal.
51
+ const displayURL = locationPreview || fileURL;
30
52
  const [loading, setLoading] = useState(true);
31
53
  // Start with a neutral box; swap to the real aspect ratio once the
32
54
  // image's natural dimensions are known so it's shown scaled, not cropped.
@@ -35,25 +57,31 @@ const CustomMessageImage: React.FC<CustomMessageImageProps> = ({
35
57
  const dispatch = useDispatch();
36
58
 
37
59
  useEffect(() => {
38
- if (!fileURL) {
60
+ if (!displayURL) {
39
61
  return;
40
62
  }
41
63
  let active = true;
42
64
  Image.getSize(
43
- fileURL,
65
+ displayURL,
44
66
  (w, h) => {
45
67
  if (active) {
46
68
  setDims(fitMediaDimensions(w, h));
47
69
  }
48
70
  },
49
- () => {
50
- /* keep the default box if the size probe fails */
71
+ (error) => {
72
+ // Keep the default box — but say WHICH url failed. A blank
73
+ // bubble is otherwise indistinguishable between "bad url",
74
+ // "403 without a token" and "host unreachable".
75
+ pushLog('warn', 'image: getSize failed', {
76
+ url: maskToken(displayURL),
77
+ error: (error as any)?.message || String(error),
78
+ });
51
79
  }
52
80
  );
53
81
  return () => {
54
82
  active = false;
55
83
  };
56
- }, [fileURL]);
84
+ }, [displayURL]);
57
85
 
58
86
  const handleOpen = () => {
59
87
  dispatch(setActiveFile({ fileName, fileURL, mimetype }));
@@ -78,7 +106,7 @@ const CustomMessageImage: React.FC<CustomMessageImageProps> = ({
78
106
  </View>
79
107
  )}
80
108
  <Image
81
- source={{ uri: fileURL || FALLBACK_IMAGE }}
109
+ source={{ uri: displayURL || FALLBACK_IMAGE }}
82
110
  style={{
83
111
  width: dims.width,
84
112
  height: dims.height,
@@ -86,6 +114,17 @@ const CustomMessageImage: React.FC<CustomMessageImageProps> = ({
86
114
  backgroundColor: 'rgba(0, 0, 0, 0)',
87
115
  }}
88
116
  onLoadEnd={() => setLoading(false)}
117
+ onError={(event) => {
118
+ setLoading(false);
119
+ pushLog('warn', 'image: load failed', {
120
+ url: maskToken(displayURL),
121
+ usingPreview: !!locationPreview,
122
+ error: (event as any)?.nativeEvent?.error,
123
+ });
124
+ if (isSecureFileUrl(displayURL)) {
125
+ requestFileTokenRecovery();
126
+ }
127
+ }}
89
128
  // The box already matches the natural aspect ratio, so 'cover'
90
129
  // fills it edge-to-edge with no actual cropping or letterboxing.
91
130
  resizeMode="cover"
@@ -1,4 +1,8 @@
1
1
  import React, { useEffect, useState } from 'react';
2
+ import {
3
+ isSecureFileUrl,
4
+ requestFileTokenRecovery,
5
+ } from '../../helpers/secureFileUrl';
2
6
  import { Image, Pressable, StyleSheet, View } from 'react-native';
3
7
  import { useDispatch } from 'react-redux';
4
8
  import {
@@ -139,7 +143,12 @@ const CustomMessageVideo: React.FC<CustomMessageVideoProps> = ({
139
143
  <PosterVideo
140
144
  previewURL={previewURL}
141
145
  onOpen={handleOpen}
142
- onPosterError={() => setPosterFailed(true)}
146
+ onPosterError={() => {
147
+ if (isSecureFileUrl(previewURL)) {
148
+ requestFileTokenRecovery();
149
+ }
150
+ setPosterFailed(true);
151
+ }}
143
152
  />
144
153
  );
145
154
  }
@@ -14,7 +14,7 @@ import { VideoCallOverlay } from '../components/VideoCalls/VideoCallOverlay';
14
14
  import XmppClient, {
15
15
  XmppCredentialsProvider,
16
16
  } from '../networking/xmppClient';
17
- import { refreshTokens } from '../roomStore/chatSettingsSlice';
17
+ import { refreshAuthTokensQuietly } from '../networking/authRefresh';
18
18
  import {
19
19
  IConfig,
20
20
  ProviderBootstrapStatus,
@@ -107,23 +107,25 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
107
107
  // the XmppClient at least doesn't crash on `undefined.password`.
108
108
  const credentialsProvider = useMemo<XmppCredentialsProvider>(() => {
109
109
  return async () => {
110
- // 1. Consumer-supplied REST refresh (when wired). This runs
111
- // before the SDK's own refresh chain so non-Ethora backends
112
- // can keep auth state in sync with their own token endpoint.
113
- const customRefresh = config?.refreshTokens?.refreshFunction;
114
- if (customRefresh) {
115
- try {
116
- const result = await customRefresh();
117
- if (result?.accessToken) {
118
- store.dispatch(
119
- refreshTokens({
120
- token: result.accessToken,
121
- refreshToken: result.refreshToken || '',
122
- })
123
- );
124
- }
125
- } catch (err) {
126
- devPushLog('warn', 'XMPP creds refresh: customRefresh failed', err);
110
+ // 1. Make sure the REST tokens are fresh before re-minting XMPP
111
+ // creds. This used to call `config.refreshTokens.refreshFunction`
112
+ // directly and then fall into step 2, which refreshes again —
113
+ // two rotations back to back, which the new backend scheme
114
+ // reads as a race at best and token reuse at worst. One call to
115
+ // the shared rotation point covers both the consumer-supplied
116
+ // function and the built-in endpoint.
117
+ if (config?.refreshTokens?.enabled) {
118
+ const rotated = await refreshAuthTokensQuietly();
119
+
120
+ if (rotated?.xmppPassword) {
121
+ const rotatedUser = store.getState().chatSettingStore.user;
122
+ return {
123
+ username:
124
+ rotatedUser?.xmppUsername ||
125
+ rotatedUser?.defaultWallet?.walletAddress ||
126
+ '',
127
+ password: rotated.xmppPassword,
128
+ };
127
129
  }
128
130
  }
129
131
 
@@ -163,7 +165,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
163
165
  }, [
164
166
  config?.jwtLogin?.enabled,
165
167
  config?.jwtLogin?.token,
166
- config?.refreshTokens?.refreshFunction,
168
+ config?.refreshTokens?.enabled,
167
169
  config?.userLogin?.enabled,
168
170
  config?.customLogin?.enabled,
169
171
  config?.initBeforeLoadAuth?.myEndpoint,
@@ -353,6 +355,29 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
353
355
  store.dispatch(setConfig(config));
354
356
  }
355
357
 
358
+ // Rotate BEFORE the first XMPP connect. On the rotating backend
359
+ // the xmpp password is a short-lived JWT that only the newest
360
+ // rotation knows, and each rotation invalidates its predecessors
361
+ // — so a snapshot's password (a host's cached `userLogin.user`,
362
+ // a persisted session) is usually already dead at boot. Without
363
+ // this, the first SASL attempt fails `not-authorized`, all three
364
+ // bootstrap retries re-present the same dead password, and the
365
+ // user dead-ends on the Connection error modal even though one
366
+ // rotation away a perfectly good session existed. The rotation
367
+ // also lands the fileToken pre-render, so secure media never
368
+ // paints blank first. Quiet + deduped; when it can't rotate
369
+ // (no refresh material, non-rotating backend) we just connect
370
+ // with what the snapshot has, exactly as before.
371
+ // jwtLogin is skipped: /users/client just minted fresh creds.
372
+ let connectPassword = resolved.xmppPassword;
373
+ if (config?.refreshTokens?.enabled && !config?.jwtLogin?.enabled) {
374
+ const rotated = await refreshAuthTokensQuietly();
375
+ if (cancelled) {return;}
376
+ if (rotated?.xmppPassword) {
377
+ connectPassword = rotated.xmppPassword;
378
+ }
379
+ }
380
+
356
381
  // Kick off REST /chats/my; allRoomPresences below needs the
357
382
  // room list to be in redux, otherwise it joins 0 MUCs and the
358
383
  // user receives zero realtime messages until they manually tap
@@ -362,7 +387,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
362
387
 
363
388
  const c = await initializeClient(
364
389
  resolved.xmppUsername!,
365
- resolved.xmppPassword,
390
+ connectPassword,
366
391
  config?.xmppSettings
367
392
  );
368
393
  if (cancelled) {return;}
@@ -4,6 +4,7 @@ import { initRoomsPresence } from './initRoomsPresence';
4
4
  import XmppClient from '../networking/xmppClient';
5
5
  import { IConfig, IRoom, User } from '../types/types';
6
6
  import { store } from '../roomStore';
7
+ import { refreshAuthTokensQuietly } from '../networking/authRefresh';
7
8
 
8
9
  const initXmppRooms = async (
9
10
  user: User,
@@ -59,7 +60,10 @@ const initXmppRooms = async (
59
60
  }
60
61
 
61
62
  if (config?.refreshTokens?.enabled) {
62
- config?.refreshTokens?.refreshFunction?.();
63
+ // Was a bare call to the consumer-supplied refreshFunction, i.e. a
64
+ // rotation completely outside the SDK's lock. Routed through the
65
+ // one rotation point, which calls that same function internally.
66
+ refreshAuthTokensQuietly();
63
67
  }
64
68
  } catch (error) {
65
69
  console.error(error);