@oxyhq/services 22.4.2 → 22.6.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 (174) hide show
  1. package/LICENSE +661 -21
  2. package/README.md +1 -1
  3. package/lib/commonjs/index.js +20 -1
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/ui/client.js +1 -1
  6. package/lib/commonjs/ui/components/FontLoader.js +18 -145
  7. package/lib/commonjs/ui/components/FontLoader.js.map +1 -1
  8. package/lib/commonjs/ui/components/FontLoader.native.js +88 -0
  9. package/lib/commonjs/ui/components/FontLoader.native.js.map +1 -0
  10. package/lib/commonjs/ui/components/OxyProvider.js +1 -1
  11. package/lib/commonjs/ui/components/fileManagement/FileDetailsModal.js +16 -27
  12. package/lib/commonjs/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
  13. package/lib/commonjs/ui/components/fileManagement/UploadPreview.js.map +1 -1
  14. package/lib/commonjs/ui/components/fileManagement/styles.js +1 -225
  15. package/lib/commonjs/ui/components/fileManagement/styles.js.map +1 -1
  16. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +5 -2
  17. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  18. package/lib/commonjs/ui/context/useOxyAccountGraph.js +7 -0
  19. package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
  20. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +14 -5
  21. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
  22. package/lib/commonjs/ui/hooks/queries/userCache.js +286 -0
  23. package/lib/commonjs/ui/hooks/queries/userCache.js.map +1 -0
  24. package/lib/commonjs/ui/hooks/useAvatarPicker.js +23 -8
  25. package/lib/commonjs/ui/hooks/useAvatarPicker.js.map +1 -1
  26. package/lib/commonjs/ui/hooks/useFileDownloadUrl.js +7 -21
  27. package/lib/commonjs/ui/hooks/useFileDownloadUrl.js.map +1 -1
  28. package/lib/commonjs/ui/hooks/useResolvedFileUrls.js +163 -0
  29. package/lib/commonjs/ui/hooks/useResolvedFileUrls.js.map +1 -0
  30. package/lib/commonjs/ui/index.js +1 -1
  31. package/lib/commonjs/ui/screens/FileManagementScreen.js +51 -22
  32. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  33. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +216 -203
  34. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  35. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +26 -9
  36. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  37. package/lib/commonjs/ui/screens/fileManagement/photoGridLayout.js +44 -0
  38. package/lib/commonjs/ui/screens/fileManagement/photoGridLayout.js.map +1 -0
  39. package/lib/commonjs/ui/session/tokenTransport.js +21 -10
  40. package/lib/commonjs/ui/session/tokenTransport.js.map +1 -1
  41. package/lib/commonjs/ui/utils/fileManagement.js +0 -40
  42. package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
  43. package/lib/module/index.js +7 -1
  44. package/lib/module/index.js.map +1 -1
  45. package/lib/module/ui/client.js +1 -1
  46. package/lib/module/ui/client.js.map +1 -1
  47. package/lib/module/ui/components/FontLoader.js +18 -146
  48. package/lib/module/ui/components/FontLoader.js.map +1 -1
  49. package/lib/module/ui/components/FontLoader.native.js +83 -0
  50. package/lib/module/ui/components/FontLoader.native.js.map +1 -0
  51. package/lib/module/ui/components/OxyProvider.js +1 -1
  52. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  53. package/lib/module/ui/components/fileManagement/FileDetailsModal.js +16 -27
  54. package/lib/module/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
  55. package/lib/module/ui/components/fileManagement/UploadPreview.js.map +1 -1
  56. package/lib/module/ui/components/fileManagement/styles.js +0 -224
  57. package/lib/module/ui/components/fileManagement/styles.js.map +1 -1
  58. package/lib/module/ui/context/hooks/useAuthOperations.js +5 -2
  59. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  60. package/lib/module/ui/context/useOxyAccountGraph.js +7 -0
  61. package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
  62. package/lib/module/ui/hooks/queries/useAccountQueries.js +14 -5
  63. package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
  64. package/lib/module/ui/hooks/queries/userCache.js +282 -0
  65. package/lib/module/ui/hooks/queries/userCache.js.map +1 -0
  66. package/lib/module/ui/hooks/useAvatarPicker.js +23 -8
  67. package/lib/module/ui/hooks/useAvatarPicker.js.map +1 -1
  68. package/lib/module/ui/hooks/useFileDownloadUrl.js +7 -21
  69. package/lib/module/ui/hooks/useFileDownloadUrl.js.map +1 -1
  70. package/lib/module/ui/hooks/useResolvedFileUrls.js +157 -0
  71. package/lib/module/ui/hooks/useResolvedFileUrls.js.map +1 -0
  72. package/lib/module/ui/index.js +1 -1
  73. package/lib/module/ui/index.js.map +1 -1
  74. package/lib/module/ui/screens/FileManagementScreen.js +52 -23
  75. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  76. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +218 -205
  77. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  78. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +26 -9
  79. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  80. package/lib/module/ui/screens/fileManagement/photoGridLayout.js +39 -0
  81. package/lib/module/ui/screens/fileManagement/photoGridLayout.js.map +1 -0
  82. package/lib/module/ui/session/tokenTransport.js +21 -10
  83. package/lib/module/ui/session/tokenTransport.js.map +1 -1
  84. package/lib/module/ui/utils/fileManagement.js +0 -39
  85. package/lib/module/ui/utils/fileManagement.js.map +1 -1
  86. package/lib/typescript/commonjs/index.d.ts +2 -0
  87. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/ui/components/FontLoader.d.ts +14 -5
  89. package/lib/typescript/commonjs/ui/components/FontLoader.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/ui/components/FontLoader.native.d.ts +13 -0
  91. package/lib/typescript/commonjs/ui/components/FontLoader.native.d.ts.map +1 -0
  92. package/lib/typescript/commonjs/ui/components/fileManagement/FileDetailsModal.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/ui/components/fileManagement/styles.d.ts +0 -218
  94. package/lib/typescript/commonjs/ui/components/fileManagement/styles.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts +9 -0
  98. package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/ui/hooks/queries/userCache.d.ts +96 -0
  100. package/lib/typescript/commonjs/ui/hooks/queries/userCache.d.ts.map +1 -0
  101. package/lib/typescript/commonjs/ui/hooks/useAvatarPicker.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/ui/hooks/useFileDownloadUrl.d.ts +3 -2
  103. package/lib/typescript/commonjs/ui/hooks/useFileDownloadUrl.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/ui/hooks/useResolvedFileUrls.d.ts +48 -0
  105. package/lib/typescript/commonjs/ui/hooks/useResolvedFileUrls.d.ts.map +1 -0
  106. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts +1 -1
  108. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/ui/screens/fileManagement/photoGridLayout.d.ts +37 -0
  111. package/lib/typescript/commonjs/ui/screens/fileManagement/photoGridLayout.d.ts.map +1 -0
  112. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +15 -8
  113. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts +4 -11
  115. package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts.map +1 -1
  116. package/lib/typescript/module/index.d.ts +3 -1
  117. package/lib/typescript/module/index.d.ts.map +1 -1
  118. package/lib/typescript/module/ui/client.d.ts +1 -1
  119. package/lib/typescript/module/ui/client.d.ts.map +1 -1
  120. package/lib/typescript/module/ui/components/FontLoader.d.ts +14 -5
  121. package/lib/typescript/module/ui/components/FontLoader.d.ts.map +1 -1
  122. package/lib/typescript/module/ui/components/FontLoader.native.d.ts +13 -0
  123. package/lib/typescript/module/ui/components/FontLoader.native.d.ts.map +1 -0
  124. package/lib/typescript/module/ui/components/fileManagement/FileDetailsModal.d.ts.map +1 -1
  125. package/lib/typescript/module/ui/components/fileManagement/styles.d.ts +0 -218
  126. package/lib/typescript/module/ui/components/fileManagement/styles.d.ts.map +1 -1
  127. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  128. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  129. package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts +9 -0
  130. package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  131. package/lib/typescript/module/ui/hooks/queries/userCache.d.ts +96 -0
  132. package/lib/typescript/module/ui/hooks/queries/userCache.d.ts.map +1 -0
  133. package/lib/typescript/module/ui/hooks/useAvatarPicker.d.ts.map +1 -1
  134. package/lib/typescript/module/ui/hooks/useFileDownloadUrl.d.ts +3 -2
  135. package/lib/typescript/module/ui/hooks/useFileDownloadUrl.d.ts.map +1 -1
  136. package/lib/typescript/module/ui/hooks/useResolvedFileUrls.d.ts +48 -0
  137. package/lib/typescript/module/ui/hooks/useResolvedFileUrls.d.ts.map +1 -0
  138. package/lib/typescript/module/ui/index.d.ts +1 -1
  139. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  140. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  141. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts +1 -1
  142. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  143. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  144. package/lib/typescript/module/ui/screens/fileManagement/photoGridLayout.d.ts +37 -0
  145. package/lib/typescript/module/ui/screens/fileManagement/photoGridLayout.d.ts.map +1 -0
  146. package/lib/typescript/module/ui/session/tokenTransport.d.ts +15 -8
  147. package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -1
  148. package/lib/typescript/module/ui/utils/fileManagement.d.ts +4 -11
  149. package/lib/typescript/module/ui/utils/fileManagement.d.ts.map +1 -1
  150. package/package.json +6 -11
  151. package/src/index.ts +8 -0
  152. package/src/ui/components/FontLoader.native.tsx +88 -0
  153. package/src/ui/components/FontLoader.tsx +16 -151
  154. package/src/ui/components/fileManagement/FileDetailsModal.tsx +17 -18
  155. package/src/ui/components/fileManagement/UploadPreview.tsx +1 -1
  156. package/src/ui/components/fileManagement/styles.ts +0 -225
  157. package/src/ui/context/hooks/useAuthOperations.ts +5 -2
  158. package/src/ui/context/useOxyAccountGraph.ts +5 -0
  159. package/src/ui/hooks/queries/__tests__/userCache.test.ts +361 -0
  160. package/src/ui/hooks/queries/useAccountQueries.ts +14 -5
  161. package/src/ui/hooks/queries/userCache.ts +308 -0
  162. package/src/ui/hooks/useAvatarPicker.ts +27 -7
  163. package/src/ui/hooks/useFileDownloadUrl.ts +7 -23
  164. package/src/ui/hooks/useResolvedFileUrls.ts +170 -0
  165. package/src/ui/screens/FileManagementScreen.tsx +55 -27
  166. package/src/ui/screens/__tests__/photoGridLayout.test.ts +57 -0
  167. package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +215 -213
  168. package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +27 -3
  169. package/src/ui/screens/fileManagement/photoGridLayout.ts +45 -0
  170. package/src/ui/session/__tests__/tokenTransport.test.ts +25 -10
  171. package/src/ui/session/tokenTransport.ts +21 -10
  172. package/src/ui/utils/fileManagement.ts +3 -46
  173. package/lib/typescript/types/expo-vector-icons.d.ts +0 -22
  174. package/src/types/expo-vector-icons.d.ts +0 -22
