@oxyhq/services 22.10.0 → 22.11.0

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 (274) hide show
  1. package/lib/commonjs/ui/components/AvatarCameraBadge.js +42 -0
  2. package/lib/commonjs/ui/components/AvatarCameraBadge.js.map +1 -0
  3. package/lib/commonjs/ui/components/OxyAccountDialogScreen.js +41 -19
  4. package/lib/commonjs/ui/components/OxyAccountDialogScreen.js.map +1 -1
  5. package/lib/commonjs/ui/components/OxyAuthChooser.js +640 -168
  6. package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
  7. package/lib/commonjs/ui/components/ProfileButton.js +14 -0
  8. package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
  9. package/lib/commonjs/ui/components/ProfileSummaryCard.js +3 -23
  10. package/lib/commonjs/ui/components/ProfileSummaryCard.js.map +1 -1
  11. package/lib/commonjs/ui/components/SurfaceScreen.js +41 -5
  12. package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -1
  13. package/lib/commonjs/ui/components/oxyAuthChooserStyles.js +78 -26
  14. package/lib/commonjs/ui/components/oxyAuthChooserStyles.js.map +1 -1
  15. package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js +83 -23
  16. package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
  17. package/lib/commonjs/ui/context/OxyContext.js +73 -37
  18. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  19. package/lib/commonjs/ui/hooks/queries/queryKeys.js +18 -1
  20. package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/queries/useFileQueries.js +129 -0
  22. package/lib/commonjs/ui/hooks/queries/useFileQueries.js.map +1 -0
  23. package/lib/commonjs/ui/hooks/useAvatarPicker.js +36 -48
  24. package/lib/commonjs/ui/hooks/useAvatarPicker.js.map +1 -1
  25. package/lib/commonjs/ui/hooks/useReduceMotion.js +57 -0
  26. package/lib/commonjs/ui/hooks/useReduceMotion.js.map +1 -0
  27. package/lib/commonjs/ui/hooks/useSurfaceHeader.js +1 -1
  28. package/lib/commonjs/ui/hooks/useSurfaceHeader.js.map +1 -1
  29. package/lib/commonjs/ui/navigation/accountDialogManager.js +22 -0
  30. package/lib/commonjs/ui/navigation/accountDialogManager.js.map +1 -1
  31. package/lib/commonjs/ui/navigation/bottomSheetManager.js +5 -6
  32. package/lib/commonjs/ui/navigation/bottomSheetManager.js.map +1 -1
  33. package/lib/commonjs/ui/navigation/routes.js +1 -0
  34. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  35. package/lib/commonjs/ui/navigation/surfaceNavStack.js +62 -15
  36. package/lib/commonjs/ui/navigation/surfaceNavStack.js.map +1 -1
  37. package/lib/commonjs/ui/navigation/surfaceRegistry.js +38 -17
  38. package/lib/commonjs/ui/navigation/surfaceRegistry.js.map +1 -1
  39. package/lib/commonjs/ui/navigation/surfaces.js +98 -21
  40. package/lib/commonjs/ui/navigation/surfaces.js.map +1 -1
  41. package/lib/commonjs/ui/screens/AvatarCropScreen.js +281 -279
  42. package/lib/commonjs/ui/screens/AvatarCropScreen.js.map +1 -1
  43. package/lib/commonjs/ui/screens/ChangeAvatarScreen.js +301 -0
  44. package/lib/commonjs/ui/screens/ChangeAvatarScreen.js.map +1 -0
  45. package/lib/commonjs/ui/screens/FileManagementScreen.js +194 -431
  46. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  47. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +16 -48
  48. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  49. package/lib/commonjs/ui/screens/fileManagement/FileLibraryError.js +63 -0
  50. package/lib/commonjs/ui/screens/fileManagement/FileLibraryError.js.map +1 -0
  51. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +54 -41
  52. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  53. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +44 -81
  54. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  55. package/lib/commonjs/ui/stores/resetSessionScopedStores.js +8 -3
  56. package/lib/commonjs/ui/stores/resetSessionScopedStores.js.map +1 -1
  57. package/lib/commonjs/ui/utils/avatarUtils.js +11 -2
  58. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  59. package/lib/commonjs/ui/utils/fileManagement.js +0 -79
  60. package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
  61. package/lib/module/ui/components/AvatarCameraBadge.js +40 -0
  62. package/lib/module/ui/components/AvatarCameraBadge.js.map +1 -0
  63. package/lib/module/ui/components/OxyAccountDialogScreen.js +41 -19
  64. package/lib/module/ui/components/OxyAccountDialogScreen.js.map +1 -1
  65. package/lib/module/ui/components/OxyAuthChooser.js +642 -171
  66. package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
  67. package/lib/module/ui/components/ProfileButton.js +15 -1
  68. package/lib/module/ui/components/ProfileButton.js.map +1 -1
  69. package/lib/module/ui/components/ProfileSummaryCard.js +2 -23
  70. package/lib/module/ui/components/ProfileSummaryCard.js.map +1 -1
  71. package/lib/module/ui/components/SurfaceScreen.js +43 -7
  72. package/lib/module/ui/components/SurfaceScreen.js.map +1 -1
  73. package/lib/module/ui/components/oxyAuthChooserStyles.js +78 -26
  74. package/lib/module/ui/components/oxyAuthChooserStyles.js.map +1 -1
  75. package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js +83 -25
  76. package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
  77. package/lib/module/ui/context/OxyContext.js +74 -38
  78. package/lib/module/ui/context/OxyContext.js.map +1 -1
  79. package/lib/module/ui/hooks/queries/queryKeys.js +16 -0
  80. package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
  81. package/lib/module/ui/hooks/queries/useFileQueries.js +120 -0
  82. package/lib/module/ui/hooks/queries/useFileQueries.js.map +1 -0
  83. package/lib/module/ui/hooks/useAvatarPicker.js +37 -49
  84. package/lib/module/ui/hooks/useAvatarPicker.js.map +1 -1
  85. package/lib/module/ui/hooks/useReduceMotion.js +53 -0
  86. package/lib/module/ui/hooks/useReduceMotion.js.map +1 -0
  87. package/lib/module/ui/hooks/useSurfaceHeader.js +1 -1
  88. package/lib/module/ui/hooks/useSurfaceHeader.js.map +1 -1
  89. package/lib/module/ui/navigation/accountDialogManager.js +20 -0
  90. package/lib/module/ui/navigation/accountDialogManager.js.map +1 -1
  91. package/lib/module/ui/navigation/bottomSheetManager.js +6 -7
  92. package/lib/module/ui/navigation/bottomSheetManager.js.map +1 -1
  93. package/lib/module/ui/navigation/routes.js +1 -0
  94. package/lib/module/ui/navigation/routes.js.map +1 -1
  95. package/lib/module/ui/navigation/surfaceNavStack.js +62 -15
  96. package/lib/module/ui/navigation/surfaceNavStack.js.map +1 -1
  97. package/lib/module/ui/navigation/surfaceRegistry.js +38 -17
  98. package/lib/module/ui/navigation/surfaceRegistry.js.map +1 -1
  99. package/lib/module/ui/navigation/surfaces.js +97 -22
  100. package/lib/module/ui/navigation/surfaces.js.map +1 -1
  101. package/lib/module/ui/screens/AvatarCropScreen.js +283 -281
  102. package/lib/module/ui/screens/AvatarCropScreen.js.map +1 -1
  103. package/lib/module/ui/screens/ChangeAvatarScreen.js +298 -0
  104. package/lib/module/ui/screens/ChangeAvatarScreen.js.map +1 -0
  105. package/lib/module/ui/screens/FileManagementScreen.js +196 -433
  106. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  107. package/lib/module/ui/screens/WelcomeNewUserScreen.js +17 -49
  108. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  109. package/lib/module/ui/screens/fileManagement/FileLibraryError.js +59 -0
  110. package/lib/module/ui/screens/fileManagement/FileLibraryError.js.map +1 -0
  111. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +54 -42
  112. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  113. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +45 -81
  114. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  115. package/lib/module/ui/stores/resetSessionScopedStores.js +8 -3
  116. package/lib/module/ui/stores/resetSessionScopedStores.js.map +1 -1
  117. package/lib/module/ui/utils/avatarUtils.js +11 -2
  118. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  119. package/lib/module/ui/utils/fileManagement.js +0 -78
  120. package/lib/module/ui/utils/fileManagement.js.map +1 -1
  121. package/lib/typescript/commonjs/ui/components/AvatarCameraBadge.d.ts +4 -0
  122. package/lib/typescript/commonjs/ui/components/AvatarCameraBadge.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/ui/components/OxyAccountDialogScreen.d.ts +2 -1
  124. package/lib/typescript/commonjs/ui/components/OxyAccountDialogScreen.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/ui/components/ProfileSummaryCard.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts +1 -4
  129. package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts +71 -19
  131. package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/ui/components/photogrid/JustifiedPhotoGrid.d.ts +10 -8
  133. package/lib/typescript/commonjs/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
  134. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts +9 -0
  136. package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/ui/hooks/queries/useFileQueries.d.ts +50 -0
  138. package/lib/typescript/commonjs/ui/hooks/queries/useFileQueries.d.ts.map +1 -0
  139. package/lib/typescript/commonjs/ui/hooks/useAvatarPicker.d.ts +15 -10
  140. package/lib/typescript/commonjs/ui/hooks/useAvatarPicker.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/ui/hooks/useReduceMotion.d.ts +14 -0
  142. package/lib/typescript/commonjs/ui/hooks/useReduceMotion.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts +1 -1
  144. package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/ui/navigation/accountDialogManager.d.ts +13 -0
  146. package/lib/typescript/commonjs/ui/navigation/accountDialogManager.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts +4 -5
  148. package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
  150. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  151. package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts +21 -0
  152. package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts.map +1 -1
  153. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts +73 -13
  154. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts.map +1 -1
  155. package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts +42 -11
  156. package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts.map +1 -1
  157. package/lib/typescript/commonjs/ui/screens/AvatarCropScreen.d.ts +18 -5
  158. package/lib/typescript/commonjs/ui/screens/AvatarCropScreen.d.ts.map +1 -1
  159. package/lib/typescript/commonjs/ui/screens/ChangeAvatarScreen.d.ts +34 -0
  160. package/lib/typescript/commonjs/ui/screens/ChangeAvatarScreen.d.ts.map +1 -0
  161. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  163. package/lib/typescript/commonjs/ui/screens/fileManagement/FileLibraryError.d.ts +23 -0
  164. package/lib/typescript/commonjs/ui/screens/fileManagement/FileLibraryError.d.ts.map +1 -0
  165. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts +4 -0
  166. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +10 -2
  168. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/ui/stores/resetSessionScopedStores.d.ts +8 -1
  170. package/lib/typescript/commonjs/ui/stores/resetSessionScopedStores.d.ts.map +1 -1
  171. package/lib/typescript/commonjs/ui/types/navigation.d.ts +2 -0
  172. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts.map +1 -1
  174. package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts +0 -48
  175. package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts.map +1 -1
  176. package/lib/typescript/module/ui/components/AvatarCameraBadge.d.ts +4 -0
  177. package/lib/typescript/module/ui/components/AvatarCameraBadge.d.ts.map +1 -0
  178. package/lib/typescript/module/ui/components/OxyAccountDialogScreen.d.ts +2 -1
  179. package/lib/typescript/module/ui/components/OxyAccountDialogScreen.d.ts.map +1 -1
  180. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
  181. package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
  182. package/lib/typescript/module/ui/components/ProfileSummaryCard.d.ts.map +1 -1
  183. package/lib/typescript/module/ui/components/SurfaceScreen.d.ts +1 -4
  184. package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -1
  185. package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts +71 -19
  186. package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts.map +1 -1
  187. package/lib/typescript/module/ui/components/photogrid/JustifiedPhotoGrid.d.ts +10 -8
  188. package/lib/typescript/module/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
  189. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  190. package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts +9 -0
  191. package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts.map +1 -1
  192. package/lib/typescript/module/ui/hooks/queries/useFileQueries.d.ts +50 -0
  193. package/lib/typescript/module/ui/hooks/queries/useFileQueries.d.ts.map +1 -0
  194. package/lib/typescript/module/ui/hooks/useAvatarPicker.d.ts +15 -10
  195. package/lib/typescript/module/ui/hooks/useAvatarPicker.d.ts.map +1 -1
  196. package/lib/typescript/module/ui/hooks/useReduceMotion.d.ts +14 -0
  197. package/lib/typescript/module/ui/hooks/useReduceMotion.d.ts.map +1 -0
  198. package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts +1 -1
  199. package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
  200. package/lib/typescript/module/ui/navigation/accountDialogManager.d.ts +13 -0
  201. package/lib/typescript/module/ui/navigation/accountDialogManager.d.ts.map +1 -1
  202. package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts +4 -5
  203. package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts.map +1 -1
  204. package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
  205. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  206. package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts +21 -0
  207. package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts.map +1 -1
  208. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts +73 -13
  209. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts.map +1 -1
  210. package/lib/typescript/module/ui/navigation/surfaces.d.ts +42 -11
  211. package/lib/typescript/module/ui/navigation/surfaces.d.ts.map +1 -1
  212. package/lib/typescript/module/ui/screens/AvatarCropScreen.d.ts +18 -5
  213. package/lib/typescript/module/ui/screens/AvatarCropScreen.d.ts.map +1 -1
  214. package/lib/typescript/module/ui/screens/ChangeAvatarScreen.d.ts +34 -0
  215. package/lib/typescript/module/ui/screens/ChangeAvatarScreen.d.ts.map +1 -0
  216. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  217. package/lib/typescript/module/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  218. package/lib/typescript/module/ui/screens/fileManagement/FileLibraryError.d.ts +23 -0
  219. package/lib/typescript/module/ui/screens/fileManagement/FileLibraryError.d.ts.map +1 -0
  220. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts +4 -0
  221. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  222. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +10 -2
  223. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  224. package/lib/typescript/module/ui/stores/resetSessionScopedStores.d.ts +8 -1
  225. package/lib/typescript/module/ui/stores/resetSessionScopedStores.d.ts.map +1 -1
  226. package/lib/typescript/module/ui/types/navigation.d.ts +2 -0
  227. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  228. package/lib/typescript/module/ui/utils/avatarUtils.d.ts.map +1 -1
  229. package/lib/typescript/module/ui/utils/fileManagement.d.ts +0 -48
  230. package/lib/typescript/module/ui/utils/fileManagement.d.ts.map +1 -1
  231. package/package.json +8 -4
  232. package/src/ui/components/AvatarCameraBadge.tsx +35 -0
  233. package/src/ui/components/OxyAccountDialogScreen.tsx +38 -15
  234. package/src/ui/components/OxyAuthChooser.tsx +722 -185
  235. package/src/ui/components/ProfileButton.tsx +16 -1
  236. package/src/ui/components/ProfileSummaryCard.tsx +2 -25
  237. package/src/ui/components/SurfaceScreen.tsx +48 -14
  238. package/src/ui/components/oxyAuthChooserStyles.ts +75 -23
  239. package/src/ui/components/photogrid/JustifiedPhotoGrid.tsx +158 -84
  240. package/src/ui/components/photogrid/__tests__/chunkPhotos.test.ts +51 -0
  241. package/src/ui/context/OxyContext.tsx +74 -34
  242. package/src/ui/hooks/queries/queryKeys.ts +15 -0
  243. package/src/ui/hooks/queries/useFileQueries.ts +162 -0
  244. package/src/ui/hooks/useAvatarPicker.ts +38 -53
  245. package/src/ui/hooks/useReduceMotion.ts +58 -0
  246. package/src/ui/hooks/useSurfaceHeader.ts +2 -1
  247. package/src/ui/navigation/__tests__/getSurfaceConfig.test.ts +55 -7
  248. package/src/ui/navigation/__tests__/surfaceNavStack.test.ts +81 -0
  249. package/src/ui/navigation/accountDialogManager.ts +28 -0
  250. package/src/ui/navigation/bottomSheetManager.ts +6 -7
  251. package/src/ui/navigation/routes.ts +3 -1
  252. package/src/ui/navigation/surfaceNavStack.ts +72 -9
  253. package/src/ui/navigation/surfaceRegistry.ts +91 -17
  254. package/src/ui/navigation/surfaces.ts +108 -29
  255. package/src/ui/screens/AvatarCropScreen.tsx +336 -298
  256. package/src/ui/screens/ChangeAvatarScreen.tsx +359 -0
  257. package/src/ui/screens/FileManagementScreen.tsx +189 -432
  258. package/src/ui/screens/WelcomeNewUserScreen.tsx +18 -48
  259. package/src/ui/screens/fileManagement/FileLibraryError.tsx +56 -0
  260. package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +56 -46
  261. package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +55 -71
  262. package/src/ui/stores/resetSessionScopedStores.ts +8 -3
  263. package/src/ui/types/navigation.ts +2 -0
  264. package/src/ui/utils/avatarUtils.ts +11 -2
  265. package/src/ui/utils/fileManagement.ts +1 -101
  266. package/lib/commonjs/ui/stores/fileStore.js +0 -153
  267. package/lib/commonjs/ui/stores/fileStore.js.map +0 -1
  268. package/lib/module/ui/stores/fileStore.js +0 -145
  269. package/lib/module/ui/stores/fileStore.js.map +0 -1
  270. package/lib/typescript/commonjs/ui/stores/fileStore.d.ts +0 -31
  271. package/lib/typescript/commonjs/ui/stores/fileStore.d.ts.map +0 -1
  272. package/lib/typescript/module/ui/stores/fileStore.d.ts +0 -31
  273. package/lib/typescript/module/ui/stores/fileStore.d.ts.map +0 -1
  274. package/src/ui/stores/fileStore.ts +0 -118
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
1
+ import React, { useState, useCallback, useMemo, useRef } from 'react';
2
2
  import {
3
3
  View,
4
4
  Text,
@@ -8,10 +8,9 @@ import {
8
8
  ActivityIndicator,
9
9
  RefreshControl,
10
10
  TextInput,
11
- Image,
12
- AccessibilityInfo,
13
11
  useWindowDimensions,
14
12
  } from 'react-native';
13
+ import { useQueryClient } from '@tanstack/react-query';
15
14
  import { Image as ExpoImage } from 'expo-image';
16
15
  import type { FileManagementScreenProps } from '../types/fileManagement';
17
16
  import { toast } from '@oxyhq/bloom';
@@ -20,13 +19,15 @@ import * as Skeleton from '@oxyhq/bloom/skeleton';
20
19
  import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
21
20
  import { Pressable } from 'react-native';
22
21
  import type { FileMetadata } from '@oxyhq/core';
23
- import { useFileStore, useFiles, useUploading as useUploadingStore, useUploadAggregateProgress, useDeleting as useDeletingStore } from '../stores/fileStore';
22
+ import { queryKeys } from '../hooks/queries/queryKeys';
23
+ import { useUserFilesInfinite, removeFileFromCache, patchFileMetadataInCache } from '../hooks/queries/useFileQueries';
24
24
  import { useSurfaceHeader } from '../hooks/useSurfaceHeader';
25
25
  import { SurfaceHeaderAction } from '../components/SurfaceHeaderAction';
26
26
  import JustifiedPhotoGrid from '../components/photogrid/JustifiedPhotoGrid';
27
27
  import { useTheme } from '@oxyhq/bloom/theme';
28
28
  import { useOxy } from '../context/OxyContext';
29
29
  import { useI18n } from '../hooks/useI18n';
30
+ import { useReduceMotion } from '../hooks/useReduceMotion';
30
31
  import { useUploadFile } from '../hooks/mutations/useAccountMutations';
31
32
  import {
32
33
  formatFileSize,
@@ -41,6 +42,7 @@ import { getErrorMessage } from './fileManagement/shared';
41
42
  import { useFileUploadState } from './fileManagement/hooks/useFileUploadState';
42
43
  import PhotoPickerView from './fileManagement/PhotoPickerSection';
43
44
  import FileListSection, { type FileListItem } from './fileManagement/FileListSection';
45
+ import FileLibraryError from './fileManagement/FileLibraryError';
44
46
  import UploadBar from './fileManagement/UploadBar';
45
47
  import { AnimatedButton } from '../components/fileManagement/AnimatedButton';
46
48
 
@@ -72,13 +74,11 @@ const screenStyles = StyleSheet.create({
72
74
  justifyContent: 'center',
73
75
  marginHorizontal: 1,
74
76
  },
75
- photoImage: { width: '100%', height: '100%', borderRadius: 8 },
76
77
  justifiedPhotoImage: { width: '100%', height: '100%', borderRadius: 6 },
77
78
  });
78
79
 
79
80
  const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
80
81
  onClose,
81
- theme,
82
82
  goBack,
83
83
  navigate,
84
84
  dismiss,
@@ -102,8 +102,35 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
102
102
  // against `user`, so switching shows/manages that account's files.
103
103
  const { user, oxyServices } = useOxy();
104
104
  const { t } = useI18n();
105
+ const queryClient = useQueryClient();
105
106
  const uploadFileMutation = useUploadFile();
106
- const files = useFiles();
107
+
108
+ // Files are owned by the ACTIVE account. `targetUserId` scopes the query key,
109
+ // so switching accounts (or an explicit `userId` prop) shows that account's
110
+ // library with no manual reset.
111
+ const targetUserId = userId || user?.id;
112
+
113
+ // The file list is React Query's, NOT a store: infinite-paginated, fetched on
114
+ // mount + on owner change, edited optimistically via cache helpers. The
115
+ // rendered list is the flattened pages.
116
+ const filesQuery = useUserFilesInfinite(targetUserId);
117
+ const files = useMemo(
118
+ () => filesQuery.data?.pages.flatMap((page) => page.files) ?? [],
119
+ [filesQuery.data],
120
+ );
121
+ // No owner resolved yet reads as "loading" (matches the old skeleton-until-
122
+ // first-load behaviour); `isLoading` is the first-page fetch only.
123
+ const isLoadingFiles = !targetUserId || filesQuery.isLoading;
124
+ const isRefreshingFiles = filesQuery.isRefetching;
125
+ const isFetchingMore = filesQuery.isFetchingNextPage;
126
+ const hasMoreFiles = filesQuery.hasNextPage ?? false;
127
+ // Terminal load failure with nothing cached — render a DISTINCT error state
128
+ // (not the empty state) so a failed load never looks like an empty library.
129
+ const hasLoadError = filesQuery.isError && files.length === 0;
130
+ const invalidateFiles = useCallback(() => {
131
+ queryClient.invalidateQueries({ queryKey: queryKeys.files.list(targetUserId) });
132
+ }, [queryClient, targetUserId]);
133
+
107
134
  const { width: windowWidth } = useWindowDimensions();
108
135
  // Prefer an explicit sheet width from the router; fall back to the window
109
136
  // so photo grids never size against a hardcoded 400px default.
@@ -111,38 +138,16 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
111
138
  typeof containerWidth === 'number' && containerWidth > 0
112
139
  ? containerWidth
113
140
  : windowWidth;
114
- const uploading = useUploadingStore();
115
- const uploadProgress = useUploadAggregateProgress();
116
- const deleting = useDeletingStore();
117
- const [loading, setLoading] = useState(true);
118
- const [refreshing, setRefreshing] = useState(false);
119
- const [paging, setPaging] = useState({ offset: 0, limit: 40, total: 0, hasMore: true, loadingMore: false });
141
+ // In-flight delete target (local UI state — was in the file store).
142
+ const [deletingId, setDeletingId] = useState<string | null>(null);
120
143
  // In selectMode we never open the detailed viewer
121
144
  const [openedFile, setOpenedFile] = useState<FileMetadata | null>(null);
122
145
  const [fileContent, setFileContent] = useState<string | null>(null);
123
146
  const [loadingFileContent, setLoadingFileContent] = useState(false);
124
147
  const [showFileDetailsInViewer, setShowFileDetailsInViewer] = useState(false);
125
- const [reduceMotion, setReduceMotion] = useState(false);
126
-
127
- // Detect reduce-motion preference once on mount + subscribe to changes.
128
- // Used by `PhotoPickerView` to skip cell stagger animations.
129
- useEffect(() => {
130
- let cancelled = false;
131
- AccessibilityInfo.isReduceMotionEnabled()
132
- .then((enabled) => {
133
- if (!cancelled) setReduceMotion(enabled);
134
- })
135
- .catch(() => {
136
- // Defaults to false; no action needed.
137
- });
138
- const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', (enabled) => {
139
- setReduceMotion(enabled);
140
- });
141
- return () => {
142
- cancelled = true;
143
- sub.remove();
144
- };
145
- }, []);
148
+ // Reduce-motion preference, read as an external store (no effect). Passed to
149
+ // `PhotoPickerView` to skip cell stagger animations.
150
+ const reduceMotion = useReduceMotion();
146
151
 
