@ethora/chat-component-rn 26.6.12 → 26.7.1

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 (281) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -2
  3. package/lib/commonjs/components/AuthForms/Login.js +4 -4
  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/MainComponents/ProfileImagePlaceholder.js +9 -8
  14. package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.js.map +1 -1
  15. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +4 -1
  16. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  17. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +8 -7
  18. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  19. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js +2 -1
  20. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  21. package/lib/commonjs/components/Modals/NewChatModal/ModalContent.js +2 -1
  22. package/lib/commonjs/components/Modals/NewChatModal/ModalContent.js.map +1 -1
  23. package/lib/commonjs/components/Modals/NewChatModal/UpdNewChatModal.js +8 -0
  24. package/lib/commonjs/components/Modals/NewChatModal/UpdNewChatModal.js.map +1 -1
  25. package/lib/commonjs/components/Modals/UserProfileModal/EditUserModal.js +7 -21
  26. package/lib/commonjs/components/Modals/UserProfileModal/EditUserModal.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/styled/AudioMessage.js +2 -1
  36. package/lib/commonjs/components/styled/AudioMessage.js.map +1 -1
  37. package/lib/commonjs/components/styled/MessageImage.js +34 -8
  38. package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
  39. package/lib/commonjs/components/styled/VideoMessage.js +7 -1
  40. package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
  41. package/lib/commonjs/context/xmppProvider.js +46 -21
  42. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  43. package/lib/commonjs/helpers/ensureScopedChatCache.js +2 -2
  44. package/lib/commonjs/helpers/ensureScopedChatCache.js.map +1 -1
  45. package/lib/commonjs/helpers/initXmppRooms.js +5 -1
  46. package/lib/commonjs/helpers/initXmppRooms.js.map +1 -1
  47. package/lib/commonjs/helpers/persistCrypto.js +122 -0
  48. package/lib/commonjs/helpers/persistCrypto.js.map +1 -0
  49. package/lib/commonjs/helpers/pickImageAsset.js +68 -0
  50. package/lib/commonjs/helpers/pickImageAsset.js.map +1 -0
  51. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js +130 -17
  52. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  53. package/lib/commonjs/helpers/secureFileUrl.js +101 -0
  54. package/lib/commonjs/helpers/secureFileUrl.js.map +1 -0
  55. package/lib/commonjs/helpers/secureKeyValue.js +83 -0
  56. package/lib/commonjs/helpers/secureKeyValue.js.map +1 -0
  57. package/lib/commonjs/helpers/secureStoreRuntime.js +59 -0
  58. package/lib/commonjs/helpers/secureStoreRuntime.js.map +1 -0
  59. package/lib/commonjs/helpers/secureUserStorage.js +75 -0
  60. package/lib/commonjs/helpers/secureUserStorage.js.map +1 -0
  61. package/lib/commonjs/hooks/useChatWrapperInit.js +3 -3
  62. package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
  63. package/lib/commonjs/hooks/useFileToken.js +10 -0
  64. package/lib/commonjs/hooks/useFileToken.js.map +1 -0
  65. package/lib/commonjs/hooks/useLogout.js +15 -7
  66. package/lib/commonjs/hooks/useLogout.js.map +1 -1
  67. package/lib/commonjs/hooks/useSendMessage.js +23 -13
  68. package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
  69. package/lib/commonjs/main.js +25 -0
  70. package/lib/commonjs/main.js.map +1 -1
  71. package/lib/commonjs/networking/api-requests/auth.api.js +99 -54
  72. package/lib/commonjs/networking/api-requests/auth.api.js.map +1 -1
  73. package/lib/commonjs/networking/api-requests/roomMembers.api.js +1 -1
  74. package/lib/commonjs/networking/api-requests/roomMembers.api.js.map +1 -1
  75. package/lib/commonjs/networking/api-requests/rooms.api.js +9 -9
  76. package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
  77. package/lib/commonjs/networking/api-requests/user.api.js +9 -9
  78. package/lib/commonjs/networking/api-requests/user.api.js.map +1 -1
  79. package/lib/commonjs/networking/apiClient.js +62 -112
  80. package/lib/commonjs/networking/apiClient.js.map +1 -1
  81. package/lib/commonjs/networking/authRefresh.js +322 -0
  82. package/lib/commonjs/networking/authRefresh.js.map +1 -0
  83. package/lib/commonjs/networking/xmppClient.js +33 -5
  84. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  85. package/lib/commonjs/roomStore/chatSettingsSlice.js +23 -6
  86. package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
  87. package/lib/commonjs/roomStore/persistence.js +29 -15
  88. package/lib/commonjs/roomStore/persistence.js.map +1 -1
  89. package/lib/commonjs/roomStore/roomsSlice.js +2 -0
  90. package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
  91. package/lib/module/components/AuthForms/Login.js +4 -4
  92. package/lib/module/components/AuthForms/Login.js.map +1 -1
  93. package/lib/module/components/MainComponents/ChatHeader.js +4 -1
  94. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  95. package/lib/module/components/MainComponents/ChatWrapper.js +14 -13
  96. package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
  97. package/lib/module/components/MainComponents/LoginWrapper.js +2 -1
  98. package/lib/module/components/MainComponents/LoginWrapper.js.map +1 -1
  99. package/lib/module/components/MainComponents/MediaMessage.js +17 -6
  100. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  101. package/lib/module/components/MainComponents/ProfileImagePlaceholder.js +9 -8
  102. package/lib/module/components/MainComponents/ProfileImagePlaceholder.js.map +1 -1
  103. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +4 -1
  104. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  105. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +8 -7
  106. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  107. package/lib/module/components/Modals/FilePreviewModal/PdfView.js +2 -1
  108. package/lib/module/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  109. package/lib/module/components/Modals/NewChatModal/ModalContent.js +2 -1
  110. package/lib/module/components/Modals/NewChatModal/ModalContent.js.map +1 -1
  111. package/lib/module/components/Modals/NewChatModal/UpdNewChatModal.js +8 -0
  112. package/lib/module/components/Modals/NewChatModal/UpdNewChatModal.js.map +1 -1
  113. package/lib/module/components/Modals/UserProfileModal/EditUserModal.js +9 -23
  114. package/lib/module/components/Modals/UserProfileModal/EditUserModal.js.map +1 -1
  115. package/lib/module/components/RoomComponents/ChatRoomItem.js +5 -1
  116. package/lib/module/components/RoomComponents/ChatRoomItem.js.map +1 -1
  117. package/lib/module/components/RoomComponents/styled/LastMessageFile.js +10 -2
  118. package/lib/module/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
  119. package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js +4 -1
  120. package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
  121. package/lib/module/components/RoomComponents/styled/LastMessageVideo.js +4 -1
  122. package/lib/module/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
  123. package/lib/module/components/styled/AudioMessage.js +2 -1
  124. package/lib/module/components/styled/AudioMessage.js.map +1 -1
  125. package/lib/module/components/styled/MessageImage.js +34 -8
  126. package/lib/module/components/styled/MessageImage.js.map +1 -1
  127. package/lib/module/components/styled/VideoMessage.js +7 -1
  128. package/lib/module/components/styled/VideoMessage.js.map +1 -1
  129. package/lib/module/context/xmppProvider.js +45 -21
  130. package/lib/module/context/xmppProvider.js.map +1 -1
  131. package/lib/module/helpers/ensureScopedChatCache.js +2 -2
  132. package/lib/module/helpers/ensureScopedChatCache.js.map +1 -1
  133. package/lib/module/helpers/initXmppRooms.js +5 -1
  134. package/lib/module/helpers/initXmppRooms.js.map +1 -1
  135. package/lib/module/helpers/persistCrypto.js +115 -0
  136. package/lib/module/helpers/persistCrypto.js.map +1 -0
  137. package/lib/module/helpers/pickImageAsset.js +63 -0
  138. package/lib/module/helpers/pickImageAsset.js.map +1 -0
  139. package/lib/module/helpers/resolveInitBeforeLoadUser.js +130 -16
  140. package/lib/module/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  141. package/lib/module/helpers/secureFileUrl.js +91 -0
  142. package/lib/module/helpers/secureFileUrl.js.map +1 -0
  143. package/lib/module/helpers/secureKeyValue.js +75 -0
  144. package/lib/module/helpers/secureKeyValue.js.map +1 -0
  145. package/lib/module/helpers/secureStoreRuntime.js +52 -0
  146. package/lib/module/helpers/secureStoreRuntime.js.map +1 -0
  147. package/lib/module/helpers/secureUserStorage.js +72 -0
  148. package/lib/module/helpers/secureUserStorage.js.map +1 -0
  149. package/lib/module/hooks/useChatWrapperInit.js +3 -3
  150. package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
  151. package/lib/module/hooks/useFileToken.js +5 -0
  152. package/lib/module/hooks/useFileToken.js.map +1 -0
  153. package/lib/module/hooks/useLogout.js +15 -7
  154. package/lib/module/hooks/useLogout.js.map +1 -1
  155. package/lib/module/hooks/useSendMessage.js +24 -14
  156. package/lib/module/hooks/useSendMessage.js.map +1 -1
  157. package/lib/module/main.js +12 -1
  158. package/lib/module/main.js.map +1 -1
  159. package/lib/module/networking/api-requests/auth.api.js +96 -53
  160. package/lib/module/networking/api-requests/auth.api.js.map +1 -1
  161. package/lib/module/networking/api-requests/roomMembers.api.js +1 -1
  162. package/lib/module/networking/api-requests/roomMembers.api.js.map +1 -1
  163. package/lib/module/networking/api-requests/rooms.api.js +9 -9
  164. package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
  165. package/lib/module/networking/api-requests/user.api.js +8 -7
  166. package/lib/module/networking/api-requests/user.api.js.map +1 -1
  167. package/lib/module/networking/apiClient.js +61 -113
  168. package/lib/module/networking/apiClient.js.map +1 -1
  169. package/lib/module/networking/authRefresh.js +311 -0
  170. package/lib/module/networking/authRefresh.js.map +1 -0
  171. package/lib/module/networking/xmppClient.js +33 -5
  172. package/lib/module/networking/xmppClient.js.map +1 -1
  173. package/lib/module/roomStore/chatSettingsSlice.js +23 -6
  174. package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
  175. package/lib/module/roomStore/persistence.js +30 -15
  176. package/lib/module/roomStore/persistence.js.map +1 -1
  177. package/lib/module/roomStore/roomsSlice.js +2 -0
  178. package/lib/module/roomStore/roomsSlice.js.map +1 -1
  179. package/lib/typescript/components/AuthForms/Login.d.ts.map +1 -1
  180. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  181. package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
  182. package/lib/typescript/components/MainComponents/LoginWrapper.d.ts.map +1 -1
  183. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  184. package/lib/typescript/components/MainComponents/ProfileImagePlaceholder.d.ts.map +1 -1
  185. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  186. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  187. package/lib/typescript/components/Modals/FilePreviewModal/PdfView.d.ts.map +1 -1
  188. package/lib/typescript/components/Modals/NewChatModal/ModalContent.d.ts +4 -2
  189. package/lib/typescript/components/Modals/NewChatModal/ModalContent.d.ts.map +1 -1
  190. package/lib/typescript/components/Modals/NewChatModal/UpdNewChatModal.d.ts.map +1 -1
  191. package/lib/typescript/components/Modals/UserProfileModal/EditUserModal.d.ts.map +1 -1
  192. package/lib/typescript/components/RoomComponents/ChatRoomItem.d.ts.map +1 -1
  193. package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts.map +1 -1
  194. package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts.map +1 -1
  195. package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts.map +1 -1
  196. package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
  197. package/lib/typescript/components/styled/MessageImage.d.ts +8 -0
  198. package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
  199. package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
  200. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  201. package/lib/typescript/helpers/initXmppRooms.d.ts.map +1 -1
  202. package/lib/typescript/helpers/persistCrypto.d.ts +13 -0
  203. package/lib/typescript/helpers/persistCrypto.d.ts.map +1 -0
  204. package/lib/typescript/helpers/pickImageAsset.d.ts +27 -0
  205. package/lib/typescript/helpers/pickImageAsset.d.ts.map +1 -0
  206. package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts.map +1 -1
  207. package/lib/typescript/helpers/secureFileUrl.d.ts +7 -0
  208. package/lib/typescript/helpers/secureFileUrl.d.ts.map +1 -0
  209. package/lib/typescript/helpers/secureKeyValue.d.ts +13 -0
  210. package/lib/typescript/helpers/secureKeyValue.d.ts.map +1 -0
  211. package/lib/typescript/helpers/secureStoreRuntime.d.ts +29 -0
  212. package/lib/typescript/helpers/secureStoreRuntime.d.ts.map +1 -0
  213. package/lib/typescript/helpers/secureUserStorage.d.ts +23 -0
  214. package/lib/typescript/helpers/secureUserStorage.d.ts.map +1 -0
  215. package/lib/typescript/hooks/useFileToken.d.ts +2 -0
  216. package/lib/typescript/hooks/useFileToken.d.ts.map +1 -0
  217. package/lib/typescript/hooks/useLogout.d.ts.map +1 -1
  218. package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
  219. package/lib/typescript/main.d.ts +6 -1
  220. package/lib/typescript/main.d.ts.map +1 -1
  221. package/lib/typescript/networking/api-requests/auth.api.d.ts +3 -27
  222. package/lib/typescript/networking/api-requests/auth.api.d.ts.map +1 -1
  223. package/lib/typescript/networking/api-requests/user.api.d.ts.map +1 -1
  224. package/lib/typescript/networking/apiClient.d.ts +12 -6
  225. package/lib/typescript/networking/apiClient.d.ts.map +1 -1
  226. package/lib/typescript/networking/authRefresh.d.ts +92 -0
  227. package/lib/typescript/networking/authRefresh.d.ts.map +1 -0
  228. package/lib/typescript/networking/xmppClient.d.ts +6 -0
  229. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  230. package/lib/typescript/roomStore/chatSettingsSlice.d.ts +16 -0
  231. package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
  232. package/lib/typescript/roomStore/persistence.d.ts.map +1 -1
  233. package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
  234. package/lib/typescript/types/types.d.ts +17 -0
  235. package/lib/typescript/types/types.d.ts.map +1 -1
  236. package/package.json +8 -1
  237. package/src/components/AuthForms/Login.tsx +3 -3
  238. package/src/components/MainComponents/ChatHeader.tsx +4 -1
  239. package/src/components/MainComponents/ChatWrapper.tsx +14 -19
  240. package/src/components/MainComponents/LoginWrapper.tsx +1 -0
  241. package/src/components/MainComponents/MediaMessage.tsx +14 -4
  242. package/src/components/MainComponents/ProfileImagePlaceholder.tsx +16 -9
  243. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +4 -1
  244. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +8 -7
  245. package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -1
  246. package/src/components/Modals/NewChatModal/ModalContent.tsx +6 -3
  247. package/src/components/Modals/NewChatModal/UpdNewChatModal.tsx +12 -1
  248. package/src/components/Modals/UserProfileModal/EditUserModal.tsx +9 -25
  249. package/src/components/RoomComponents/ChatRoomItem.tsx +9 -1
  250. package/src/components/RoomComponents/styled/LastMessageFile.tsx +14 -2
  251. package/src/components/RoomComponents/styled/LastMessagePhoto.tsx +4 -1
  252. package/src/components/RoomComponents/styled/LastMessageVideo.tsx +4 -1
  253. package/src/components/styled/AudioMessage.tsx +2 -1
  254. package/src/components/styled/MessageImage.tsx +45 -6
  255. package/src/components/styled/VideoMessage.tsx +10 -1
  256. package/src/context/xmppProvider.tsx +47 -23
  257. package/src/helpers/ensureScopedChatCache.ts +2 -2
  258. package/src/helpers/initXmppRooms.tsx +5 -1
  259. package/src/helpers/persistCrypto.ts +127 -0
  260. package/src/helpers/pickImageAsset.ts +68 -0
  261. package/src/helpers/resolveInitBeforeLoadUser.ts +125 -18
  262. package/src/helpers/secureFileUrl.ts +103 -0
  263. package/src/helpers/secureKeyValue.ts +80 -0
  264. package/src/helpers/secureStoreRuntime.ts +62 -0
  265. package/src/helpers/secureUserStorage.ts +80 -0
  266. package/src/hooks/useChatWrapperInit.ts +3 -3
  267. package/src/hooks/useFileToken.ts +7 -0
  268. package/src/hooks/useLogout.tsx +15 -9
  269. package/src/hooks/useSendMessage.tsx +28 -13
  270. package/src/main.ts +22 -1
  271. package/src/networking/api-requests/auth.api.ts +125 -52
  272. package/src/networking/api-requests/roomMembers.api.ts +1 -1
  273. package/src/networking/api-requests/rooms.api.ts +9 -9
  274. package/src/networking/api-requests/user.api.ts +8 -7
  275. package/src/networking/apiClient.ts +68 -123
  276. package/src/networking/authRefresh.ts +430 -0
  277. package/src/networking/xmppClient.ts +37 -5
  278. package/src/roomStore/chatSettingsSlice.ts +31 -9
  279. package/src/roomStore/persistence.ts +39 -19
  280. package/src/roomStore/roomsSlice.ts +2 -0
  281. package/src/types/types.ts +17 -0