@@ -28,8 +28,8 @@ import { useUploadFile } from '../hooks/mutations/useAccountMutations';
28
28
  import {
29
29
  formatFileSize,
30
30
  getFileIcon,
31
- getSafeDownloadUrl,
32
31
  } from '../utils/fileManagement';
32
+ import { useResolvedFileUrls, fileThumbSource } from '../hooks/useResolvedFileUrls';
33
33
  import { FileViewer } from '../components/fileManagement/FileViewer';
34
34
  import { FileDetailsModal } from '../components/fileManagement/FileDetailsModal';
35
35
  import { UploadPreview } from '../components/fileManagement/UploadPreview';
@@ -356,12 +356,18 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
356
356
  onClose?.();
357
357
  }, [selectMode, multiSelect, selectedIds, files, onConfirmSelection, onClose, defaultVisibility, oxyServices, linkContext]);
358
358
 
359
- // Helper to safely request a thumbnail variant only for image mime types.
360
- const getSafeDownloadUrlCallback = useCallback(
361
- (file: FileMetadata, variant = 'thumb') => {
362
- return getSafeDownloadUrl(file, variant, (fileId: string, variant?: string) => oxyServices.getFileDownloadUrl(fileId, variant));
363
- },
364
- [oxyServices]
359
+ // Private-safe thumbnail URLs for the currently-shown files, resolved in
360
+ // ONE batch per page (not N per-tile) and cached by React Query. Uploads
361
+ // default to private, so the synchronous public-CDN URL would 404 for
362
+ // every private thumbnail.
363
+ const resolvedThumbUrls = useResolvedFileUrls(oxyServices, filteredFiles, user?.id);
364
+
365
+ // Image source for a grid tile: the resolved private-safe URL for a
366
+ // persisted file, or the locally-picked preview for an in-flight optimistic
367
+ // entry. Never builds an asset URL from a `temp-…`/uploading id.
368
+ const thumbSourceFor = useCallback(
369
+ (file: FileMetadata): string | undefined => fileThumbSource(file, resolvedThumbUrls),
370
+ [resolvedThumbUrls],
365
371
  );
366
372
 
367
373
  const bloomTheme = useTheme();
@@ -480,7 +486,14 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
480
486
  await Promise.all(
481
487
  photosToLoad.map(async (photo) => {
482
488
  try {
483
- const downloadUrl = getSafeDownloadUrlCallback(photo, 'thumb');
489
+ const downloadUrl = thumbSourceFor(photo);
490
+ // URL not resolved yet — skip (do NOT record fallback
491
+ // dims) so this photo is re-measured once its private-safe
492
+ // URL resolves. Measuring the broken public URL is exactly
493
+ // the bug we are fixing.
494
+ if (!downloadUrl) {
495
+ return;
496
+ }
484
497
 
485
498
  // Unified approach using Image.getSize (works on all platforms)
486
499
  await new Promise<void>((resolve) => {
@@ -515,7 +528,19 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
515
528
  } finally {
516
529
  setLoadingDimensions(false);
517
530
  }
518
- }, [getSafeDownloadUrlCallback, photoDimensions]);
531
+ }, [thumbSourceFor, photoDimensions]);
532
+
533
+ // Re-measure photo dimensions when their private-safe URLs resolve. The
534
+ // justified grid's own trigger fires on the photo SET, not on URL
535
+ // availability, so newly-resolved private tiles would otherwise keep their
536
+ // skipped (unmeasured) state and render with fallback geometry.
537
+ useEffect(() => {
538
+ if (viewMode !== 'photos') return;
539
+ const photos = filteredFiles.filter((file) => file.contentType.startsWith('image/'));
540
+ if (photos.length > 0) {
541
+ loadPhotoDimensions(photos);
542
+ }
543
+ }, [resolvedThumbUrls, viewMode, filteredFiles, loadPhotoDimensions]);
519
544
 
520
545
  // Create justified rows from photos with responsive algorithm
521
546
  const createJustifiedRows = useCallback((photos: FileMetadata[], containerWidth: number) => {
@@ -646,9 +671,10 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
646
671
  // Unified download function - works on all platforms
647
672
  const handleFileDownload = async (fileId: string, filename: string) => {
648
673
  try {
649
- // Try to use the download URL with a simple approach
650
- // On web, this creates a download link. On mobile, it opens the URL.
651
- const downloadUrl = oxyServices.getFileDownloadUrl(fileId);
674
+ // Resolve an authenticated, private-safe URL. The synchronous
675
+ // `getFileDownloadUrl` yields the public CDN origin, which 404s for
676
+ // private assets (the default visibility).
677
+ const downloadUrl = await oxyServices.getFileDownloadUrlAsync(fileId);
652
678
 
653
679
  // For web platforms, use link download
654
680
  if (typeof window !== 'undefined' && window.document) {
@@ -715,8 +741,10 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
715
741
  file.contentType.includes('pdf') ||
716
742
  file.contentType.startsWith('video/') ||
717
743
  file.contentType.startsWith('audio/')) {
718
- // For images, PDFs, videos, and audio, we'll use the URL directly
719
- const downloadUrl = oxyServices.getFileDownloadUrl(file.id);
744
+ // For images, PDFs, videos, and audio, we render the URL
745
+ // directly. Resolve the authenticated, private-safe URL —
746
+ // the synchronous public-CDN URL 404s for private assets.
747
+ const downloadUrl = await oxyServices.getFileDownloadUrlAsync(file.id);
720
748
  setFileContent(downloadUrl);
721
749
  } else {
722
750
  // For text files, get the content using authenticated request
@@ -755,7 +783,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
755
783
  };
756
784
 
757
785
  const renderSimplePhotoItem = useCallback((photo: FileMetadata, index: number) => {
758
- const downloadUrl = getSafeDownloadUrlCallback(photo, 'thumb');
786
+ const downloadUrl = thumbSourceFor(photo);
759
787
 
760
788
  // Calculate photo item width based on actual container size from bottom sheet
761
789
  let itemsPerRow = 3; // Default for mobile
@@ -803,10 +831,10 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
803
831
  </View>
804
832
  </TouchableOpacity>
805
833
  );
806
- }, [oxyServices, safeContainerWidth, selectMode, selectedIds, colors.primary, colors.text]);
834
+ }, [thumbSourceFor, safeContainerWidth, selectMode, selectedIds, colors.primary, colors.text]);
807
835
 
808
836
  const renderJustifiedPhotoItem = useCallback((photo: FileMetadata, width: number, height: number, isLast: boolean) => {
809
- const downloadUrl = getSafeDownloadUrlCallback(photo, 'thumb');
837
+ const downloadUrl = thumbSourceFor(photo);
810
838
 
811
839
  return (
812
840
  <TouchableOpacity
@@ -843,7 +871,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
843
871
  </View>
844
872
  </TouchableOpacity>
845
873
  );
846
- }, [oxyServices, selectMode, selectedIds, multiSelect, colors.primary, colors.text]);
874
+ }, [thumbSourceFor, selectMode, selectedIds, multiSelect, colors.primary, colors.text]);
847
875
 
848
876
  // Run initial load once per targetUserId change to avoid accidental loops
849
877
  const lastLoadedFor = useRef<string | undefined>(undefined);
@@ -879,7 +907,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
879
907
  <View style={fileManagementStyles.filePreview}>
880
908
  {isImage && (
881
909
  <ExpoImage
882
- source={{ uri: getSafeDownloadUrlCallback(file, 'thumb') }}
910
+ source={{ uri: thumbSourceFor(file) }}
883
911
  style={fileManagementStyles.previewImage}
884
912
  contentFit="cover"
885
913
  transition={120}
@@ -899,7 +927,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
899
927
  {isVideo && (
900
928
  <View style={fileManagementStyles.videoPreviewWrapper}>
901
929
  <ExpoImage
902
- source={{ uri: getSafeDownloadUrlCallback(file, 'thumb') }}
930
+ source={{ uri: thumbSourceFor(file) }}
903
931
  style={fileManagementStyles.videoPosterImage}
904
932
  contentFit="cover"
905
933
  transition={120}
@@ -919,7 +947,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
919
947
  style={[fileManagementStyles.fallbackIcon, { display: isImage ? 'none' : 'flex' }]}
920
948
  >
921
949
  <Ionicons
922
- name={getFileIcon(file.contentType) as React.ComponentProps<typeof Ionicons>['name']}
950
+ name={getFileIcon(file.contentType)}
923
951
  size={32}
924
952
  color={colors.primary}
925
953
  />
@@ -934,7 +962,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
934
962
  ) : (
935
963
  <View style={fileManagementStyles.fileIconContainer}>
936
964
  <Ionicons
937
- name={getFileIcon(file.contentType) as React.ComponentProps<typeof Ionicons>['name']}
965
+ name={getFileIcon(file.contentType)}
938
966
  size={32}
939
967
  color={colors.primary}
940
968
  />
@@ -1016,7 +1044,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1016
1044
  fileIcon = (
1017
1045
  <View style={{ width: 36, height: 36, borderRadius: 18, overflow: 'hidden' }}>
1018
1046
  <ExpoImage
1019
- source={{ uri: getSafeDownloadUrlCallback(file, 'thumb') }}
1047
+ source={{ uri: thumbSourceFor(file) }}
1020
1048
  style={{ width: 36, height: 36 }}
1021
1049
  contentFit="cover"
1022
1050
  transition={120}
@@ -1032,7 +1060,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1032
1060
  fileIcon = (
1033
1061
  <View style={{ width: 36, height: 36, borderRadius: 18, overflow: 'hidden', backgroundColor: '#000000', position: 'relative' }}>
1034
1062
  <ExpoImage
1035
- source={{ uri: getSafeDownloadUrlCallback(file, 'thumb') }}
1063
+ source={{ uri: thumbSourceFor(file) }}
1036
1064
  style={{ width: 36, height: 36 }}
1037
1065
  contentFit="cover"
1038
1066
  transition={120}
@@ -1102,7 +1130,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1102
1130
  ) : undefined,
1103
1131
  };
1104
1132
  });
1105
- }, [filteredFiles, theme, deleting, handleFileDownload, confirmFileDelete, handleFileOpen, getSafeDownloadUrlCallback, selectMode, selectedIds]);
1133
+ }, [filteredFiles, theme, deleting, handleFileDownload, confirmFileDelete, handleFileOpen, thumbSourceFor, selectMode, selectedIds]);
1106
1134
 
