@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,5 +1,5 @@
1
1
  import type React from 'react';
2
- import { useCallback, useMemo, useState } from 'react';
2
+ import { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import {
4
4
  View,
5
5
  Pressable,
@@ -17,6 +17,7 @@ import { getAccountDisplayName, getAccountFallbackHandle } from '@oxyhq/core';
17
17
  import { useAuth } from '../hooks/useAuth';
18
18
  import { useOxy } from '../context/OxyContext';
19
19
  import { useI18n } from '../hooks/useI18n';
20
+ import { registerAccountDialogConsumerHooks } from '../navigation/accountDialogManager';
20
21
 
21
22
  const isWeb = Platform.OS === 'web';
22
23
 
@@ -102,6 +103,9 @@ export interface ProfileButtonProps {
102
103
  const ProfileButton: React.FC<ProfileButtonProps> = ({
103
104
  expanded = true,
104
105
  avatarSize,
106
+ onNavigateManage,
107
+ onAddAccount,
108
+ onNavigateProfile,
105
109
  className,
106
110
  style,
107
111
  }) => {
@@ -127,6 +131,17 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
127
131
  openAccountDialog('accounts');
128
132
  }, [openAccountDialog]);
129
133
 
134
+ useEffect(() => {
135
+ if (!onNavigateManage && !onAddAccount && !onNavigateProfile) {
136
+ return undefined;
137
+ }
138
+ return registerAccountDialogConsumerHooks({
139
+ onNavigateManage,
140
+ onAddAccount,
141
+ onNavigateProfile,
142
+ });
143
+ }, [onNavigateManage, onAddAccount, onNavigateProfile]);
144
+
130
145
  const avatarUrl = useMemo(
131
146
  () => (user?.avatar ? oxyServices.getFileDownloadUrl(user.avatar, 'thumb') : undefined),
132
147
  [user?.avatar, oxyServices],
@@ -1,11 +1,10 @@
1
1
  import type React from 'react';
2
2
  import { View, type ViewStyle } from 'react-native';
3
- import { Ionicons } from '@expo/vector-icons';
4
- import { useTheme } from '@oxyhq/bloom/theme';
5
3
  import { H4, Text } from '@oxyhq/bloom/typography';
6
4
  import { Avatar } from '@oxyhq/bloom/avatar';
7
5
  import { Card } from '@oxyhq/bloom/card';
8
6
  import { PressableScale } from '@oxyhq/bloom/pressable-scale';
7
+ import AvatarCameraBadge from './AvatarCameraBadge';
9
8
 
10
9
  /**
11
10
  * ProfileSummaryCard — the shared "who am I" summary block rendered at the top
@@ -37,18 +36,6 @@ export interface ProfileSummaryCardProps {
37
36
  avatarAccessibilityLabel?: string;
38
37
  }
39
38
 
40
- const badgeStyle: ViewStyle = {
41
- position: 'absolute',
42
- right: 0,
43
- bottom: 0,
44
- width: 28,
45
- height: 28,
46
- borderRadius: 14,
47
- alignItems: 'center',
48
- justifyContent: 'center',
49
- borderWidth: 2,
50
- };
51
-
52
39
  const cardStyle: ViewStyle = {
53
40
  borderRadius: 20,
54
41
  marginBottom: 16,
@@ -67,8 +54,6 @@ const ProfileSummaryCard: React.FC<ProfileSummaryCardProps> = ({
67
54
  showCameraBadge = false,
68
55
  avatarAccessibilityLabel,
69
56
  }) => {
70
- const bloomTheme = useTheme();
71
-
72
57
  const avatar = <Avatar source={avatarUri} name={displayName} size={avatarSize} />;
73
58
 
74
59
  const avatarNode = onAvatarPress ? (
@@ -80,15 +65,7 @@ const ProfileSummaryCard: React.FC<ProfileSummaryCardProps> = ({
80
65
  className="mb-space-12"
81
66
  >
82
67
  {avatar}
83
- {showCameraBadge ? (
84
- <View style={badgeStyle} className="bg-fill-brand border-border-image">
85
- <Ionicons
86
- name="camera"
87
- size={14}
88
- color={bloomTheme.colors.primaryForeground}
89
- />
90
- </View>
91
- ) : null}
68
+ {showCameraBadge ? <AvatarCameraBadge /> : null}
92
69
  </PressableScale>
93
70
  ) : (
94
71
  <View className="mb-space-12">{avatar}</View>
@@ -1,14 +1,18 @@
1
1
  import React, { useCallback, useEffect, useMemo, useState, type ErrorInfo } from 'react';
2
- import { StyleSheet, Text, View } from 'react-native';
2
+ import { StyleSheet, Text, View, useWindowDimensions } from 'react-native';
3
3
  import { useStore } from 'zustand';
4
4
  import { useTheme } from '@oxyhq/bloom/theme';
5
- import { useDialogHeader, type DialogHeaderConfig } from '@oxyhq/bloom/dialog';
5
+ import {
6
+ useDialogFrame,
7
+ useDialogHeader,
8
+ type DialogHeaderConfig,
9
+ } from '@oxyhq/bloom/dialog';
6
10
  import type { SurfaceControls } from '@oxyhq/bloom/surfaces';
7
11
  import type { RouteName } from '../navigation/routes';
8
12
  import { getScreenComponent } from '../navigation/routes';
9
13
  import { pushSurfaceBackHandler } from '../navigation/surfaceBackBridge';
10
14
  import type { SurfaceNavStack } from '../navigation/surfaceNavStack';
11
- import { getSurfaceConfig, type SurfacePresentation } from '../navigation/surfaceRegistry';
15
+ import { getSurfaceConfig } from '../navigation/surfaceRegistry';
12
16
  import {
13
17
  closeAllRouteSurfaces,
14
18
  navigateWithinOrPresent,
@@ -76,8 +80,6 @@ export interface SurfaceScreenProps {
76
80
  navStack: SurfaceNavStack;
77
81
  /** Bloom's per-surface controls (dismiss / present a child) — the DEPTH axis. */
78
82
  surface: SurfaceControls;
79
- /** This surface's presentation, so `navigate` knows when to drill in vs. stack. */
80
- presentation: SurfacePresentation;
81
83
  /** Whether backdrop / Escape / back may dismiss at the root frame. Defaults to true. */
82
84
  dismissOnBackdrop?: boolean;
83
85
  }
@@ -94,7 +96,6 @@ export interface SurfaceScreenProps {
94
96
  function SurfaceScreen({
95
97
  navStack,
96
98
  surface,
97
- presentation,
98
99
  dismissOnBackdrop = true,
99
100
  }: SurfaceScreenProps) {
100
101
  const theme = useTheme();
@@ -109,17 +110,24 @@ function SurfaceScreen({
109
110
 
110
111
  const navigate = useCallback(
111
112
  (route: RouteName, props?: Record<string, unknown>) =>
112
- navigateWithinOrPresent(presentation, navStack, route, props),
113
- [navStack, presentation],
113
+ navigateWithinOrPresent(navStack, route, props),
114
+ [navStack],
114
115
  );
115
116
 
116
117
  const replace = useCallback(
117
118
  (route: RouteName, props?: Record<string, unknown>) =>
118
- replaceWithinOrPresent(presentation, navStack, route, props),
119
- [navStack, presentation],
119
+ replaceWithinOrPresent(navStack, route, props),
120
+ [navStack],
120
121
  );
121
122
 
122
- const dismiss = useCallback((result?: unknown) => navStack.requestDismiss(result), [navStack]);
123
+ // Screens' `dismiss(result)` resolves an active sub-flow (morphed-in avatar
124
+ // picker) back to its caller and pops to its entry frame; with no flow it
125
+ // dismisses the whole surface (a screen presented as its own surface) — the
126
+ // historical behaviour. `navStack.resolveFlowOrDismiss` owns that fork.
127
+ const dismiss = useCallback(
128
+ (result?: unknown) => navStack.resolveFlowOrDismiss(result),
129
+ [navStack],
130
+ );
123
131
 
124
132
  const canGoBack = useCallback(() => navStack.canGoBack(), [navStack]);
125
133
 
@@ -165,9 +173,35 @@ function SurfaceScreen({
165
173
  const [headerContent, setHeaderContent] = useState<SurfaceHeaderContent | null>(null);
166
174
  const headerContext = useMemo(() => ({ setContent: setHeaderContent }), []);
167
175
 
168
- const headerMode = useMemo(
169
- () => getSurfaceConfig(top.route, top.props).header,
170
- [top.route, top.props],
176
+ const config = useMemo(() => getSurfaceConfig(top.route, top.props), [top.route, top.props]);
177
+ const headerMode = config.header;
178
+
179
+ // --- Size morphing on nav-within ---------------------------------------
180
+ // The Dialog cannot see a drill-in from its own props (only this host
181
+ // re-renders when the nav stack moves), so it is told which frame is on
182
+ // screen. When that identity changes the surface MORPHS — the panel animates
183
+ // between the two frames' sizes instead of hard-cutting. A wizard step counts
184
+ // as a frame: it swaps the visible content inside one screen the same way.
185
+ // A `stacks` route opens its OWN surface (never a frame here); everything else
186
+ // morphs.
187
+ //
188
+ // A route may declare an EXPLICIT large morph target (`frameSize`) — the media
189
+ // selector's own-scroller grid, which the panel can't measure, grows the
190
+ // container to a near-full-height, wider card and shrinks back on exit. Resolve
191
+ // its viewport-relative height to px here (the Dialog clamps to the viewport).
192
+ const { height: viewportHeight } = useWindowDimensions();
193
+ const frameSize = useMemo(() => {
194
+ const spec = config.frameSize;
195
+ if (!spec) return undefined;
196
+ const height =
197
+ spec.heightRatio !== undefined ? Math.round(viewportHeight * spec.heightRatio) : undefined;
198
+ return { height, maxWidth: spec.maxWidth };
199
+ }, [config.frameSize, viewportHeight]);
200
+ useDialogFrame(
201
+ useMemo(
202
+ () => ({ key: `${top.route}#${top.step}`, morph: config.morph, size: frameSize }),
203
+ [top.route, top.step, config.morph, frameSize],
204
+ ),
171
205
  );
172
206
  // Show a back affordance whenever the surface can navigate back — either an
173
207
  // earlier frame in this surface's stack, or an earlier wizard step.
@@ -42,36 +42,88 @@ export const authChooserStyles = StyleSheet.create({
42
42
  fontSize: 12.5,
43
43
  marginTop: 1,
44
44
  },
45
- /** Prominent current-account header (Google-style collapsed row). */
46
- currentAccountRow: {
47
- flexDirection: 'row',
45
+ /**
46
+ * Animated-collapse container for the account switch list — the outer clip and
47
+ * the inner measuring wrapper. Style-based (NOT NativeWind): the container
48
+ * carries the animated `height`/`opacity` off a reanimated style, and the
49
+ * measuring wrapper must stay className-free so its `onLayout` fires on RN-Web.
50
+ */
51
+ collapse: {
52
+ overflow: 'hidden',
53
+ },
54
+ collapseMeasure: {
55
+ width: '100%',
56
+ },
57
+ /**
58
+ * The HERO block that opens the account menu: the current account's large
59
+ * avatar, a greeting, and the "Manage your Oxy account" pill — centred and
60
+ * deliberately chrome-free (no card, no grouped-section surface) so the
61
+ * grouped cards below read as the menu proper. The account's email sits above
62
+ * it in the Dialog's own nav bar, not here.
63
+ */
64
+ hero: {
48
65
  alignItems: 'center',
66
+ paddingTop: 4,
67
+ paddingBottom: 20,
49
68
  gap: 12,
50
- paddingVertical: 8,
51
- paddingHorizontal: 4,
52
69
  },
53
- currentAccountMeta: {
54
- flex: 1,
55
- minWidth: 0,
70
+ /**
71
+ * The current account's accent ring in the switch list, drawn OUTSIDE its
72
+ * avatar as an overlay so it adds no width to the row — every avatar in the
73
+ * list keeps the same footprint and the same content line. The 3px offset is
74
+ * the ring's 2px stroke plus a 1px gap.
75
+ */
76
+ currentAvatarRing: {
77
+ position: 'absolute',
78
+ top: -3,
79
+ left: -3,
80
+ right: -3,
81
+ bottom: -3,
82
+ borderWidth: 2,
83
+ borderRadius: 9999,
56
84
  },
57
- currentAccountName: {
58
- fontSize: 16,
59
- fontWeight: '700',
85
+ /**
86
+ * The hero avatar's pressable wrapper — the "change your photo" entry point.
87
+ * `relative` so the accent ring + camera badge (both absolutely positioned)
88
+ * anchor to the avatar box.
89
+ */
90
+ heroAvatarPressable: {
91
+ position: 'relative',
92
+ },
93
+ /** The hero avatar's accent ring — an overlay, like the row-level one. */
94
+ heroAvatarRing: {
95
+ position: 'absolute',
96
+ top: -4,
97
+ left: -4,
98
+ right: -4,
99
+ bottom: -4,
100
+ borderWidth: 3,
101
+ borderRadius: 9999,
60
102
  },
61
- currentAccountEmail: {
62
- fontSize: 13,
63
- marginTop: 2,
103
+ /** Greeting + address stacked tight, centred (the hero's own 12px gap sits
104
+ * between this block and the avatar / manage pill, not between these lines). */
105
+ heroNameBlock: {
106
+ alignItems: 'center',
107
+ gap: 2,
64
108
  },
65
- manageButton: {
66
- width: '100%',
67
- borderRadius: 14,
68
- marginTop: 4,
109
+ heroGreeting: {
110
+ fontSize: 22,
111
+ fontWeight: '600',
112
+ textAlign: 'center',
69
113
  },
70
- addBadge: {
71
- width: 40,
72
- height: 40,
73
- borderRadius: 9999,
74
- borderWidth: StyleSheet.hairlineWidth,
114
+ /** The account address (`username@oxy.so` / `@handle`) under the greeting. */
115
+ heroAddress: {
116
+ fontSize: 14,
117
+ textAlign: 'center',
118
+ },
119
+ /**
120
+ * The switch row's disclosure affordance — a filled circle around the chevron,
121
+ * right-aligned to the same 12px row inset every other trailing element uses.
122
+ */
123
+ chevronCircle: {
124
+ width: 32,
125
+ height: 32,
126
+ borderRadius: 16,
75
127
  alignItems: 'center',
76
128
  justifyContent: 'center',
77
129
  },
@@ -1,12 +1,70 @@
1
- import React, { useEffect, useMemo, useState, useCallback } from 'react';
2
- import { View, Text, useWindowDimensions, type LayoutChangeEvent } from 'react-native';
1
+ import React, { useMemo, useState, useCallback } from 'react';
2
+ import { View, Text, Image, useWindowDimensions, type LayoutChangeEvent } from 'react-native';
3
+ import { useQuery, keepPreviousData } from '@tanstack/react-query';
3
4
  import type { FileMetadata } from '@oxyhq/core';
4
5
 
6
+ type PhotoSize = { width: number; height: number };
7
+
8
+ /** Stable empty map so an unresolved query yields a referentially-stable value. */
9
+ const EMPTY_DIMENSIONS: Record<string, PhotoSize> = {};
10
+
11
+ /**
12
+ * Measure the intrinsic size of every photo whose thumbnail URL has resolved,
13
+ * via `Image.getSize`. Pure/async (no React state) so it can be a React Query
14
+ * `queryFn`. Photos without a resolved URL are skipped and re-measured on the
15
+ * next signature change; a load error falls back to 1×1 (→ the 4/3 default).
16
+ */
17
+ async function measurePhotoDimensions(
18
+ photos: FileMetadata[],
19
+ getThumbUrl: (photo: FileMetadata) => string | undefined,
20
+ ): Promise<Record<string, PhotoSize>> {
21
+ const measured: Record<string, PhotoSize> = {};
22
+ await Promise.all(
23
+ photos.map(
24
+ (photo) =>
25
+ new Promise<void>((resolve) => {
26
+ const url = getThumbUrl(photo);
27
+ if (!url) {
28
+ resolve();
29
+ return;
30
+ }
31
+ Image.getSize(
32
+ url,
33
+ (width, height) => {
34
+ measured[photo.id] = { width, height };
35
+ resolve();
36
+ },
37
+ () => {
38
+ measured[photo.id] = { width: 1, height: 1 };
39
+ resolve();
40
+ },
41
+ );
42
+ }),
43
+ ),
44
+ );
45
+ return measured;
46
+ }
47
+
48
+ /** Fixed photos-per-row for the justified grid (Apple Photos-style trio). */
49
+ export const PHOTOS_PER_ROW = 3;
50
+
51
+ /** Chunk a photo list into fixed-length rows (last row may be short). */
52
+ export function chunkPhotos(photos: FileMetadata[], perRow: number): FileMetadata[][] {
53
+ const rows: FileMetadata[][] = [];
54
+ for (let i = 0; i < photos.length; i += perRow) {
55
+ rows.push(photos.slice(i, i + perRow));
56
+ }
57
+ return rows;
58
+ }
59
+
5
60
  export interface JustifiedPhotoGridProps {
6
61
  photos: FileMetadata[];
7
- photoDimensions: { [key: string]: { width: number; height: number } };
8
- loadPhotoDimensions: (photos: FileMetadata[]) => Promise<void>;
9
- createJustifiedRows: (photos: FileMetadata[], containerWidth: number) => FileMetadata[][];
62
+ /**
63
+ * Resolve a private-safe thumbnail URL for a photo (or `undefined` while the
64
+ * URL is still resolving). The grid measures each photo's intrinsic size from
65
+ * this URL and re-measures automatically once a pending URL resolves.
66
+ */
67
+ getThumbUrl: (photo: FileMetadata) => string | undefined;
10
68
  renderJustifiedPhotoItem: (photo: FileMetadata, width: number, height: number, isLast: boolean) => React.ReactElement;
11
69
  textColor: string;
12
70
  /**
@@ -22,12 +80,15 @@ export interface JustifiedPhotoGridProps {
22
80
  /**
23
81
  * Responsive justified photo grid that stretches to the provided containerWidth.
24
82
  * Uses flex rows with proportional children widths instead of absolute pixel widths so it always fills.
83
+ *
84
+ * Owns its own intrinsic-dimension measurement: it calls `Image.getSize` on each
85
+ * photo's resolved thumbnail URL and reflows once real aspect ratios are known
86
+ * (falling back to 4/3 until then). Measurement re-runs whenever a photo is added
87
+ * OR its (asynchronously-resolved) private-safe URL first appears.
25
88
  */
26
89
  const JustifiedPhotoGrid: React.FC<JustifiedPhotoGridProps> = ({
27
90
  photos,
28
- photoDimensions,
29
- loadPhotoDimensions,
30
- createJustifiedRows,
91
+ getThumbUrl,
31
92
  renderJustifiedPhotoItem,
32
93
  textColor,
33
94
  containerWidth: explicitWidth,
@@ -52,11 +113,27 @@ const JustifiedPhotoGrid: React.FC<JustifiedPhotoGridProps> = ({
52
113
  const w = e.nativeEvent.layout.width;
53
114
  setMeasuredWidth(prev => (prev === w ? prev : w));
54
115
  }, [resolvedExplicitWidth]);
55
- // Ensure dimensions are loaded for displayed photos
56
- useEffect(() => {
57
- loadPhotoDimensions(photos);
58
- // eslint-disable-next-line react-hooks/exhaustive-deps
59
- }, [photos.map(p => p.id).join(',')]);
116
+
117
+ // Signature of every photo id + its currently-resolved URL — the measurement
118
+ // query key. A private tile's URL resolves asynchronously after it enters the
119
+ // list, so keying on THIS (not just the photo set) re-measures the moment a
120
+ // URL lands, not just when photos are added.
121
+ const urlSignature = useMemo(
122
+ () => photos.map((p) => `${p.id}:${getThumbUrl(p) ?? ''}`).join('|'),
123
+ [photos, getThumbUrl],
124
+ );
125
+
126
+ // Intrinsic dimensions via React Query (no effect): re-runs when the signature
127
+ // changes and reflows the justified rows. `keepPreviousData` holds the last
128
+ // measured map during a re-measure so tiles never flash back to the 4/3 default.
129
+ const dimensionsQuery = useQuery({
130
+ queryKey: ['justifiedPhotoDimensions', urlSignature],
131
+ queryFn: () => measurePhotoDimensions(photos, getThumbUrl),
132
+ placeholderData: keepPreviousData,
133
+ staleTime: Number.POSITIVE_INFINITY,
134
+ gcTime: 5 * 60 * 1000,
135
+ });
136
+ const photoDimensions = dimensionsQuery.data ?? EMPTY_DIMENSIONS;
60
137
 
61
138
  // Group photos by date first
62
139
  const photosByDate = useMemo(() => {
@@ -80,77 +157,74 @@ const JustifiedPhotoGrid: React.FC<JustifiedPhotoGridProps> = ({
80
157
  <View style={{ width: '100%' }} onLayout={onLayoutContainer}>
81
158
  {sortedDates.map((date: string) => {
82
159
  const dayPhotos = photosByDate[date];
83
- // createJustifiedRows should build rows such that the "ideal" height (availableWidth / totalAspect) stays within min/max.
84
- // We pass the effective container width.
85
- const dateWidth = dateWidths[date] ?? effectiveWidth; // fallback to overall width until measured
86
- const rows = dateWidth > 0 ? createJustifiedRows(dayPhotos, dateWidth) : [];
87
- return (
88
- <View
89
- key={date}
90
- style={{ marginBottom: 24, width: '100%' }}
91
- onLayout={e => onLayoutDate(date, e.nativeEvent.layout.width)}
92
- >
93
- <Text style={{ fontSize: 14, fontWeight: '600', marginBottom: 12, color: textColor }}>
94
- {new Date(date).toLocaleDateString(dateFormatLocale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
95
- </Text>
96
- <View style={{ width: '100%' }}>
97
- {rows.map((row, rowIndex) => {
98
- // Compute total aspect ratios using loaded dimensions (fallback 4/3)
99
- const aspects = row.map(p => {
100
- const dims = photoDimensions[p.id];
101
- return dims ? dims.width / dims.height : 4 / 3;
102
- });
103
- const totalAspect = aspects.reduce((a, b) => a + b, 0);
104
- const gapsTotal = gap * (row.length - 1);
105
- const availableWidth = dateWidth - gapsTotal;
106
- // Ideal height that perfectly fills width when preserving aspect ratios
107
- const idealHeight = availableWidth / totalAspect;
108
- // We rely on row construction keeping idealHeight within min/max bounds; if not, clamp but then distribute leftover/overflow.
109
- let rowHeight = idealHeight;
110
- let widthAdjustment = 0; // difference to distribute if clamped
111
- if (idealHeight < minRowHeight) {
112
- rowHeight = minRowHeight;
113
- widthAdjustment = availableWidth - rowHeight * totalAspect; // negative means overflow
114
- } else if (idealHeight > maxRowHeight) {
115
- rowHeight = maxRowHeight;
116
- widthAdjustment = availableWidth - rowHeight * totalAspect;
117
- }
118
-
119
- // Pre-compute widths maintaining aspect ratios
120
- let widths = aspects.map(ar => ar * rowHeight);
121
- // If we have widthAdjustment (due to clamping) distribute proportionally so row still fills exactly
122
- if (widthAdjustment !== 0) {
123
- const widthSum = widths.reduce((a, b) => a + b, 0);
124
- widths = widths.map(w => w + (w / widthSum) * widthAdjustment);
125
- }
126
-
127
- // To combat rounding issues, adjust last item width to fill precisely
128
- const widthSumRounded = widths.reduce((a, b) => a + b, 0);
129
- const roundingDiff = availableWidth - widthSumRounded;
130
- if (Math.abs(roundingDiff) > 0.5) {
131
- widths[widths.length - 1] += roundingDiff; // minimal correction
132
- }
133
-
134
- return (
135
- <View key={rowIndex} style={{ flexDirection: 'row', width: '100%', marginBottom: 4 }}>
136
- {row.map((p, i) => {
137
- const photoWidth = widths[i];
138
- return (
139
- <View
140
- key={p.id}
141
- style={{ width: photoWidth, height: rowHeight, marginRight: i === row.length - 1 ? 0 : gap }}
142
- >
143
- {renderJustifiedPhotoItem(p, photoWidth, rowHeight, i === row.length - 1)}
144
- </View>
145
- );
146
- })}
147
- </View>
148
- );
149
- })}
150
- </View>
151
- </View>
152
- );
153
- })}
160
+ // Fall back to the overall width until this section is measured.
161
+ const dateWidth = dateWidths[date] ?? effectiveWidth;
162
+ const rows = dateWidth > 0 ? chunkPhotos(dayPhotos, PHOTOS_PER_ROW) : [];
163
+ return (
164
+ <View
165
+ key={date}
166
+ style={{ marginBottom: 24, width: '100%' }}
167
+ onLayout={e => onLayoutDate(date, e.nativeEvent.layout.width)}
168
+ >
169
+ <Text style={{ fontSize: 14, fontWeight: '600', marginBottom: 12, color: textColor }}>
170
+ {new Date(date).toLocaleDateString(dateFormatLocale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
171
+ </Text>
172
+ <View style={{ width: '100%' }}>
173
+ {rows.map((row, rowIndex) => {
174
+ // Total aspect ratio of the row, using measured dims (fallback 4/3).
175
+ const aspects = row.map(p => {
176
+ const dims = photoDimensions[p.id];
177
+ return dims ? dims.width / dims.height : 4 / 3;
178
+ });
179
+ const totalAspect = aspects.reduce((a, b) => a + b, 0);
180
+ const gapsTotal = gap * (row.length - 1);
181
+ const availableWidth = dateWidth - gapsTotal;
182
+ // Ideal height that perfectly fills width when preserving aspect ratios.
183
+ const idealHeight = availableWidth / totalAspect;
184
+ // Clamp to min/max, then distribute the leftover/overflow proportionally.
185
+ let rowHeight = idealHeight;
186
+ let widthAdjustment = 0;
187
+ if (idealHeight < minRowHeight) {
188
+ rowHeight = minRowHeight;
189
+ widthAdjustment = availableWidth - rowHeight * totalAspect;
190
+ } else if (idealHeight > maxRowHeight) {
191
+ rowHeight = maxRowHeight;
192
+ widthAdjustment = availableWidth - rowHeight * totalAspect;
193
+ }
194
+
195
+ let widths = aspects.map(ar => ar * rowHeight);
196
+ if (widthAdjustment !== 0) {
197
+ const widthSum = widths.reduce((a, b) => a + b, 0);
198
+ widths = widths.map(w => w + (w / widthSum) * widthAdjustment);
199
+ }
200
+
201
+ // Correct any rounding drift on the last tile so the row fills exactly.
202
+ const widthSumRounded = widths.reduce((a, b) => a + b, 0);
203
+ const roundingDiff = availableWidth - widthSumRounded;
204
+ if (Math.abs(roundingDiff) > 0.5) {
205
+ widths[widths.length - 1] += roundingDiff;
206
+ }
207
+
208
+ return (
209
+ <View key={row[0]?.id ?? rowIndex} style={{ flexDirection: 'row', width: '100%', marginBottom: 4 }}>
210
+ {row.map((p, i) => {
211
+ const photoWidth = widths[i];
212
+ return (
213
+ <View
214
+ key={p.id}
215
+ style={{ width: photoWidth, height: rowHeight, marginRight: i === row.length - 1 ? 0 : gap }}
216
+ >
217
+ {renderJustifiedPhotoItem(p, photoWidth, rowHeight, i === row.length - 1)}
218
+ </View>
219
+ );
220
+ })}
221
+ </View>
222
+ );
223
+ })}
224
+ </View>
225
+ </View>
226
+ );
227
+ })}
154
228
  </View>
155
229
  );
156
230
  };
@@ -0,0 +1,51 @@
1
+ import type { FileMetadata } from '@oxyhq/core';
2
+ import { chunkPhotos, PHOTOS_PER_ROW } from '../JustifiedPhotoGrid';
3
+
4
+ // `chunkPhotos` is the row-building logic the justified photo grid owns (moved
5
+ // out of FileManagementScreen's former `createJustifiedRows`). It is pure, so it
6
+ // is unit-testable — unlike the grid's intrinsic-dimension measurement, which
7
+ // depends on native `Image.getSize`.
8
+ const photo = (id: string): FileMetadata => ({
9
+ id,
10
+ filename: `${id}.jpg`,
11
+ contentType: 'image/jpeg',
12
+ length: 1,
13
+ chunkSize: 0,
14
+ uploadDate: '2026-01-01T00:00:00.000Z',
15
+ metadata: {},
16
+ variants: [],
17
+ });
18
+
19
+ const ids = (rows: FileMetadata[][]): string[][] => rows.map((row) => row.map((p) => p.id));
20
+
21
+ describe('chunkPhotos', () => {
22
+ it('returns no rows for an empty list', () => {
23
+ expect(chunkPhotos([], PHOTOS_PER_ROW)).toEqual([]);
24
+ });
25
+
26
+ it('groups photos into full rows of the given size', () => {
27
+ const photos = ['a', 'b', 'c', 'd', 'e', 'f'].map(photo);
28
+ expect(ids(chunkPhotos(photos, PHOTOS_PER_ROW))).toEqual([
29
+ ['a', 'b', 'c'],
30
+ ['d', 'e', 'f'],
31
+ ]);
32
+ });
33
+
34
+ it('leaves the final row short when the count is not a multiple of the row size', () => {
35
+ const photos = ['a', 'b', 'c', 'd'].map(photo);
36
+ expect(ids(chunkPhotos(photos, PHOTOS_PER_ROW))).toEqual([
37
+ ['a', 'b', 'c'],
38
+ ['d'],
39
+ ]);
40
+ });
41
+
42
+ it('preserves order and never drops or duplicates a photo', () => {
43
+ const photos = Array.from({ length: 10 }, (_, i) => photo(`p${i}`));
44
+ const flat = chunkPhotos(photos, PHOTOS_PER_ROW).flat().map((p) => p.id);
45
+ expect(flat).toEqual(photos.map((p) => p.id));
46
+ });
47
+
48
+ it('defaults to a trio per row', () => {
49
+ expect(PHOTOS_PER_ROW).toBe(3);
50
+ });
51
+ });