147
152
  // Image-only picker mode: when the consumer restricts to image MIME types
148
153
  // (e.g. avatar picker), photos grid is the more useful default view.
@@ -216,25 +221,54 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
216
221
 
217
222
  return sorted;
218
223
  }, [files, searchQuery, viewMode, sortBy, sortOrder]);
219
- const [photoDimensions, setPhotoDimensions] = useState<{ [key: string]: { width: number, height: number } }>({});
220
- const photoDimensionsRef = useRef(photoDimensions);
221
- photoDimensionsRef.current = photoDimensions;
222
- const [loadingDimensions, setLoadingDimensions] = useState(false);
223
- // Selection state
224
- const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set(initialSelectedIds));
225
- const [lastSelectedFileId, setLastSelectedFileId] = useState<string | null>(null);
224
+ // Selection state. `initialSelectedIds` is the INITIAL selection only (lazy
225
+ // init) there is no prop→state sync effect, since no caller mutates it
226
+ // after mount.
227
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set(initialSelectedIds));
226
228
  const scrollViewRef = useRef<ScrollView>(null);
227
229
  const photoScrollViewRef = useRef<ScrollView>(null);
228
- const containerRef = useRef<View>(null);
229
- useEffect(() => {
230
- if (initialSelectedIds?.length) {
231
- setSelectedIds(new Set(initialSelectedIds));
230
+
231
+ // Scroll the browse list/grid to a just-selected file. Driven directly from
232
+ // the selection event handler (double-rAF so the selection re-render has
233
+ // committed first) — not an effect. No-ops in the image-only picker, whose
234
+ // own FlatList is not either ScrollView ref.
235
+ const scrollToSelectedFile = useCallback((fileId: string) => {
236
+ if (viewMode === 'all' && scrollViewRef.current) {
237
+ const itemIndex = filteredFiles.findIndex(file => file.id === fileId);
238
+ if (itemIndex < 0) return;
239
+ // GroupedItem dense rows are ~65px, +30px when a description wraps.
240
+ const baseItemHeight = 65;
241
+ const descriptionHeight = 30;
242
+ let scrollPosition = 0;
243
+ for (let i = 0; i <= itemIndex && i < filteredFiles.length; i++) {
244
+ scrollPosition += baseItemHeight;
245
+ if (filteredFiles[i].metadata?.description) scrollPosition += descriptionHeight;
246
+ }
247
+ // Offset for the header/controls/search/stats chrome (~250px) so the
248
+ // item lands near the top, not under the chrome.
249
+ const finalScrollPosition = 250 + scrollPosition - 150;
250
+ requestAnimationFrame(() => {
251
+ requestAnimationFrame(() => {
252
+ scrollViewRef.current?.scrollTo({ y: Math.max(0, finalScrollPosition), animated: true });
253
+ });
254
+ });
255
+ } else if (viewMode === 'photos' && photoScrollViewRef.current) {
256
+ const photos = filteredFiles.filter(file => file.contentType.startsWith('image/'));
257
+ const photoIndex = photos.findIndex(p => p.id === fileId);
258
+ if (photoIndex < 0) return;
259
+ // Rough estimate for the justified grid (3 photos/row, variable heights).
260
+ const row = Math.floor(photoIndex / 3);
261
+ const finalScrollPosition = row * (150 + 4) - 100;
262
+ requestAnimationFrame(() => {
263
+ requestAnimationFrame(() => {
264
+ photoScrollViewRef.current?.scrollTo({ y: Math.max(0, finalScrollPosition), animated: true });
265
+ });
266
+ });
232
267
  }
233
- }, [initialSelectedIds]);
268
+ }, [viewMode, filteredFiles]);
234
269
 
235
270
  const toggleSelect = useCallback(async (file: FileMetadata) => {
236
- // Allow selection in regular mode for bulk operations
237
- // if (!selectMode) return;
271
+ // Selection is allowed in regular mode too (for bulk operations).
238
272
  if (disabledMimeTypes.length) {
239
273
  const blocked = disabledMimeTypes.some(mt => file.contentType === mt || file.contentType.startsWith(mt.endsWith('/') ? mt : `${mt}/`));
240
274
  if (blocked) {
@@ -253,9 +287,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
253
287
  }
254
288
  }
255
289
 
256
- // Track the selected file for scrolling
257
- setLastSelectedFileId(file.id);
258
-
259
290
  // Link file to entity if linkContext is provided
260
291
  if (linkContext) {
261
292
  try {
@@ -303,12 +334,13 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
303
334
  }
304
335
  return next;
305
336
  });
306
- }, [selectMode, multiSelect, onSelect, onClose, goBack, dismiss, disabledMimeTypes, maxSelection, afterSelect, defaultVisibility, oxyServices, linkContext]);
337
+ scrollToSelectedFile(file.id);
338
+ }, [multiSelect, onSelect, onClose, goBack, dismiss, disabledMimeTypes, maxSelection, afterSelect, defaultVisibility, oxyServices, linkContext, t, scrollToSelectedFile]);
307
339
 