@@ -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,
@@ -49,8 +49,7 @@ import {
49
49
  setUnreadSyncing,
50
50
  } from '../roomStore/roomsSlice';
51
51
  import { runHistoryPreloadScheduler } from '../helpers/historyPreloadScheduler';
52
- import { asyncLocalStorage } from '../hooks/useLocalStorage';
53
- import { localStorageConstants } from '../helpers/constants/LOCAL_STORAGE';
52
+ import { secureUserStorage } from '../helpers/secureUserStorage';
54
53
  import { clearPersistedState } from '../roomStore/persistence';
55
54
  import { pushLog as devPushLog } from '../utils/devLogger';
56
55
 
@@ -107,23 +106,25 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
107
106
  // the XmppClient at least doesn't crash on `undefined.password`.
108
107
  const credentialsProvider = useMemo<XmppCredentialsProvider>(() => {
109
108
  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);
109
+ // 1. Make sure the REST tokens are fresh before re-minting XMPP
110
+ // creds. This used to call `config.refreshTokens.refreshFunction`
111
+ // directly and then fall into step 2, which refreshes again —
112
+ // two rotations back to back, which the new backend scheme
113
+ // reads as a race at best and token reuse at worst. One call to
114
+ // the shared rotation point covers both the consumer-supplied
115
+ // function and the built-in endpoint.
116
+ if (config?.refreshTokens?.enabled) {
117
+ const rotated = await refreshAuthTokensQuietly();
118
+
119
+ if (rotated?.xmppPassword) {
120
+ const rotatedUser = store.getState().chatSettingStore.user;
121
+ return {
122
+ username:
123
+ rotatedUser?.xmppUsername ||
124
+ rotatedUser?.defaultWallet?.walletAddress ||
125
+ '',
126
+ password: rotated.xmppPassword,
127
+ };
127
128
  }
128
129
  }
129
130
 
@@ -163,7 +164,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
163
164
  }, [
164
165
  config?.jwtLogin?.enabled,
165
166
  config?.jwtLogin?.token,
166
- config?.refreshTokens?.refreshFunction,
167
+ config?.refreshTokens?.enabled,
167
168
  config?.userLogin?.enabled,
168
169
  config?.customLogin?.enabled,
169
170
  config?.initBeforeLoadAuth?.myEndpoint,
@@ -322,7 +323,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
322
323
  devPushLog('rn', 'initBeforeLoad: clearStoreBeforeInit');
323
324
  store.dispatch(setLogoutState());
324
325
  store.dispatch(logout());
325
- await asyncLocalStorage(localStorageConstants.ETHORA_USER).remove().catch(() => undefined);
326
+ await secureUserStorage().remove().catch(() => undefined);
326
327
  }