1107
1135
  // Scroll to selected file after selection
1108
1136
  useEffect(() => {
@@ -1191,7 +1219,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1191
1219
  }, [lastSelectedFileId]);
1192
1220
 
1193
1221
  const renderPhotoItem = (photo: FileMetadata, index: number) => {
1194
- const downloadUrl = getSafeDownloadUrlCallback(photo, 'thumb');
1222
+ const downloadUrl = thumbSourceFor(photo);
1195
1223
 
1196
1224
  // Calculate photo item width based on actual container size from bottom sheet
1197
1225
  let itemsPerRow = 3; // Default for mobile
@@ -1570,7 +1598,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1570
1598
  hasMore={paging.hasMore}
1571
1599
  loadingMore={paging.loadingMore}
1572
1600
  reduceMotion={reduceMotion}
1573
- getThumbUrl={getSafeDownloadUrlCallback}
1601
+ getThumbUrl={thumbSourceFor}
1574
1602
  primaryColor={colors.primary}
1575
1603
  isOwner={isOwner}
1576
1604
  onTogglePhoto={toggleSelect}
@@ -0,0 +1,57 @@
1
+ import {
2
+ computePhotoGridLayout,
3
+ PHOTO_GRID_GUTTER,
4
+ PHOTO_GRID_WIDE_BREAKPOINT,
5
+ } from '../fileManagement/photoGridLayout';
6
+
7
+ describe('computePhotoGridLayout', () => {
8
+ it('uses 3 columns below the wide breakpoint', () => {
9
+ expect(computePhotoGridLayout(390).columns).toBe(3);
10
+ expect(computePhotoGridLayout(500).columns).toBe(3);
11
+ expect(computePhotoGridLayout(PHOTO_GRID_WIDE_BREAKPOINT - 1).columns).toBe(3);
12
+ });
13
+
14
+ it('uses 4 columns at/above the wide breakpoint', () => {
15
+ expect(computePhotoGridLayout(PHOTO_GRID_WIDE_BREAKPOINT).columns).toBe(4);
16
+ expect(computePhotoGridLayout(700).columns).toBe(4);
17
+ expect(computePhotoGridLayout(1024).columns).toBe(4);
18
+ });
19
+
20
+ it('floors the tile size so a full row never exceeds the container', () => {
21
+ // The whole point of the bug fix: geometry derives from the passed
22
+ // container width, and a full row of tiles + gutters must fit inside it.
23
+ for (const width of [320, 375, 390, 428, 500, 599, 600, 768, 834, 1024]) {
24
+ const { columns, cellSize, gutter } = computePhotoGridLayout(width);
25
+ const rowWidth = columns * cellSize + (columns - 1) * gutter;
26
+ expect(rowWidth).toBeLessThanOrEqual(width);
27
+ // ...and the floor is tight (adding one px per tile would overflow).
28
+ expect((columns * (cellSize + 1)) + (columns - 1) * gutter).toBeGreaterThan(width);
29
+ }
30
+ });
31
+
32
+ it('derives smaller tiles from a narrow sheet than from a wide screen', () => {
33
+ // Regression guard for the original bug: a centered dialog (narrow
34
+ // container) must NOT size tiles from a wider screen width.
35
+ const narrowSheet = computePhotoGridLayout(500); // 3 cols
36
+ const wideScreen = computePhotoGridLayout(1200); // 4 cols
37
+ expect(narrowSheet.cellSize).toBeLessThan(wideScreen.cellSize);
38
+ // Cross-check the exact arithmetic against the gutter constant.
39
+ expect(narrowSheet.cellSize).toBe(
40
+ Math.floor((500 - PHOTO_GRID_GUTTER * 2) / 3),
41
+ );
42
+ expect(wideScreen.cellSize).toBe(
43
+ Math.floor((1200 - PHOTO_GRID_GUTTER * 3) / 4),
44
+ );
45
+ });
46
+
47
+ it('returns cellSize 0 for a not-yet-measured (<= 0) width', () => {
48
+ expect(computePhotoGridLayout(0).cellSize).toBe(0);
49
+ expect(computePhotoGridLayout(-10).cellSize).toBe(0);
50
+ // Columns still resolve to the narrow default so callers can render.
51
+ expect(computePhotoGridLayout(0).columns).toBe(3);
52
+ });
53
+
54
+ it('exposes the gutter it used to derive the tile size', () => {
55
+ expect(computePhotoGridLayout(500).gutter).toBe(PHOTO_GRID_GUTTER);
56
+ });
57
+ });