308
340
  const confirmMultiSelection = useCallback(async () => {
309
341
  if (!selectMode || !multiSelect) return;
310
342
  const map: Record<string, FileMetadata> = {};
311
- files.forEach(f => { map[f.id] = f; });
343
+ for (const f of files) { map[f.id] = f; }
312
344
  const chosen = Array.from(selectedIds).map(id => map[id]).filter(Boolean);
313
345
 
314
346
  // Update visibility and link files if needed
@@ -373,70 +405,15 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
373
405
  const backgroundColor = colors.backgroundSecondary;
374
406
  const borderColor = colors.border;
375
407
 
376
- const targetUserId = userId || user?.id;
377
-
378
- const storeSetDeleting = useFileStore(s => s.setDeleting);
379
-
380
- const loadFiles = useCallback(async (mode: 'initial' | 'refresh' | 'silent' | 'more' = 'initial') => {
381
- if (!targetUserId) return;
382
-
383
- try {
384
- if (mode === 'refresh') {
385
- setRefreshing(true);
386
- } else if (mode === 'initial') {
387
- setLoading(true);
388
- setPaging(p => ({ ...p, offset: 0, hasMore: true }));
389
- } else if (mode === 'more') {
390
- // Prevent duplicate fetches
391
- setPaging(p => ({ ...p, loadingMore: true }));
392
- }
393
- const currentPaging = mode === 'more' ? (prevPagingRef.current ?? paging) : paging;
394
- const effectiveOffset = mode === 'more' ? currentPaging.offset + currentPaging.limit : 0;
395
- const response = await oxyServices.listUserFiles(currentPaging.limit, effectiveOffset);
396
- const assets: FileMetadata[] = (response.files || []).map((f: { id: string; originalName?: string; sha256?: string; mime?: string; size?: number; createdAt?: string; metadata?: Record<string, unknown>; variants?: unknown[] }) => ({
397
- id: f.id,
398
- filename: f.originalName ?? f.sha256 ?? '',
399
- contentType: f.mime ?? '',
400
- length: f.size ?? 0,
401
- chunkSize: 0,
402
- uploadDate: f.createdAt ?? '',
403
- metadata: f.metadata ?? {},
404
- variants: (f.variants ?? []) as FileMetadata['variants'],
405
- }));
406
- if (mode === 'more') {
407
- // append
408
- useFileStore.getState().setFiles(assets, { merge: true });
409
- setPaging(p => ({
410
- ...p,
411
- offset: effectiveOffset,
412
- total: response.total || (effectiveOffset + assets.length),
413
- hasMore: response.hasMore,
414
- loadingMore: false,
415
- }));
416
- } else {
417
- useFileStore.getState().setFiles(assets, { merge: false });
418
- setPaging(p => ({
419
- ...p,
420
- offset: 0,
421
- total: response.total || assets.length,
422
- hasMore: response.hasMore,
423
- loadingMore: false,
424
- }));
425
- }
426
- } catch (error: unknown) {
427
- toast.error(getErrorMessage(error) || t('fileManagement.toasts.loadFailed'));
428
- } finally {
429
- setLoading(false);
430
- setRefreshing(false);
431
- setPaging(p => ({ ...p, loadingMore: false }));
432
- }
433
- }, [targetUserId, oxyServices, paging]);
434
-
435
408
  // Self-contained document-picking + upload-preview state and handlers.
409
+ // Optimistic uploads edit the file query cache directly (see
410
+ // useFileQueries), so no loader is threaded in.
436
411
  const {
437
412
  isPickingDocument,
438
413
  pendingFiles,
439
414
  showUploadPreview,
415
+ uploading,
416
+ uploadProgress,
440
417
  handleFileUpload,
441
418
  handleConfirmUpload,
442
419
  handleCancelUpload,
@@ -453,105 +430,9 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
453
430
  onClose,
454
431
  selectedIds,
455
432
  setSelectedIds,
456
- loadFiles,
457
433
  t,
458
434
  });
459
435
 
460
- // Keep a ref to avoid stale closure when calculating next offset
461
- const prevPagingRef = useRef(paging);
462
- useEffect(() => { prevPagingRef.current = paging; }, [paging]);
463
-
464
- // (removed effect; filteredFiles is memoized)
465
-
466
- // Load photo dimensions for justified grid - unified approach using Image.getSize
467
- const loadPhotoDimensions = useCallback(async (photos: FileMetadata[]) => {
468
- if (photos.length === 0) return;
469
-
470
- setLoadingDimensions(true);
471
-
472
- // Snapshot ids missing from the cache at kick-off; concurrent runs may
473
- // overlap but each merge is applied via a functional updater.
474
- const photosToLoad = photos.filter((photo) => !photoDimensionsRef.current[photo.id]);
475
-
476
- if (photosToLoad.length === 0) {
477
- setLoadingDimensions(false);
478
- return;
479
- }
480
-
481
- try {
482
- const measured: { [key: string]: { width: number; height: number } } = {};
483
-
484
- await Promise.all(
485
- photosToLoad.map(async (photo) => {
486
- try {
487
- const downloadUrl = thumbSourceFor(photo);
488
- // URL not resolved yet — skip (do NOT record fallback
489
- // dims) so this photo is re-measured once its private-safe
490
- // URL resolves. Measuring the broken public URL is exactly
491
- // the bug we are fixing.
492
- if (!downloadUrl) {
493
- return;
494
- }
495
-
496
- // Unified approach using Image.getSize (works on all platforms)
497
- await new Promise<void>((resolve) => {
498
- Image.getSize(
499
- downloadUrl,
500
- (width: number, height: number) => {
501
- measured[photo.id] = { width, height };
502
- resolve();
503
- },
504
- () => {
505
- // Fallback dimensions
506
- measured[photo.id] = { width: 1, height: 1 };
507
- resolve();
508
- }
509
- );
510
- });
511
- } catch (error) {
512
- // Fallback dimensions for any errors
513
- measured[photo.id] = { width: 1, height: 1 };
514
- }
515
- })
516
- );
517
-
518
- if (Object.keys(measured).length > 0) {
519
- setPhotoDimensions((prev) => ({ ...prev, ...measured }));
520
- }
521
- } catch (error) {
522
- // Photo dimensions loading failed, continue without dimensions
523
- } finally {
524
- setLoadingDimensions(false);
525
- }
526
- }, [thumbSourceFor]);
527
-
528
- // Re-measure photo dimensions when their private-safe URLs resolve. The
529
- // justified grid's own trigger fires on the photo SET, not on URL
530
- // availability, so newly-resolved private tiles would otherwise keep their
531
- // skipped (unmeasured) state and render with fallback geometry.
532
- useEffect(() => {
533
- if (viewMode !== 'photos') return;
534
- const photos = filteredFiles.filter((file) => file.contentType.startsWith('image/'));
535
- if (photos.length > 0) {
536
- loadPhotoDimensions(photos);
537
- }
538
- }, [resolvedThumbUrls, viewMode, filteredFiles, loadPhotoDimensions]);
539
-
540
- // Create justified rows from photos with responsive algorithm
541
- const createJustifiedRows = useCallback((photos: FileMetadata[], containerWidth: number) => {
542
- if (photos.length === 0) return [];
543
-
544
- const rows: FileMetadata[][] = [];
545
- const photosPerRow = 3; // Fixed 3 photos per row for consistency
546
-
547
- for (let i = 0; i < photos.length; i += photosPerRow) {
548
- const rowPhotos = photos.slice(i, i + photosPerRow);
549
- rows.push(rowPhotos);
550
- }
551
-
552
- return rows;
553
- }, []);
554
-
555
436
  const confirmFileDelete = useCallback(async (fileId: string, filename: string) => {
556
437
  const confirmed = await surfaces.confirm({
557
438
  title: t('fileManagement.deleteFile') || 'Delete File',
@@ -563,32 +444,29 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
563
444
  if (!confirmed) return;
564
445
 
565
446
  try {
566
- storeSetDeleting(fileId);
447
+ setDeletingId(fileId);
567
448
  await oxyServices.deleteFile(fileId);
568
449
 
569
450
  toast.success(t('fileManagement.toasts.deleteSuccess'));
570
451
 
571
- // Reload files after successful deletion
572
- // Optimistic remove
573
- useFileStore.getState().removeFile(fileId);
574
- // Silent background reconcile
575
- setTimeout(() => loadFiles('silent'), 800);
452
+ // Remove from the query cache, then reconcile against the server.
453
+ removeFileFromCache(queryClient, targetUserId, fileId);
454
+ invalidateFiles();
576
455
  } catch (error: unknown) {
577
-
578
456
  // Provide specific error messages
579
457
  if (getErrorMessage(error)?.includes('File not found') || getErrorMessage(error)?.includes('404')) {
580
458
  toast.error(t('fileManagement.toasts.fileNotFound'));
581
- // Still reload files to refresh the list
582
- setTimeout(() => loadFiles('silent'), 800);
459
+ // It's already gone server-side reconcile the list.
460
+ invalidateFiles();
583
461
  } else if (getErrorMessage(error)?.includes('permission') || getErrorMessage(error)?.includes('403')) {
584
462
  toast.error(t('fileManagement.toasts.noPermission'));
585
463
  } else {
586
464
  toast.error(getErrorMessage(error) || t('fileManagement.toasts.deleteFailed'));
587
465
  }
588
466
  } finally {
589
- storeSetDeleting(null);
467
+ setDeletingId(null);
590
468
  }
591
- }, [storeSetDeleting, oxyServices, loadFiles, t]);
469
+ }, [oxyServices, queryClient, targetUserId, invalidateFiles, t]);
592
470
 
593
471
  const confirmBulkDelete = useCallback(async () => {
594
472
  if (selectedIds.size === 0) return;
@@ -606,7 +484,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
606
484
  const deletePromises = Array.from(selectedIds).map(async (fileId) => {
607
485
  try {
608
486
  await oxyServices.deleteFile(fileId);
609
- useFileStore.getState().removeFile(fileId);
487
+ removeFileFromCache(queryClient, targetUserId, fileId);
610
488
  return { success: true, fileId };
611
489
  } catch (error: unknown) {
612
490
  return { success: false, fileId, error };
@@ -625,11 +503,11 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
625
503
  }
626
504
 
627
505
  setSelectedIds(new Set());
628
- setTimeout(() => loadFiles('silent'), 800);
506
+ invalidateFiles();
629
507
  } catch (error: unknown) {
630
508
  toast.error(getErrorMessage(error) || t('fileManagement.toasts.bulkDeleteError'));
631
509
  }
632
- }, [selectedIds, oxyServices, loadFiles, t]);
510
+ }, [selectedIds, oxyServices, queryClient, targetUserId, invalidateFiles, t]);
633
511
 