327
328
 
328
329
  await ensureScopedChatCache(config);
@@ -353,6 +354,29 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
353
354
  store.dispatch(setConfig(config));
354
355
  }
355
356
 
357
+ // Rotate BEFORE the first XMPP connect. On the rotating backend
358
+ // the xmpp password is a short-lived JWT that only the newest
359
+ // rotation knows, and each rotation invalidates its predecessors
360
+ // — so a snapshot's password (a host's cached `userLogin.user`,
361
+ // a persisted session) is usually already dead at boot. Without
362
+ // this, the first SASL attempt fails `not-authorized`, all three
363
+ // bootstrap retries re-present the same dead password, and the
364
+ // user dead-ends on the Connection error modal even though one
365
+ // rotation away a perfectly good session existed. The rotation
366
+ // also lands the fileToken pre-render, so secure media never
367
+ // paints blank first. Quiet + deduped; when it can't rotate
368
+ // (no refresh material, non-rotating backend) we just connect
369
+ // with what the snapshot has, exactly as before.
370
+ // jwtLogin is skipped: /users/client just minted fresh creds.
371
+ let connectPassword = resolved.xmppPassword;
372
+ if (config?.refreshTokens?.enabled && !config?.jwtLogin?.enabled) {
373
+ const rotated = await refreshAuthTokensQuietly();
374
+ if (cancelled) {return;}
375
+ if (rotated?.xmppPassword) {
376
+ connectPassword = rotated.xmppPassword;
377
+ }
378
+ }
379
+
356
380
  // Kick off REST /chats/my; allRoomPresences below needs the