634
512
  const handleBulkVisibilityChange = useCallback(async (visibility: 'private' | 'public' | 'unlisted') => {
635
513
  if (selectedIds.size === 0) return;
@@ -650,22 +528,20 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
650
528
 
651
529
  if (successful > 0) {
652
530
  toast.success(t('fileManagement.toasts.visibilitySuccess', { count: successful, visibility }));
653
- // Update file metadata in store
654
- Array.from(selectedIds).forEach(fileId => {
655
- useFileStore.getState().updateFile(fileId, {
656
- metadata: { ...files.find(f => f.id === fileId)?.metadata, visibility } as Partial<FileMetadata>['metadata']
657
- });
658
- });
531
+ // Reflect the new visibility in the cached files.
532
+ for (const fileId of selectedIds) {
533
+ patchFileMetadataInCache(queryClient, targetUserId, fileId, { visibility });
534
+ }
659
535
  }
660
536
  if (failed > 0) {
661
537
  toast.error(t('fileManagement.toasts.visibilityFailed', { count: failed }));
662
538
  }
663
539
 
664
- setTimeout(() => loadFiles('silent'), 800);
540
+ invalidateFiles();
665
541
  } catch (error: unknown) {
666
542
  toast.error(getErrorMessage(error) || t('fileManagement.toasts.visibilityError'));
667
543
  }
668
- }, [selectedIds, oxyServices, files, loadFiles, t]);
544
+ }, [selectedIds, oxyServices, queryClient, targetUserId, invalidateFiles, t]);
669
545
 
670
546
  const handleVisibilityChange = useCallback(async () => {
671
547
  if (selectedIds.size === 0) return;
@@ -683,7 +559,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
683
559
  }, [selectedIds.size, handleBulkVisibilityChange, t]);
684
560
 
685
561
  // Unified download function - works on all platforms
686
- const handleFileDownload = async (fileId: string, filename: string) => {
562
+ const handleFileDownload = useCallback(async (fileId: string, filename: string) => {
687
563
  try {
688
564
  // Resolve an authenticated, private-safe URL. The synchronous
689
565
  // `getFileDownloadUrl` yields the public CDN origin, which 404s for
@@ -727,10 +603,9 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
727
603
  } catch (error: unknown) {
728
604
  toast.error(getErrorMessage(error) || t('fileManagement.toasts.downloadFailed'));
729
605
  }
730
- };
606
+ }, [oxyServices, t]);
731
607
 
732
-
733
- const handleFileOpen = async (file: FileMetadata) => {
608
+ const handleFileOpen = useCallback(async (file: FileMetadata) => {
734
609
  if (selectMode) {
735
610
  toggleSelect(file);
736
611
  return;
@@ -782,23 +657,23 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
782
657
  } finally {
783
658
  setLoadingFileContent(false);
784
659
  }
785
- };
660
+ }, [selectMode, toggleSelect, oxyServices, t]);
786
661
 