357
381
  // room list to be in redux, otherwise it joins 0 MUCs and the
358
382
  // user receives zero realtime messages until they manually tap
@@ -362,7 +386,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
362
386
 
363
387
  const c = await initializeClient(
364
388
  resolved.xmppUsername!,
365
- resolved.xmppPassword,
389
+ connectPassword,
366
390
  config?.xmppSettings
367
391
  );
368
392
  if (cancelled) {return;}
@@ -3,7 +3,7 @@ import { store } from '../roomStore';
3
3
  import { logout } from '../roomStore/chatSettingsSlice';
4
4
  import { setLogoutState } from '../roomStore/roomsSlice';
5
5
  import { asyncLocalStorage } from '../hooks/useLocalStorage';
6
- import { localStorageConstants } from './constants/LOCAL_STORAGE';
6
+ import { secureUserStorage } from './secureUserStorage';
7
7
  import { clearRoomsRestCache } from '../networking/api-requests/rooms.api';
8
8
  import { clearPersistedState } from '../roomStore/persistence';
9
9
  import {
@@ -53,7 +53,7 @@ export async function ensureScopedChatCache(config?: IConfig): Promise<void> {
53
53
  clearRoomsRestCache();
54
54
  store.dispatch(setLogoutState());
55
55
  store.dispatch(logout());
56
- await asyncLocalStorage(localStorageConstants.ETHORA_USER).remove();
56
+ await secureUserStorage().remove();
57
57
  await clearPersistedState();
58
58
  }
59
59
 
@@ -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);
@@ -0,0 +1,127 @@
1
+ import CryptoJS from 'crypto-js';
2
+ import { secureGet, secureSet } from './secureKeyValue';
3
+
4
+ // AES-256-CBC at rest for the bulk persisted state (message history +
5
+ // room list) that `roomStore/persistence.ts` writes to AsyncStorage.
6
+ // This is envelope encryption, not a SecureStore write of the payload
7
+ // itself: expo-secure-store has historically capped a single value's
8
+ // size on Android (chunking exists in newer releases, but the payload
9
+ // here is unbounded — up to MESSAGE_LIMIT messages per room, times
10
+ // every room), so only the small symmetric KEY goes through the
11
+ // platform Keychain/Keystore (`secureKeyValue.ts`); the ciphertext
12
+ // itself stays in plain AsyncStorage, unreadable without that key.
13
+ const CIPHER_KEY_STORE_KEY = 'ethora_history_cipher_key';
14
+ const ENVELOPE_VERSION = 1;
15
+ const KEY_BYTES = 32; // AES-256
16
+ const IV_BYTES = 16; // one AES block
17
+
18
+ interface Envelope {
19
+ v: number;
20
+ iv: string; // base64
21
+ ct: string; // base64 ciphertext
22
+ }
23
+
24
+ /**
25
+ * `crypto-js`'s own `WordArray.random()` falls back to `Math.random()`
26
+ * when it can't detect a browser `window.crypto` — not safe for key/IV
27
+ * material. `react-native-get-random-values` (a REQUIRED peer
28
+ * dependency, imported at the top of `main.ts`/`index.js` before
29
+ * anything else in the SDK runs) polyfills the real thing onto
30
+ * `global.crypto.getRandomValues` instead; source bytes from that and
31
+ * hand them to crypto-js as a WordArray rather than trust its default.
32
+ */
33
+ const randomWordArray = (byteLength: number): CryptoJS.lib.WordArray => {
34
+ const bytes = new Uint8Array(byteLength);
35
+ (global as any).crypto.getRandomValues(bytes);
36
+ const words: number[] = [];
37
+ for (let i = 0; i < bytes.length; i += 4) {
38
+ words.push(
39
+ ((bytes[i] || 0) << 24) |
40
+ ((bytes[i + 1] || 0) << 16) |
41
+ ((bytes[i + 2] || 0) << 8) |
42
+ (bytes[i + 3] || 0)
43
+ );
44
+ }
45
+ return CryptoJS.lib.WordArray.create(words, byteLength);
46
+ };
47
+
48
+ let cachedKeyPromise: Promise<CryptoJS.lib.WordArray> | null = null;
49
+
50
+ /**
51
+ * One AES-256 key per install, generated on first use and persisted
52
+ * through the SecureStore/Keychain bridge. Reused across logins on
53
+ * purpose: the ciphertext it protects is deleted wholesale on logout
54
+ * (`clearPersistedState`), so there is no cross-account data for a
55
+ * stable key to expose — only the encrypted blob of whoever is
56
+ * currently logged in ever exists on disk at a given time.
57
+ */
58
+ const loadOrCreateKey = (): Promise<CryptoJS.lib.WordArray> => {
59
+ if (cachedKeyPromise) {return cachedKeyPromise;}
60
+ cachedKeyPromise = (async () => {
61
+ const existing = await secureGet(CIPHER_KEY_STORE_KEY);
62
+ if (existing) {
63
+ return CryptoJS.enc.Base64.parse(existing);
64
+ }
65
+ const key = randomWordArray(KEY_BYTES);
66
+ await secureSet(CIPHER_KEY_STORE_KEY, CryptoJS.enc.Base64.stringify(key));
67
+ return key;
68
+ })();
69
+ return cachedKeyPromise;
70
+ };
71
+
72
+ /** Encrypt a JSON-serialisable payload for storage. Returns the string to persist. */
73
+ export async function encryptForPersist(plaintext: string): Promise<string> {
74
+ const key = await loadOrCreateKey();
75
+ const iv = randomWordArray(IV_BYTES);
76
+ const encrypted = CryptoJS.AES.encrypt(plaintext, key, {
77
+ iv,
78
+ mode: CryptoJS.mode.CBC,
79
+ padding: CryptoJS.pad.Pkcs7,
80
+ });
81
+ const envelope: Envelope = {
82
+ v: ENVELOPE_VERSION,
83
+ iv: CryptoJS.enc.Base64.stringify(iv),
84
+ ct: encrypted.ciphertext.toString(CryptoJS.enc.Base64),
85
+ };
86
+ return JSON.stringify(envelope);
87
+ }
88
+
89
+ /**
90
+ * Decrypt a value previously written by `encryptForPersist`. Returns
91
+ * null (never throws) for anything that isn't a valid envelope for the
92
+ * current key — including a plaintext blob from a pre-encryption
93
+ * install: the app just re-hydrates that room/history from the server,
94
+ * same as any other empty-cache cold start.
95
+ */
96
+ export async function decryptFromPersist(
97
+ stored: string
98
+ ): Promise<string | null> {
99
+ try {
100
+ const envelope = JSON.parse(stored) as Partial<Envelope>;
101
+ if (
102
+ !envelope ||
103
+ envelope.v !== ENVELOPE_VERSION ||
104
+ !envelope.iv ||
105
+ !envelope.ct
106
+ ) {
107
+ return null;
108
+ }
109
+ const key = await loadOrCreateKey();
110
+ const iv = CryptoJS.enc.Base64.parse(envelope.iv);
111
+ const ciphertext = CryptoJS.enc.Base64.parse(envelope.ct);
112
+ const decrypted = CryptoJS.AES.decrypt(
113
+ { ciphertext } as CryptoJS.lib.CipherParams,
114
+ key,
115
+ { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
116
+ );
117
+ const plaintext = decrypted.toString(CryptoJS.enc.Utf8);
118
+ return plaintext || null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ /** Test seam only. */
125
+ export const __resetPersistCryptoKeyForTests = (): void => {
126
+ cachedKeyPromise = null;
127
+ };
@@ -0,0 +1,68 @@
1
+ import { Alert, Linking } from 'react-native';
2
+ import * as ImagePicker from 'expo-image-picker';
3
+
4
+ /**
5
+ * A picked image in the shape React Native's FormData understands.
6
+ *
7
+ * NOT a web `File`: RN streams a multipart part straight from a `file://`
8
+ * uri and has no usable Blob/File/atob path. Screens ported from the web
9
+ * client used to carry base64 -> Blob -> File helpers that could never
10
+ * have run here.
11
+ */
12
+ export interface PickedImage {
13
+ uri: string;
14
+ type: string;
15
+ name: string;
16
+ }
17
+
18
+ /**
19
+ * Ask for library permission, open the picker, and normalise the result.
20
+ *
21
+ * Returns `null` for every non-error outcome — permission refused, user
22
+ * cancelled — so call sites can simply keep their current value.
23
+ *
24
+ * Exists because the avatar pickers are wired to `onPress`, which hands
25
+ * the callback a SyntheticEvent. Storing that event as "the image" (what
26
+ * two screens used to do) rendered a pooled event as an <Image source>
27
+ * and then crashed with "Property is not configurable" when React tried
28
+ * to release it — on top of never letting the user pick anything.
29
+ */
30
+ export const pickImageAsset = async (): Promise<PickedImage | null> => {
31
+ try {
32
+ // expo-image-picker owns the permission prompt internally and
33
+ // returns a `granted` flag.
34
+ const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
35
+ if (!perm.granted) {
36
+ Alert.alert(
37
+ 'Permission required',
38
+ 'Photo library permission is needed to select images.',
39
+ [
40
+ { text: 'Cancel', style: 'cancel' },
41
+ { text: 'Open Settings', onPress: () => Linking.openSettings() },
42
+ ]
43
+ );
44
+ return null;
45
+ }
46
+
47
+ const result = await ImagePicker.launchImageLibraryAsync({
48
+ mediaTypes: ['images'],
49
+ allowsEditing: true,
50
+ aspect: [1, 1],
51
+ quality: 0.8,
52
+ });
53
+ if (result.canceled || !result.assets?.[0]) {
54
+ return null;
55
+ }
56
+
57
+ const asset = result.assets[0];
58
+ const originalName = asset.fileName || asset.uri.split('/').pop();
59
+ return {
60
+ uri: asset.uri,
61
+ type: asset.mimeType || 'image/jpeg',
62
+ name: originalName || `image_${Date.now()}.jpg`,
63
+ };
64
+ } catch (error) {
65
+ console.error('Failed to pick an image:', error);
66
+ return null;
67
+ }
68
+ };