787
- const handleCloseFile = () => {
662
+ const handleCloseFile = useCallback(() => {
788
663
  setOpenedFile(null);
789
664
  setFileContent(null);
790
665
  setShowFileDetailsInViewer(false);
791
666
  // Don't reset view mode when closing a file
792
- };
667
+ }, []);
793
668
 
794
- const showFileDetailsModal = (file: FileMetadata) => {
669
+ const showFileDetailsModal = useCallback((file: FileMetadata) => {
795
670
  presentFileDetails({
796
671
  file,
797
672
  onDownload: handleFileDownload,
798
673
  onDelete: confirmFileDelete,
799
674
  isOwner: user?.id === targetUserId,
800
675
  });
801
- };
676
+ }, [handleFileDownload, confirmFileDelete, user?.id, targetUserId]);
802
677
 
803
678
  const renderJustifiedPhotoItem = useCallback((photo: FileMetadata, width: number, height: number, isLast: boolean) => {
804
679
  const downloadUrl = thumbSourceFor(photo);
@@ -836,18 +711,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
836
711
  </View>
837
712
  </TouchableOpacity>
838
713
  );
839
- }, [thumbSourceFor, selectMode, selectedIds, multiSelect, colors.primary, colors.text]);
840
-
841
- // Run initial load once per targetUserId change to avoid accidental loops
842
- const lastLoadedFor = useRef<string | undefined>(undefined);
843
- useEffect(() => {
844
- const key = targetUserId || 'anonymous';
845
- if (lastLoadedFor.current !== key) {
846
- lastLoadedFor.current = key;
847
- loadFiles('initial');
848
- }
849
- // eslint-disable-next-line react-hooks/exhaustive-deps
850
- }, [targetUserId]);
714
+ }, [thumbSourceFor, selectMode, selectedIds, multiSelect, colors.primary, colors.text, handleFileOpen]);
851
715
 
852
716
  // SettingsListItem-based file items (for 'all' view)
853
717
  const groupedFileItems: FileListItem[] = useMemo(() => {
@@ -945,9 +809,9 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
945
809
  className="w-[34px] h-[34px] rounded-[8px] items-center justify-center"
946
810
  style={{ backgroundColor: colors.negativeSubtle }}
947
811
  onPress={() => confirmFileDelete(file.id, file.filename)}
948
- disabled={deleting === file.id}
812
+ disabled={deletingId === file.id}
949
813
  >
950
- {deleting === file.id ? (
814
+ {deletingId === file.id ? (
951
815
  <ActivityIndicator size="small" color={colors.error} />
952
816
  ) : (
953
817
  <Ionicons name="trash" size={18} color={colors.error} />
@@ -957,89 +821,30 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
957
821
  ) : undefined,
958
822
  };
959
823
  });
960
- }, [filteredFiles, theme, deleting, handleFileDownload, confirmFileDelete, handleFileOpen, thumbSourceFor, selectMode, selectedIds]);
961
-
962
- // Scroll to selected file after selection
963
- useEffect(() => {
964
- if (lastSelectedFileId && selectMode) {
965
- if (viewMode === 'all' && scrollViewRef.current) {
966
- // Find the index of the selected file (filteredFiles is already sorted)
967
- const itemIndex = filteredFiles.findIndex(file => file.id === lastSelectedFileId);
968
-
969
- if (itemIndex >= 0) {
970
- // Estimate item height (GroupedItem with dense mode is approximately 60-70px)
971
- // Account for description rows which add extra height
972
- const baseItemHeight = 65;
973
- const descriptionHeight = 30; // Approximate height for description
974
- // Use filteredFiles which is already sorted according to user's selection
975
- const sortedFiles = filteredFiles;
976
-
977
- // Calculate total height up to this item
978
- let scrollPosition = 0;
979
- for (let i = 0; i <= itemIndex && i < sortedFiles.length; i++) {
980
- const file = sortedFiles[i];
981
- scrollPosition += baseItemHeight;
982
- if (file.metadata?.description) {
983
- scrollPosition += descriptionHeight;
984
- }
985
- }
986
-
987
- // Add header, controls, search, and stats height (approximately 250px)
988
- const headerHeight = 250;
989
- const finalScrollPosition = headerHeight + scrollPosition - 150; // Offset to show item near top
990
-
991
- // Use requestAnimationFrame to ensure DOM is updated before scrolling
992
- requestAnimationFrame(() => {
993
- requestAnimationFrame(() => {
994
- scrollViewRef.current?.scrollTo({
995
- y: Math.max(0, finalScrollPosition),
996
- animated: true,
997
- });
998
- });
999
- });
1000
- }
1001
- } else if (viewMode === 'photos' && photoScrollViewRef.current) {
1002
- // For photo grid, find the photo index
1003
- const photos = filteredFiles.filter(file => file.contentType.startsWith('image/'));
1004
- const photoIndex = photos.findIndex(p => p.id === lastSelectedFileId);
1005
-
1006
- if (photoIndex >= 0) {
1007
- // Rough scroll estimate for the justified grid (3 photos/row; variable row heights).
1008
- const itemsPerRow = 3;
1009
- const estimatedRowHeight = 150;
1010
- const rowGap = 4;
1011
- const row = Math.floor(photoIndex / itemsPerRow);
1012
- const finalScrollPosition = row * (estimatedRowHeight + rowGap) - 100;
1013
-
1014
- // Use requestAnimationFrame to ensure DOM is updated before scrolling
1015
- requestAnimationFrame(() => {
1016
- requestAnimationFrame(() => {
1017
- photoScrollViewRef.current?.scrollTo({
1018
- y: Math.max(0, finalScrollPosition),
1019
- animated: true,
1020
- });
1021
- });
1022
- });
1023
- }
1024
- }
1025
- }
1026
- }, [lastSelectedFileId, selectMode, viewMode, filteredFiles, safeContainerWidth]);
1027
-
1028
- // Clear selected file ID after scroll animation completes
1029
- useEffect(() => {
1030
- if (lastSelectedFileId && scrollViewRef.current) {
1031
- const timeoutId = setTimeout(() => {
1032
- setLastSelectedFileId(null);
1033
- }, 600); // Allow time for scroll animation to complete
1034
-
1035
- return () => clearTimeout(timeoutId);
1036
- }
1037
- }, [lastSelectedFileId]);
824
+ }, [filteredFiles, colors, deletingId, toggleSelect, handleFileDownload, confirmFileDelete, handleFileOpen, thumbSourceFor, selectMode, selectedIds]);
825
+
826
+ // Browse-themed terminal load-failure state DISTINCT from the empty state.
827
+ const renderLoadError = () => (
828
+ <FileLibraryError
829
+ title={t('fileManagement.loadError.title')}
830
+ description={t('fileManagement.loadError.description')}
831
+ retryLabel={t('fileManagement.retry')}
832
+ onRetry={() => filesQuery.refetch()}
833
+ iconColor={colors.error}
834
+ titleColor={colors.text}
835
+ descriptionColor={colors.textSecondary}
836
+ buttonColor={colors.primary}
837
+ />
838
+ );
1038
839
 
1039
- const renderPhotoGrid = useCallback(() => {
840
+ // Browse "photos" view. The justified grid owns its own dimension
841
+ // measurement + reflow (see JustifiedPhotoGrid); this only supplies the
842
+ // photo set, the private-safe URL resolver, and the tile renderer.
843
+ const renderPhotoGrid = () => {
1040
844
  const photos = filteredFiles.filter(file => file.contentType.startsWith('image/'));
1041
845
 
1042
846
  if (photos.length === 0) {
847
+ if (hasLoadError) return renderLoadError();
1043
848
  return (
1044
849
  <View className="items-center py-[40px] px-[24px]">
1045
850
  <Ionicons name="images-outline" size={64} color={colors.textTertiary} />
@@ -1056,9 +861,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1056
861
  onPress={handleFileUpload}
1057
862
  disabled={uploading || isPickingDocument}
1058
863
  >
1059
- {uploading ? (
1060
- <ActivityIndicator size="small" color="#FFFFFF" />
1061
- ) : isPickingDocument ? (
864
+ {(uploading || isPickingDocument) ? (
1062
865
  <ActivityIndicator size="small" color="#FFFFFF" />
1063
866
  ) : (
1064
867
  <>
@@ -1079,8 +882,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1079
882
  contentContainerClassName="p-[10px]"
1080
883
  refreshControl={
1081
884
  <RefreshControl
1082
- refreshing={refreshing}
1083
- onRefresh={() => loadFiles('refresh')}
885
+ refreshing={isRefreshingFiles}
886
+ onRefresh={() => filesQuery.refetch()}
1084
887
  tintColor={colors.primary}
1085
888
  />
1086
889
  }
@@ -1088,24 +891,15 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1088
891
  onScroll={({ nativeEvent }) => {
1089
892
  const { layoutMeasurement, contentOffset, contentSize } = nativeEvent;
1090
893
  const distanceFromBottom = contentSize.height - (contentOffset.y + layoutMeasurement.height);
1091
- if (distanceFromBottom < 200 && !paging.loadingMore && paging.hasMore) {
1092
- loadFiles('more');
894
+ if (distanceFromBottom < 200 && !isFetchingMore && hasMoreFiles) {
895
+ filesQuery.fetchNextPage();
1093
896
  }
1094
897
  }}
1095
898
  scrollEventThrottle={250}
1096
899
  >
1097
- {loadingDimensions && (
1098
- <View className="flex-row items-center justify-center py-[16px] gap-[8px]">
1099
- <ActivityIndicator size="small" color={colors.primary} />
1100
- <Text className="text-[14px] italic" style={{ color: colors.textSecondary }}>{t('fileManagement.loadingPhotoLayout')}</Text>
1101
- </View>
1102
- )}
1103
-
1104
900
  <JustifiedPhotoGrid
1105
901
  photos={photos}
1106
- photoDimensions={photoDimensions}
1107
- loadPhotoDimensions={loadPhotoDimensions}
1108
- createJustifiedRows={createJustifiedRows}
902
+ getThumbUrl={thumbSourceFor}
1109
903
  renderJustifiedPhotoItem={renderJustifiedPhotoItem}
1110
904
  textColor={colors.text}
1111
905
  containerWidth={
@@ -1116,28 +910,9 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1116
910
  />
1117
911
  </ScrollView>
1118
912
  );
1119
- }, [
1120
- filteredFiles,
1121
- colors,
1122
- user?.id,
1123
- targetUserId,
1124
- uploading,
1125
- handleFileUpload,
1126
- refreshing,
1127
- loadFiles,
1128
- loadingDimensions,
1129
- photoDimensions,
1130
- loadPhotoDimensions,
1131
- createJustifiedRows,
1132
- renderJustifiedPhotoItem,
1133
- containerWidth,
1134
- ]);
1135
-
1136
- // Inline justified grid removed (moved to components/photogrid/JustifiedPhotoGrid.tsx)
1137
-
1138
-
913
+ };
1139
914
 
1140
- const renderEmptyState = () => (
915
+ const renderEmptyState = () => hasLoadError ? renderLoadError() : (
1141
916
  <View className="items-center py-[40px] px-[24px]">
1142
917
  <Ionicons name="folder-open-outline" size={64} color={colors.textTertiary} />
1143
918
  <Text className="text-[24px] font-bold mt-[16px] mb-[8px]" style={{ color: colors.text }}>{t('fileManagement.emptyFiles.title')}</Text>
@@ -1154,9 +929,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1154
929
  onPress={handleFileUpload}
1155
930
  disabled={uploading || isPickingDocument}
1156
931
  >
1157
- {uploading ? (
1158
- <ActivityIndicator size="small" color="#FFFFFF" />
1159
- ) : isPickingDocument ? (
932
+ {(uploading || isPickingDocument) ? (
1160
933
  <ActivityIndicator size="small" color="#FFFFFF" />
1161
934
  ) : (
1162
935
  <>
@@ -1237,7 +1010,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1237
1010
  },
1238
1011
  );
1239
1012
 
1240
- if (loading && !isImageOnlyPicker) {
1013
+ if (isLoadingFiles && !isImageOnlyPicker) {
1241
1014
  const GRID_PADDING = 10;
1242
1015
  const TILE_GAP = 4;
1243
1016
  const GRID_COLUMNS = 3;
@@ -1304,13 +1077,15 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1304
1077
  multiSelect={multiSelect}
1305
1078
  maxSelection={maxSelection}
1306
1079
  allowUpload={allowUpload}
1307
- refreshing={refreshing}
1080
+ refreshing={isRefreshingFiles}
1308
1081
  uploading={uploading}
1309
1082
  isPickingDocument={isPickingDocument}
1310
1083
  uploadProgress={uploadProgress}
1311
- hasMore={paging.hasMore}
1312
- loadingMore={paging.loadingMore}
1313
- loading={loading}
1084
+ hasMore={hasMoreFiles}
1085
+ loadingMore={isFetchingMore}
1086
+ loading={isLoadingFiles}
1087
+ loadError={hasLoadError}
1088
+ onRetry={() => filesQuery.refetch()}
1314
1089
  reduceMotion={reduceMotion}
1315
1090
  getThumbUrl={thumbSourceFor}
1316
1091
  primaryColor={colors.primary}
@@ -1319,8 +1094,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1319
1094
  // Long-press preview surfaces the file-detail panel on the stack.
1320
1095
  onPreviewPhoto={(file) => showFileDetailsModal(file)}
1321
1096
  onUpload={handleFileUpload}
1322
- onRefresh={() => loadFiles('refresh')}
1323
- onLoadMore={() => loadFiles('more')}
1097
+ onRefresh={() => filesQuery.refetch()}
1098
+ onLoadMore={() => filesQuery.fetchNextPage()}
1324
1099
  onCancel={() => {
1325
1100
  if (onSelect || onConfirmSelection) {
1326
1101
  // Legacy callback caller: preserve close/back behaviour.
@@ -1400,37 +1175,33 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1400
1175
  style={screenStyles.viewModeButton}
1401
1176
  accessibilityLabel={t('fileManagement.a11y.viewPhotos') || 'Show photos only'}
1402
1177
  />
1403
- {!isImageOnlyPicker && (
1404
- <>
1405
- <AnimatedButton
1406
- isSelected={viewMode === 'videos'}
1407
- onPress={() => setViewMode('videos')}
1408
- icon={viewMode === 'videos' ? 'video' : 'video-outline'}
1409
- primaryColor={colors.primary}
1410
- textColor={colors.text}
1411
- style={screenStyles.viewModeButton}
1412
- accessibilityLabel={t('fileManagement.a11y.viewVideos') || 'Show videos only'}
1413
- />
1414
- <AnimatedButton
1415
- isSelected={viewMode === 'documents'}
1416
- onPress={() => setViewMode('documents')}
1417
- icon={viewMode === 'documents' ? 'file-document' : 'file-document-outline'}
1418
- primaryColor={colors.primary}
1419
- textColor={colors.text}
1420
- style={screenStyles.viewModeButton}
1421
- accessibilityLabel={t('fileManagement.a11y.viewDocuments') || 'Show documents only'}
1422
- />
1423
- <AnimatedButton
1424
- isSelected={viewMode === 'audio'}
1425
- onPress={() => setViewMode('audio')}
1426
- icon={viewMode === 'audio' ? 'music-note' : 'music-note-outline'}
1427
- primaryColor={colors.primary}
1428
- textColor={colors.text}
1429
- style={screenStyles.viewModeButton}
1430
- accessibilityLabel={t('fileManagement.a11y.viewAudio') || 'Show audio only'}
1431
- />
1432
- </>
1433
- )}
1178
+ <AnimatedButton
1179
+ isSelected={viewMode === 'videos'}
1180
+ onPress={() => setViewMode('videos')}
1181
+ icon={viewMode === 'videos' ? 'video' : 'video-outline'}
1182
+ primaryColor={colors.primary}
1183
+ textColor={colors.text}
1184
+ style={screenStyles.viewModeButton}
1185
+ accessibilityLabel={t('fileManagement.a11y.viewVideos') || 'Show videos only'}
1186
+ />
1187
+ <AnimatedButton
1188
+ isSelected={viewMode === 'documents'}
1189
+ onPress={() => setViewMode('documents')}
1190
+ icon={viewMode === 'documents' ? 'file-document' : 'file-document-outline'}
1191
+ primaryColor={colors.primary}
1192
+ textColor={colors.text}
1193
+ style={screenStyles.viewModeButton}
1194
+ accessibilityLabel={t('fileManagement.a11y.viewDocuments') || 'Show documents only'}
1195
+ />
1196
+ <AnimatedButton
1197
+ isSelected={viewMode === 'audio'}
1198
+ onPress={() => setViewMode('audio')}
1199
+ icon={viewMode === 'audio' ? 'music-note' : 'music-note-outline'}
1200
+ primaryColor={colors.primary}
1201
+ textColor={colors.text}
1202
+ style={screenStyles.viewModeButton}
1203
+ accessibilityLabel={t('fileManagement.a11y.viewAudio') || 'Show audio only'}
1204
+ />
1434
1205
  </View>
1435
1206
  </ScrollView>
1436
1207
  <TouchableOpacity
@@ -1468,18 +1239,14 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1468
1239
  color={colors.textSecondary}
1469
1240
  />
1470
1241
  </TouchableOpacity>
1471
- {user?.id === targetUserId && (!selectMode || (selectMode && allowUploadInSelectMode)) && (
1242
+ {user?.id === targetUserId && (!selectMode || allowUploadInSelectMode) && (
1472
1243
  <TouchableOpacity
1473
- className={`h-[44px] rounded-[22px] items-center justify-center ${isImageOnlyPicker ? 'w-auto min-w-[44px] px-[14px]' : 'w-[44px]'}`}
1244
+ className="h-[44px] w-[44px] rounded-[22px] items-center justify-center"
1474
1245
  style={{ backgroundColor: colors.primary }}
1475
1246
  onPress={handleFileUpload}
1476
1247
  disabled={uploading || isPickingDocument}
1477
1248
  accessibilityRole="button"
1478
- accessibilityLabel={
1479
- isImageOnlyPicker
1480
- ? (t('fileManagement.a11y.uploadFromDevice') || 'Upload photo from device')
1481
- : (t('fileManagement.a11y.uploadFile') || 'Upload file')
1482
- }
1249
+ accessibilityLabel={t('fileManagement.a11y.uploadFile') || 'Upload file'}
1483
1250
  accessibilityState={{ busy: uploading || isPickingDocument }}
1484
1251
  >
1485
1252
  {uploading ? (
@@ -1493,13 +1260,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1493
1260
  </View>
1494
1261
  ) : isPickingDocument ? (
1495
1262
  <ActivityIndicator size="small" color="#FFFFFF" />
1496
- ) : isImageOnlyPicker ? (
1497
- <View className="flex-row items-center gap-[6px]">
1498
- <Ionicons name="cloud-upload" size={18} color="#FFFFFF" />
1499
- <Text className="text-white text-[14px] font-semibold" numberOfLines={1}>
1500
- {t('fileManagement.upload') || 'Upload'}
1501
- </Text>
1502
- </View>
1503
1263
  ) : (
1504
1264
  <Ionicons name="add" size={22} color="#FFFFFF" />
1505
1265
  )}
@@ -1573,12 +1333,12 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1573
1333
  filteredFiles={filteredFiles}
1574
1334
  searchQuery={searchQuery}
1575
1335
  items={groupedFileItems}
1576
- paging={paging}
1577
- refreshing={refreshing}
1336
+ paging={{ loadingMore: isFetchingMore, hasMore: hasMoreFiles }}
1337
+ refreshing={isRefreshingFiles}
1578
1338
  colors={colors}
1579
1339
  t={t}
1580
- onRefresh={() => loadFiles('refresh')}
1581
- onLoadMore={() => loadFiles('more')}
1340
+ onRefresh={() => filesQuery.refetch()}
1341
+ onLoadMore={() => filesQuery.fetchNextPage()}
1582
1342
  onClearSearch={() => setSearchQuery('')}
1583
1343
  renderEmptyState={renderEmptyState}
1584
1344
  />
@@ -1593,9 +1353,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1593
1353
  t={t}
1594
1354
  />
1595
1355
  )}
1596
-
1597
- {/* Selection bar removed; actions are now in header */}
1598
- {/* Global loadingMore bar removed; now inline in scroll areas */}
1599
1356
  </View>
1600
1357
  );
1601
1358
  };