@oxyhq/services 5.13.3 → 5.13.10

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 (156) hide show
  1. package/lib/commonjs/core/HttpClient.js +1 -1
  2. package/lib/commonjs/core/HttpClient.js.map +1 -1
  3. package/lib/commonjs/core/OxyServices.js +82 -8
  4. package/lib/commonjs/core/OxyServices.js.map +1 -1
  5. package/lib/commonjs/i18n/locales/en-US.json +222 -6
  6. package/lib/commonjs/lib/sonner.js.map +1 -1
  7. package/lib/commonjs/ui/components/GroupedItem.js +24 -22
  8. package/lib/commonjs/ui/components/GroupedItem.js.map +1 -1
  9. package/lib/commonjs/ui/components/OxyProvider.js +35 -14
  10. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  11. package/lib/commonjs/ui/navigation/routes.js +36 -1
  12. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  13. package/lib/commonjs/ui/screens/AccountOverviewScreen.js +150 -5
  14. package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
  15. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +475 -319
  16. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  17. package/lib/commonjs/ui/screens/AccountVerificationScreen.js +217 -0
  18. package/lib/commonjs/ui/screens/AccountVerificationScreen.js.map +1 -0
  19. package/lib/commonjs/ui/screens/FileManagementScreen.js +911 -213
  20. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  21. package/lib/commonjs/ui/screens/HelpSupportScreen.js +131 -0
  22. package/lib/commonjs/ui/screens/HelpSupportScreen.js.map +1 -0
  23. package/lib/commonjs/ui/screens/HistoryViewScreen.js +258 -0
  24. package/lib/commonjs/ui/screens/HistoryViewScreen.js.map +1 -0
  25. package/lib/commonjs/ui/screens/LegalDocumentsScreen.js +211 -0
  26. package/lib/commonjs/ui/screens/LegalDocumentsScreen.js.map +1 -0
  27. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js +0 -1
  28. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  29. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +307 -0
  30. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -0
  31. package/lib/commonjs/ui/screens/ProfileScreen.js +1 -7
  32. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  33. package/lib/commonjs/ui/screens/SavesCollectionsScreen.js +205 -0
  34. package/lib/commonjs/ui/screens/SavesCollectionsScreen.js.map +1 -0
  35. package/lib/commonjs/ui/screens/SearchSettingsScreen.js +239 -0
  36. package/lib/commonjs/ui/screens/SearchSettingsScreen.js.map +1 -0
  37. package/lib/commonjs/ui/screens/SignInScreen.js +14 -29
  38. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  39. package/lib/commonjs/utils/asyncUtils.js +1 -0
  40. package/lib/commonjs/utils/asyncUtils.js.map +1 -1
  41. package/lib/commonjs/utils/cache.js +4 -4
  42. package/lib/commonjs/utils/cache.js.map +1 -1
  43. package/lib/commonjs/utils/index.js +0 -6
  44. package/lib/commonjs/utils/index.js.map +1 -1
  45. package/lib/module/core/HttpClient.js +1 -1
  46. package/lib/module/core/HttpClient.js.map +1 -1
  47. package/lib/module/core/OxyServices.js +82 -8
  48. package/lib/module/core/OxyServices.js.map +1 -1
  49. package/lib/module/i18n/locales/en-US.json +222 -6
  50. package/lib/module/lib/sonner.js.map +1 -1
  51. package/lib/module/ui/components/GroupedItem.js +24 -22
  52. package/lib/module/ui/components/GroupedItem.js.map +1 -1
  53. package/lib/module/ui/components/OxyProvider.js +40 -17
  54. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  55. package/lib/module/ui/navigation/routes.js +36 -1
  56. package/lib/module/ui/navigation/routes.js.map +1 -1
  57. package/lib/module/ui/screens/AccountOverviewScreen.js +151 -6
  58. package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
  59. package/lib/module/ui/screens/AccountSettingsScreen.js +475 -319
  60. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  61. package/lib/module/ui/screens/AccountVerificationScreen.js +212 -0
  62. package/lib/module/ui/screens/AccountVerificationScreen.js.map +1 -0
  63. package/lib/module/ui/screens/FileManagementScreen.js +913 -212
  64. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  65. package/lib/module/ui/screens/HelpSupportScreen.js +126 -0
  66. package/lib/module/ui/screens/HelpSupportScreen.js.map +1 -0
  67. package/lib/module/ui/screens/HistoryViewScreen.js +253 -0
  68. package/lib/module/ui/screens/HistoryViewScreen.js.map +1 -0
  69. package/lib/module/ui/screens/LegalDocumentsScreen.js +206 -0
  70. package/lib/module/ui/screens/LegalDocumentsScreen.js.map +1 -0
  71. package/lib/module/ui/screens/PremiumSubscriptionScreen.js +0 -1
  72. package/lib/module/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  73. package/lib/module/ui/screens/PrivacySettingsScreen.js +302 -0
  74. package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -0
  75. package/lib/module/ui/screens/ProfileScreen.js +1 -7
  76. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  77. package/lib/module/ui/screens/SavesCollectionsScreen.js +200 -0
  78. package/lib/module/ui/screens/SavesCollectionsScreen.js.map +1 -0
  79. package/lib/module/ui/screens/SearchSettingsScreen.js +234 -0
  80. package/lib/module/ui/screens/SearchSettingsScreen.js.map +1 -0
  81. package/lib/module/ui/screens/SignInScreen.js +14 -29
  82. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  83. package/lib/module/utils/asyncUtils.js +1 -0
  84. package/lib/module/utils/asyncUtils.js.map +1 -1
  85. package/lib/module/utils/cache.js +3 -3
  86. package/lib/module/utils/cache.js.map +1 -1
  87. package/lib/module/utils/index.js +1 -1
  88. package/lib/module/utils/index.js.map +1 -1
  89. package/lib/typescript/core/OxyServices.d.ts +30 -6
  90. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  91. package/lib/typescript/lib/sonner.d.ts +1 -0
  92. package/lib/typescript/lib/sonner.d.ts.map +1 -1
  93. package/lib/typescript/types/expo-document-picker.d.ts +36 -0
  94. package/lib/typescript/ui/components/GroupedItem.d.ts.map +1 -1
  95. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  96. package/lib/typescript/ui/navigation/routes.d.ts +1 -1
  97. package/lib/typescript/ui/navigation/routes.d.ts.map +1 -1
  98. package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
  99. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  100. package/lib/typescript/ui/screens/AccountVerificationScreen.d.ts +5 -0
  101. package/lib/typescript/ui/screens/AccountVerificationScreen.d.ts.map +1 -0
  102. package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
  103. package/lib/typescript/ui/screens/HelpSupportScreen.d.ts +5 -0
  104. package/lib/typescript/ui/screens/HelpSupportScreen.d.ts.map +1 -0
  105. package/lib/typescript/ui/screens/HistoryViewScreen.d.ts +5 -0
  106. package/lib/typescript/ui/screens/HistoryViewScreen.d.ts.map +1 -0
  107. package/lib/typescript/ui/screens/LegalDocumentsScreen.d.ts +5 -0
  108. package/lib/typescript/ui/screens/LegalDocumentsScreen.d.ts.map +1 -0
  109. package/lib/typescript/ui/screens/PremiumSubscriptionScreen.d.ts.map +1 -1
  110. package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts +5 -0
  111. package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -0
  112. package/lib/typescript/ui/screens/ProfileScreen.d.ts.map +1 -1
  113. package/lib/typescript/ui/screens/SavesCollectionsScreen.d.ts +5 -0
  114. package/lib/typescript/ui/screens/SavesCollectionsScreen.d.ts.map +1 -0
  115. package/lib/typescript/ui/screens/SearchSettingsScreen.d.ts +5 -0
  116. package/lib/typescript/ui/screens/SearchSettingsScreen.d.ts.map +1 -0
  117. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  118. package/lib/typescript/utils/asyncUtils.d.ts.map +1 -1
  119. package/lib/typescript/utils/cache.d.ts +3 -3
  120. package/lib/typescript/utils/cache.d.ts.map +1 -1
  121. package/lib/typescript/utils/index.d.ts +1 -1
  122. package/lib/typescript/utils/index.d.ts.map +1 -1
  123. package/package.json +1 -1
  124. package/src/core/HttpClient.ts +1 -1
  125. package/src/core/OxyServices.ts +80 -8
  126. package/src/i18n/locales/en-US.json +222 -6
  127. package/src/lib/sonner.ts +1 -0
  128. package/src/types/expo-document-picker.d.ts +36 -0
  129. package/src/ui/components/GroupedItem.tsx +23 -21
  130. package/src/ui/components/OxyProvider.tsx +33 -11
  131. package/src/ui/navigation/routes.ts +42 -0
  132. package/src/ui/screens/AccountOverviewScreen.tsx +175 -5
  133. package/src/ui/screens/AccountSettingsScreen.tsx +521 -360
  134. package/src/ui/screens/AccountVerificationScreen.tsx +235 -0
  135. package/src/ui/screens/FileManagementScreen.tsx +934 -208
  136. package/src/ui/screens/HelpSupportScreen.tsx +143 -0
  137. package/src/ui/screens/HistoryViewScreen.tsx +280 -0
  138. package/src/ui/screens/LegalDocumentsScreen.tsx +220 -0
  139. package/src/ui/screens/PremiumSubscriptionScreen.tsx +0 -1
  140. package/src/ui/screens/PrivacySettingsScreen.tsx +332 -0
  141. package/src/ui/screens/ProfileScreen.tsx +1 -8
  142. package/src/ui/screens/SavesCollectionsScreen.tsx +222 -0
  143. package/src/ui/screens/SearchSettingsScreen.tsx +219 -0
  144. package/src/ui/screens/SignInScreen.tsx +19 -35
  145. package/src/utils/asyncUtils.ts +1 -0
  146. package/src/utils/cache.ts +3 -3
  147. package/src/utils/index.ts +1 -1
  148. package/lib/commonjs/ui/components/StepBasedScreen.README.md +0 -337
  149. package/lib/commonjs/ui/components/internal/TextField.md +0 -436
  150. package/lib/commonjs/ui/styles/FONTS.md +0 -126
  151. package/lib/module/ui/components/StepBasedScreen.README.md +0 -337
  152. package/lib/module/ui/components/internal/TextField.md +0 -436
  153. package/lib/module/ui/styles/FONTS.md +0 -126
  154. package/src/ui/components/StepBasedScreen.README.md +0 -337
  155. package/src/ui/components/internal/TextField.md +0 -436
  156. package/src/ui/styles/FONTS.md +0 -126
@@ -12,6 +12,9 @@ import {
12
12
  Modal,
13
13
  TextInput,
14
14
  Image, // kept for Image.getSize only
15
+ Animated,
16
+ Easing,
17
+ Alert,
15
18
  } from 'react-native';
16
19
  import { Image as ExpoImage } from 'expo-image';
17
20
  import type { BaseScreenProps } from '../navigation/types';
@@ -92,18 +95,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
92
95
  linkContext,
93
96
  }) => {
94
97
  const { user, oxyServices } = useOxy();
95
-
96
- // Debug: log the actual container width
97
- useEffect(() => {
98
- console.log('[FileManagementScreen] Container width (full):', containerWidth);
99
- // Padding structure:
100
- // - containerWidth = full bottom sheet container width (measured from OxyProvider)
101
- // - photoScrollContainer adds padding: 16 (32px total horizontal padding)
102
- // - Available content width = containerWidth - 32
103
- const availableContentWidth = containerWidth - 32;
104
- console.log('[FileManagementScreen] Available content width:', availableContentWidth);
105
- console.log('[FileManagementScreen] Spacing fix applied: 4px uniform gap both horizontal and vertical');
106
- }, [containerWidth]);
107
98
  const files = useFiles();
108
99
  const uploading = useUploadingStore();
109
100
  const uploadProgress = useUploadAggregateProgress();
@@ -118,24 +109,63 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
118
109
  const [fileContent, setFileContent] = useState<string | null>(null);
119
110
  const [loadingFileContent, setLoadingFileContent] = useState(false);
120
111
  const [showFileDetailsInViewer, setShowFileDetailsInViewer] = useState(false);
121
- const [viewMode, setViewMode] = useState<'all' | 'photos'>('all');
112
+ const [viewMode, setViewMode] = useState<'all' | 'photos' | 'videos' | 'documents' | 'audio'>('all');
122
113
  const [searchQuery, setSearchQuery] = useState('');
123
- // Derived filtered files (avoid setState loops)
114
+ const [sortBy, setSortBy] = useState<'date' | 'size' | 'name' | 'type'>('date');
115
+ const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
116
+ const [pendingFiles, setPendingFiles] = useState<Array<{ file: File | Blob; preview?: string; size: number; name: string; type: string }>>([]);
117
+ const [showUploadPreview, setShowUploadPreview] = useState(false);
118
+ // Derived filtered and sorted files (avoid setState loops)
124
119
  const filteredFiles = useMemo(() => {
125
120
  let filteredByMode = files;
126
121
  if (viewMode === 'photos') {
127
122
  filteredByMode = files.filter(file => file.contentType.startsWith('image/'));
123
+ } else if (viewMode === 'videos') {
124
+ filteredByMode = files.filter(file => file.contentType.startsWith('video/'));
125
+ } else if (viewMode === 'documents') {
126
+ filteredByMode = files.filter(file =>
127
+ file.contentType.includes('pdf') ||
128
+ file.contentType.includes('document') ||
129
+ file.contentType.includes('text') ||
130
+ file.contentType.includes('msword') ||
131
+ file.contentType.includes('excel') ||
132
+ file.contentType.includes('spreadsheet') ||
133
+ file.contentType.includes('presentation') ||
134
+ file.contentType.includes('powerpoint')
135
+ );
136
+ } else if (viewMode === 'audio') {
137
+ filteredByMode = files.filter(file => file.contentType.startsWith('audio/'));
128
138
  }
129
- if (!searchQuery.trim()) {
130
- return filteredByMode;
139
+
140
+ let filtered = filteredByMode;
141
+ if (searchQuery.trim()) {
142
+ const query = searchQuery.toLowerCase();
143
+ filtered = filteredByMode.filter(file =>
144
+ file.filename.toLowerCase().includes(query) ||
145
+ file.contentType.toLowerCase().includes(query) ||
146
+ (file.metadata?.description && file.metadata.description.toLowerCase().includes(query))
147
+ );
131
148
  }
132
- const query = searchQuery.toLowerCase();
133
- return filteredByMode.filter(file =>
134
- file.filename.toLowerCase().includes(query) ||
135
- file.contentType.toLowerCase().includes(query) ||
136
- (file.metadata?.description && file.metadata.description.toLowerCase().includes(query))
137
- );
138
- }, [files, searchQuery, viewMode]);
149
+
150
+ // Sort files
151
+ const sorted = [...filtered].sort((a, b) => {
152
+ let comparison = 0;
153
+ if (sortBy === 'date') {
154
+ const dateA = new Date(a.uploadDate || 0).getTime();
155
+ const dateB = new Date(b.uploadDate || 0).getTime();
156
+ comparison = dateA - dateB;
157
+ } else if (sortBy === 'size') {
158
+ comparison = (a.length || 0) - (b.length || 0);
159
+ } else if (sortBy === 'name') {
160
+ comparison = (a.filename || '').localeCompare(b.filename || '');
161
+ } else if (sortBy === 'type') {
162
+ comparison = (a.contentType || '').localeCompare(b.contentType || '');
163
+ }
164
+ return sortOrder === 'asc' ? comparison : -comparison;
165
+ });
166
+
167
+ return sorted;
168
+ }, [files, searchQuery, viewMode, sortBy, sortOrder]);
139
169
  const [isDragging, setIsDragging] = useState(false);
140
170
  const [photoDimensions, setPhotoDimensions] = useState<{ [key: string]: { width: number, height: number } }>({});
141
171
  const [loadingDimensions, setLoadingDimensions] = useState(false);
@@ -144,6 +174,11 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
144
174
  const MIN_BANNER_MS = 600;
145
175
  // Selection state
146
176
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set(initialSelectedIds));
177
+ const [lastSelectedFileId, setLastSelectedFileId] = useState<string | null>(null);
178
+ const scrollViewRef = useRef<ScrollView>(null);
179
+ const photoScrollViewRef = useRef<ScrollView>(null);
180
+ const itemRefs = useRef<Map<string, number>>(new Map()); // Track item positions
181
+ const containerRef = useRef<View>(null); // Ref for drag and drop container
147
182
  useEffect(() => {
148
183
  if (initialSelectedIds && initialSelectedIds.length) {
149
184
  setSelectedIds(new Set(initialSelectedIds));
@@ -151,7 +186,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
151
186
  }, [initialSelectedIds]);
152
187
 
153
188
  const toggleSelect = useCallback(async (file: FileMetadata) => {
154
- if (!selectMode) return;
189
+ // Allow selection in regular mode for bulk operations
190
+ // if (!selectMode) return;
155
191
  if (disabledMimeTypes.length) {
156
192
  const blocked = disabledMimeTypes.some(mt => file.contentType === mt || file.contentType.startsWith(mt.endsWith('/') ? mt : mt + '/'));
157
193
  if (blocked) {
@@ -165,12 +201,13 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
165
201
  if (fileVisibility !== defaultVisibility) {
166
202
  try {
167
203
  await oxyServices.assetUpdateVisibility(file.id, defaultVisibility);
168
- console.log(`Updated file ${file.id} visibility from ${fileVisibility} to ${defaultVisibility}`);
169
204
  } catch (error) {
170
- console.error('Failed to update file visibility:', error);
171
205
  // Continue anyway - selection shouldn't fail if visibility update fails
172
206
  }
173
207
  }
208
+
209
+ // Track the selected file for scrolling
210
+ setLastSelectedFileId(file.id);
174
211
 
175
212
  // Link file to entity if linkContext is provided
176
213
  if (linkContext) {
@@ -183,9 +220,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
183
220
  defaultVisibility,
184
221
  (linkContext as any).webhookUrl
185
222
  );
186
- console.log(`Linked file ${file.id} to ${linkContext.app}/${linkContext.entityType}/${linkContext.entityId}`);
187
223
  } catch (error) {
188
- console.error('Failed to link file:', error);
189
224
  // Continue anyway - selection shouldn't fail if linking fails
190
225
  }
191
226
  }
@@ -228,9 +263,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
228
263
  if (fileVisibility !== defaultVisibility) {
229
264
  try {
230
265
  await oxyServices.assetUpdateVisibility(file.id, defaultVisibility);
231
- console.log(`Updated file ${file.id} visibility from ${fileVisibility} to ${defaultVisibility}`);
232
266
  } catch (error) {
233
- console.error(`Failed to update visibility for ${file.id}:`, error);
267
+ // Visibility update failed, continue with selection
234
268
  }
235
269
  }
236
270
 
@@ -245,9 +279,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
245
279
  defaultVisibility,
246
280
  (linkContext as any).webhookUrl
247
281
  );
248
- console.log(`Linked file ${file.id} to ${linkContext.app}/${linkContext.entityType}/${linkContext.entityId}`);
249
282
  } catch (error) {
250
- console.error(`Failed to link file ${file.id}:`, error);
283
+ // File linking failed, continue with selection
251
284
  }
252
285
  }
253
286
  });
@@ -378,7 +411,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
378
411
  }));
379
412
  }
380
413
  } catch (error: any) {
381
- console.error('Failed to load files:', error);
382
414
  toast.error(error.message || 'Failed to load files');
383
415
  } finally {
384
416
  setLoading(false);
@@ -465,7 +497,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
465
497
  setPhotoDimensions(newDimensions);
466
498
  }
467
499
  } catch (error) {
468
- console.error('Error loading photo dimensions:', error);
500
+ // Photo dimensions loading failed, continue without dimensions
469
501
  } finally {
470
502
  setLoadingDimensions(false);
471
503
  }
@@ -554,67 +586,171 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
554
586
  // Silent background refresh to ensure metadata/variants updated
555
587
  setTimeout(() => { loadFiles('silent'); }, 1200);
556
588
  } catch (error: any) {
557
- console.error('Upload error:', error);
558
589
  toast.error(error.message || 'Failed to upload files');
559
590
  } finally {
560
591
  storeSetUploadProgress(null);
561
592
  }
562
593
  };
563
594
 
564
- const handleFileUpload = async () => {
595
+ const handleFileSelection = useCallback(async (selectedFiles: File[] | any[]) => {
596
+ const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
597
+ const processedFiles: Array<{ file: File | Blob; preview?: string; size: number; name: string; type: string }> = [];
598
+
599
+ for (const file of selectedFiles) {
600
+ // Validate file size
601
+ if (file.size > MAX_FILE_SIZE) {
602
+ toast.error(`"${file.name}" is too large. Maximum file size is ${formatFileSize(MAX_FILE_SIZE)}`);
603
+ continue;
604
+ }
605
+
606
+ // Generate preview for images
607
+ let preview: string | undefined;
608
+ if (file.type.startsWith('image/')) {
609
+ preview = URL.createObjectURL(file);
610
+ }
611
+
612
+ processedFiles.push({
613
+ file,
614
+ preview,
615
+ size: file.size,
616
+ name: file.name,
617
+ type: file.type
618
+ });
619
+ }
620
+
621
+ if (processedFiles.length === 0) return;
622
+
623
+ // Show preview modal for user to review files before upload
624
+ setPendingFiles(processedFiles);
625
+ setShowUploadPreview(true);
626
+ }, []);
627
+
628
+ const handleConfirmUpload = async () => {
629
+ if (pendingFiles.length === 0) return;
630
+
631
+ setShowUploadPreview(false);
632
+ uploadStartRef.current = Date.now();
633
+ storeSetUploading(true);
634
+ storeSetUploadProgress(null);
635
+
565
636
  try {
566
- uploadStartRef.current = Date.now();
567
- storeSetUploading(true);
568
- storeSetUploadProgress(null);
637
+ const filesToUpload = pendingFiles.map(pf => pf.file as File);
638
+ storeSetUploadProgress({ current: 0, total: filesToUpload.length });
639
+ await processFileUploads(filesToUpload);
640
+
641
+ // Cleanup preview URLs
642
+ pendingFiles.forEach(pf => {
643
+ if (pf.preview) {
644
+ URL.revokeObjectURL(pf.preview);
645
+ }
646
+ });
647
+ setPendingFiles([]);
648
+ endUpload();
649
+ } catch (error: any) {
650
+ toast.error(error.message || 'Failed to upload files');
651
+ endUpload();
652
+ }
653
+ };
654
+
655
+ const handleCancelUpload = () => {
656
+ // Cleanup preview URLs
657
+ pendingFiles.forEach(pf => {
658
+ if (pf.preview) {
659
+ URL.revokeObjectURL(pf.preview);
660
+ }
661
+ });
662
+ setPendingFiles([]);
663
+ setShowUploadPreview(false);
664
+ };
665
+
666
+ const removePendingFile = (index: number) => {
667
+ const file = pendingFiles[index];
668
+ if (file.preview) {
669
+ URL.revokeObjectURL(file.preview);
670
+ }
671
+ const updated = pendingFiles.filter((_, i) => i !== index);
672
+ setPendingFiles(updated);
673
+ if (updated.length === 0) {
674
+ setShowUploadPreview(false);
675
+ }
676
+ };
569
677
 
678
+ const handleFileUpload = async () => {
679
+ try {
570
680
  if (Platform.OS === 'web') {
571
- // Web file picker implementation
681
+ // Enhanced web file picker
572
682
  const input = document.createElement('input');
573
683
  input.type = 'file';
574
684
  input.multiple = true;
575
685
  input.accept = '*/*';
576
- // Fallback: if the user cancels the dialog (no onchange fires or 0 files), hide banner
686
+
577
687
  const cancellationTimer = setTimeout(() => {
578
688
  const state = useFileStore.getState();
579
689
  if (state.uploading && uploadStartRef.current && !state.uploadProgress) {
580
- // No selection happened; treat as cancel
581
690
  endUpload();
582
691
  }
583
- }, 1500); // allow enough time for user to pick
692
+ }, 1500);
584
693
 
585
694
  input.onchange = async (e: any) => {
586
695
  clearTimeout(cancellationTimer);
587
696
  const selectedFiles = Array.from(e.target.files || []) as File[];
588
697
  if (selectedFiles.length === 0) {
589
- // User explicitly canceled (some browsers still fire onchange with empty list)
590
698
  endUpload();
591
699
  return;
592
700
  }
593
- storeSetUploadProgress({ current: 0, total: selectedFiles.length });
594
- await processFileUploads(selectedFiles);
595
- endUpload();
701
+ await handleFileSelection(selectedFiles);
596
702
  };
597
703
 
598
704
  input.click();
599
705
  } else {
600
- // Mobile - show info that file picker can be added
601
- const installCommand = 'npm install expo-document-picker';
602
- const message = `Mobile File Upload\n\nTo enable file uploads on mobile, install expo-document-picker:\n\n${installCommand}\n\nThen import and use DocumentPicker.getDocumentAsync() in this method.`;
603
-
604
- if (window.confirm(`${message}\n\nWould you like to copy the install command?`)) {
605
- toast.info(`Install: ${installCommand}`);
606
- } else {
607
- toast.info('Mobile file upload requires expo-document-picker');
706
+ // Mobile file picker with expo-document-picker
707
+ try {
708
+ // Dynamically import to avoid breaking if not installed
709
+ const DocumentPicker = await import('expo-document-picker').catch(() => null);
710
+
711
+ if (!DocumentPicker) {
712
+ toast.error('File picker not available. Please install expo-document-picker');
713
+ return;
714
+ }
715
+
716
+ const result = await DocumentPicker.getDocumentAsync({
717
+ type: '*/*',
718
+ multiple: true,
719
+ copyToCacheDirectory: true,
720
+ });
721
+
722
+ if (result.canceled) {
723
+ return;
724
+ }
725
+
726
+ // Convert expo document picker results to File-like objects
727
+ const files: File[] = [];
728
+ for (const doc of result.assets) {
729
+ if (doc.file) {
730
+ // expo-document-picker provides a File-like object
731
+ files.push(doc.file as File);
732
+ } else if (doc.uri) {
733
+ // Fallback: fetch and create Blob
734
+ const response = await fetch(doc.uri);
735
+ const blob = await response.blob();
736
+ const file = new File([blob], doc.name || 'file', { type: doc.mimeType || 'application/octet-stream' });
737
+ files.push(file);
738
+ }
739
+ }
740
+
741
+ if (files.length > 0) {
742
+ await handleFileSelection(files);
743
+ }
744
+ } catch (error: any) {
745
+ if (error.message?.includes('expo-document-picker')) {
746
+ toast.error('File picker not available. Please install expo-document-picker');
747
+ } else {
748
+ toast.error(error.message || 'Failed to select files');
749
+ }
608
750
  }
609
751
  }
610
752
  } catch (error: any) {
611
- toast.error(error.message || 'Failed to upload file');
612
- } finally {
613
- // IMPORTANT: Do NOT call endUpload here.
614
- // We only want to hide the banner after the actual upload(s) complete.
615
- // The input.onchange handler invokes processFileUploads then calls endUpload().
616
- // Calling endUpload here caused the banner to disappear while files were still uploading.
617
- storeSetUploadProgress(null); // keep clearing any stale progress
753
+ toast.error(error.message || 'Failed to open file picker');
618
754
  }
619
755
  };
620
756
 
@@ -623,18 +759,12 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
623
759
  const confirmed = window.confirm(`Are you sure you want to delete "${filename}"? This action cannot be undone.`);
624
760
 
625
761
  if (!confirmed) {
626
- console.log('Delete cancelled by user');
627
762
  return;
628
763
  }
629
764
 
630
765
  try {
631
- console.log('Deleting file:', { fileId, filename });
632
- console.log('Target user ID:', targetUserId);
633
- console.log('Current user ID:', user?.id);
634
766
  storeSetDeleting(fileId);
635
-
636
- const result = await oxyServices.deleteFile(fileId);
637
- console.log('Delete result:', result);
767
+ await oxyServices.deleteFile(fileId);
638
768
 
639
769
  toast.success('File deleted successfully');
640
770
 
@@ -644,8 +774,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
644
774
  // Silent background reconcile
645
775
  setTimeout(() => loadFiles('silent'), 800);
646
776
  } catch (error: any) {
647
- console.error('Delete error:', error);
648
- console.error('Error details:', error.response?.data || error.message);
649
777
 
650
778
  // Provide specific error messages
651
779
  if (error.message?.includes('File not found') || error.message?.includes('404')) {
@@ -662,88 +790,155 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
662
790
  }
663
791
  };
664
792
 
665
- // Drag and drop handlers for web
666
- const handleDragOver = (e: any) => {
667
- if (Platform.OS === 'web' && user?.id === targetUserId) {
668
- e.preventDefault();
669
- setIsDragging(true);
670
- }
671
- };
672
-
673
- const handleDragEnter = (e: any) => {
674
- if (Platform.OS === 'web' && user?.id === targetUserId) {
675
- e.preventDefault();
676
- setIsDragging(true);
793
+ const handleBulkDelete = useCallback(async () => {
794
+ if (selectedIds.size === 0) return;
795
+
796
+ const fileMap: Record<string, FileMetadata> = {};
797
+ files.forEach(f => { fileMap[f.id] = f; });
798
+ const selectedFiles = Array.from(selectedIds).map(id => fileMap[id]).filter(Boolean);
799
+
800
+ const confirmed = window.confirm(
801
+ `Are you sure you want to delete ${selectedFiles.length} file(s)? This action cannot be undone.`
802
+ );
803
+
804
+ if (!confirmed) return;
805
+
806
+ try {
807
+ const deletePromises = Array.from(selectedIds).map(async (fileId) => {
808
+ try {
809
+ await oxyServices.deleteFile(fileId);
810
+ useFileStore.getState().removeFile(fileId);
811
+ return { success: true, fileId };
812
+ } catch (error: any) {
813
+ return { success: false, fileId, error };
814
+ }
815
+ });
816
+
817
+ const results = await Promise.allSettled(deletePromises);
818
+ const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
819
+ const failed = results.length - successful;
820
+
821
+ if (successful > 0) {
822
+ toast.success(`${successful} file(s) deleted successfully`);
823
+ }
824
+ if (failed > 0) {
825
+ toast.error(`${failed} file(s) failed to delete`);
826
+ }
827
+
828
+ setSelectedIds(new Set());
829
+ setTimeout(() => loadFiles('silent'), 800);
830
+ } catch (error: any) {
831
+ toast.error(error.message || 'Failed to delete files');
677
832
  }
678
- };
833
+ }, [selectedIds, files, oxyServices, loadFiles]);
679
834
 
680
- const handleDragLeave = (e: any) => {
681
- if (Platform.OS === 'web') {
682
- e.preventDefault();
683
- setIsDragging(false);
835
+ const handleBulkVisibilityChange = useCallback(async (visibility: 'private' | 'public' | 'unlisted') => {
836
+ if (selectedIds.size === 0) return;
837
+
838
+ try {
839
+ const updatePromises = Array.from(selectedIds).map(async (fileId) => {
840
+ try {
841
+ await oxyServices.assetUpdateVisibility(fileId, visibility);
842
+ return { success: true, fileId };
843
+ } catch (error: any) {
844
+ return { success: false, fileId, error };
845
+ }
846
+ });
847
+
848
+ const results = await Promise.allSettled(updatePromises);
849
+ const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
850
+ const failed = results.length - successful;
851
+
852
+ if (successful > 0) {
853
+ toast.success(`${successful} file(s) visibility updated to ${visibility}`);
854
+ // Update file metadata in store
855
+ Array.from(selectedIds).forEach(fileId => {
856
+ useFileStore.getState().updateFile(fileId, {
857
+ metadata: { ...files.find(f => f.id === fileId)?.metadata, visibility }
858
+ } as any);
859
+ });
860
+ }
861
+ if (failed > 0) {
862
+ toast.error(`${failed} file(s) failed to update visibility`);
863
+ }
864
+
865
+ setTimeout(() => loadFiles('silent'), 800);
866
+ } catch (error: any) {
867
+ toast.error(error.message || 'Failed to update visibility');
684
868
  }
685
- };
869
+ }, [selectedIds, oxyServices, files, loadFiles]);
686
870
 
687
- // Global drag listeners (web) to catch drags outside component bounds
871
+ // Global drag listeners (web) - attach to document for reliable drag and drop
688
872
  useEffect(() => {
689
873
  if (Platform.OS !== 'web' || user?.id !== targetUserId) return;
690
- const onDocDragEnter = (e: any) => {
691
- if (e?.dataTransfer?.types?.includes('Files')) setIsDragging(true);
874
+
875
+ let dragCounter = 0; // Track drag enter/leave to handle nested elements
876
+
877
+ const onDragEnter = (e: DragEvent) => {
878
+ dragCounter++;
879
+ if (e?.dataTransfer?.types?.includes('Files')) {
880
+ e.preventDefault();
881
+ e.stopPropagation();
882
+ setIsDragging(true);
883
+ }
692
884
  };
693
- const onDocDragOver = (e: any) => {
885
+
886
+ const onDragOver = (e: DragEvent) => {
694
887
  if (e?.dataTransfer?.types?.includes('Files')) {
695
888
  e.preventDefault();
889
+ e.stopPropagation();
890
+ // Keep dragging state true while over document
696
891
  setIsDragging(true);
697
892
  }
698
893
  };
699
- const onDocDrop = (e: any) => {
894
+
895
+ const onDrop = async (e: DragEvent) => {
896
+ dragCounter = 0;
897
+ setIsDragging(false);
898
+
700
899
  if (e?.dataTransfer?.files?.length) {
701
900
  e.preventDefault();
702
- setIsDragging(false);
901
+ e.stopPropagation();
902
+
903
+ try {
904
+ const files = Array.from(e.dataTransfer.files) as File[];
905
+ if (files.length > 0) {
906
+ await handleFileSelection(files);
907
+ }
908
+ } catch (error: any) {
909
+ toast.error(error.message || 'Failed to upload files');
910
+ }
703
911
  }
704
912
  };
705
- const onDocDragLeave = (e: any) => {
706
- if (!e.relatedTarget && e.screenX === 0 && e.screenY === 0) setIsDragging(false);
913
+
914
+ const onDragLeave = (e: DragEvent) => {
915
+ dragCounter--;
916
+ // Only hide drag overlay if we're actually leaving the document (drag counter reaches 0)
917
+ if (dragCounter === 0) {
918
+ setIsDragging(false);
919
+ }
707
920
  };
708
- document.addEventListener('dragenter', onDocDragEnter);
709
- document.addEventListener('dragover', onDocDragOver);
710
- document.addEventListener('drop', onDocDrop);
711
- document.addEventListener('dragleave', onDocDragLeave);
921
+
922
+ // Attach to document for global drag detection
923
+ document.addEventListener('dragenter', onDragEnter, false);
924
+ document.addEventListener('dragover', onDragOver, false);
925
+ document.addEventListener('drop', onDrop, false);
926
+ document.addEventListener('dragleave', onDragLeave, false);
927
+
712
928
  return () => {
713
- document.removeEventListener('dragenter', onDocDragEnter);
714
- document.removeEventListener('dragover', onDocDragOver);
715
- document.removeEventListener('drop', onDocDrop);
716
- document.removeEventListener('dragleave', onDocDragLeave);
929
+ document.removeEventListener('dragenter', onDragEnter, false);
930
+ document.removeEventListener('dragover', onDragOver, false);
931
+ document.removeEventListener('drop', onDrop, false);
932
+ document.removeEventListener('dragleave', onDragLeave, false);
717
933
  };
718
- }, [user?.id, targetUserId]);
934
+ }, [user?.id, targetUserId, handleFileSelection]);
719
935
 
720
- const handleDrop = async (e: any) => {
721
- if (Platform.OS === 'web' && user?.id === targetUserId) {
722
- e.preventDefault();
723
- setIsDragging(false);
724
- uploadStartRef.current = Date.now();
725
- storeSetUploading(true);
726
-
727
- try {
728
- const files = Array.from(e.dataTransfer.files) as File[];
729
- if (files.length > 0) storeSetUploadProgress({ current: 0, total: files.length });
730
- await processFileUploads(files);
731
- } catch (error: any) {
732
- toast.error(error.message || 'Failed to upload files');
733
- } finally {
734
- endUpload();
735
- }
736
- }
737
- };
738
936
 
739
937
  const handleFileDownload = async (fileId: string, filename: string) => {
740
938
  try {
741
939
  if (Platform.OS === 'web') {
742
- console.log('Downloading file:', { fileId, filename });
743
-
744
940
  // Use the public download URL method
745
941
  const downloadUrl = oxyServices.getFileDownloadUrl(fileId);
746
- console.log('Download URL:', downloadUrl);
747
942
 
748
943
  try {
749
944
  // Method 1: Try simple link download first
@@ -757,7 +952,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
757
952
 
758
953
  toast.success('File download started');
759
954
  } catch (linkError) {
760
- console.warn('Link download failed, trying fetch method:', linkError);
761
955
 
762
956
  // Method 2: Fallback to authenticated download
763
957
  const blob = await oxyServices.getFileContentAsBlob(fileId);
@@ -779,7 +973,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
779
973
  toast.info('File download not implemented for mobile yet');
780
974
  }
781
975
  } catch (error: any) {
782
- console.error('Download error:', error);
783
976
  toast.error(error.message || 'Failed to download file');
784
977
  }
785
978
  };
@@ -837,7 +1030,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
837
1030
  setFileContent(content);
838
1031
  }
839
1032
  } catch (error: any) {
840
- console.error('Failed to load file content:', error);
841
1033
  if (error.message?.includes('404') || error.message?.includes('not found')) {
842
1034
  toast.error('File not found. It may have been deleted.');
843
1035
  } else {
@@ -850,7 +1042,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
850
1042
  setFileContent(null);
851
1043
  }
852
1044
  } catch (error: any) {
853
- console.error('Failed to open file:', error);
854
1045
  toast.error(error.message || 'Failed to open file');
855
1046
  } finally {
856
1047
  setLoadingFileContent(false);
@@ -905,8 +1096,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
905
1096
  contentFit="cover"
906
1097
  transition={120}
907
1098
  cachePolicy="memory-disk"
908
- onError={(e: any) => {
909
- console.error('Photo failed to load:', (e as any)?.nativeEvent ?? e);
1099
+ onError={() => {
1100
+ // Photo failed to load, will show placeholder
910
1101
  }}
911
1102
  accessibilityLabel={photo.filename}
912
1103
  />
@@ -945,8 +1136,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
945
1136
  contentFit="cover"
946
1137
  transition={120}
947
1138
  cachePolicy="memory-disk"
948
- onError={(e: any) => {
949
- console.error('Photo failed to load:', (e as any)?.nativeEvent ?? e);
1139
+ onError={() => {
1140
+ // Photo failed to load, will show placeholder
950
1141
  }}
951
1142
  accessibilityLabel={photo.filename}
952
1143
  />
@@ -1005,8 +1196,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1005
1196
  contentFit="cover"
1006
1197
  transition={120}
1007
1198
  cachePolicy="memory-disk"
1008
- onError={(_: any) => {
1009
- console.warn('Failed to load image preview.');
1199
+ onError={() => {
1200
+ // Image preview failed to load
1010
1201
  }}
1011
1202
  accessibilityLabel={file.filename}
1012
1203
  />
@@ -1129,10 +1320,15 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1129
1320
 
1130
1321
  // GroupedSection-based file items (for 'all' view) replacing legacy flat list look
1131
1322
  const groupedFileItems = useMemo(() => {
1132
- return filteredFiles
1133
- .filter(f => true)
1134
- .sort((a, b) => new Date(b.uploadDate).getTime() - new Date(a.uploadDate).getTime())
1135
- .map((file) => {
1323
+ // filteredFiles is already sorted, so just use it directly
1324
+ const sortedFiles = filteredFiles;
1325
+
1326
+ // Store file positions for scrolling
1327
+ sortedFiles.forEach((file, index) => {
1328
+ itemRefs.current.set(file.id, index);
1329
+ });
1330
+
1331
+ return sortedFiles.map((file) => {
1136
1332
  const isImage = file.contentType.startsWith('image/');
1137
1333
  const isVideo = file.contentType.startsWith('video/');
1138
1334
  const hasPreview = isImage || isVideo;
@@ -1147,13 +1343,29 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1147
1343
  title: file.filename,
1148
1344
  subtitle: `${formatFileSize(file.length)} • ${new Date(file.uploadDate).toLocaleDateString()}`,
1149
1345
  theme: theme as 'light' | 'dark',
1150
- onPress: () => handleFileOpen(file),
1346
+ onPress: () => {
1347
+ // Support selection in regular mode with long press or if already selecting
1348
+ if (!selectMode && selectedIds.size > 0) {
1349
+ // If already in selection mode (some files selected), toggle selection
1350
+ toggleSelect(file);
1351
+ } else {
1352
+ handleFileOpen(file);
1353
+ }
1354
+ },
1355
+ onLongPress: !selectMode ? () => {
1356
+ // Enable selection mode on long press
1357
+ if (selectedIds.size === 0) {
1358
+ setSelectedIds(new Set([file.id]));
1359
+ } else {
1360
+ toggleSelect(file);
1361
+ }
1362
+ } : undefined,
1151
1363
  showChevron: false,
1152
1364
  dense: true,
1153
1365
  multiRow: !!file.metadata?.description,
1154
- selected: selectMode && isSelected,
1155
- // Hide action buttons when selecting
1156
- customContent: !selectMode ? (
1366
+ selected: (selectMode || selectedIds.size > 0) && isSelected,
1367
+ // Hide action buttons when selecting (in selectMode or bulk operations mode)
1368
+ customContent: (!selectMode && selectedIds.size === 0) ? (
1157
1369
  <View style={styles.groupedActions}>
1158
1370
  {(isImage || isVideo || file.contentType.includes('pdf')) && (
1159
1371
  <TouchableOpacity
@@ -1191,6 +1403,92 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1191
1403
  });
1192
1404
  }, [filteredFiles, theme, themeStyles, deleting, handleFileDownload, handleFileDelete, handleFileOpen, getSafeDownloadUrl, selectMode, selectedIds]);
1193
1405
 
1406
+ // Scroll to selected file after selection
1407
+ useEffect(() => {
1408
+ if (lastSelectedFileId && selectMode) {
1409
+ if (viewMode === 'all' && scrollViewRef.current) {
1410
+ // Find the index of the selected file
1411
+ const itemIndex = itemRefs.current.get(lastSelectedFileId);
1412
+
1413
+ if (itemIndex !== undefined && itemIndex >= 0) {
1414
+ // Estimate item height (GroupedItem with dense mode is approximately 60-70px)
1415
+ // Account for description rows which add extra height
1416
+ const baseItemHeight = 65;
1417
+ const descriptionHeight = 30; // Approximate height for description
1418
+ // Use filteredFiles which is already sorted according to user's selection
1419
+ const sortedFiles = filteredFiles;
1420
+
1421
+ // Calculate total height up to this item
1422
+ let scrollPosition = 0;
1423
+ for (let i = 0; i <= itemIndex && i < sortedFiles.length; i++) {
1424
+ const file = sortedFiles[i];
1425
+ scrollPosition += baseItemHeight;
1426
+ if (file.metadata?.description) {
1427
+ scrollPosition += descriptionHeight;
1428
+ }
1429
+ }
1430
+
1431
+ // Add header, controls, search, and stats height (approximately 250px)
1432
+ const headerHeight = 250;
1433
+ const finalScrollPosition = headerHeight + scrollPosition - 150; // Offset to show item near top
1434
+
1435
+ // Use requestAnimationFrame to ensure DOM is updated before scrolling
1436
+ requestAnimationFrame(() => {
1437
+ requestAnimationFrame(() => {
1438
+ scrollViewRef.current?.scrollTo({
1439
+ y: Math.max(0, finalScrollPosition),
1440
+ animated: true,
1441
+ });
1442
+ });
1443
+ });
1444
+ }
1445
+ } else if (viewMode === 'photos' && photoScrollViewRef.current) {
1446
+ // For photo grid, find the photo index
1447
+ const photos = filteredFiles.filter(file => file.contentType.startsWith('image/'));
1448
+ const photoIndex = photos.findIndex(p => p.id === lastSelectedFileId);
1449
+
1450
+ if (photoIndex >= 0) {
1451
+ // Estimate photo item height based on grid layout
1452
+ // Calculate items per row
1453
+ let itemsPerRow = 3;
1454
+ if (containerWidth > 768) itemsPerRow = 6;
1455
+ else if (containerWidth > 480) itemsPerRow = 4;
1456
+
1457
+ const scrollContainerPadding = 32;
1458
+ const gaps = (itemsPerRow - 1) * 4;
1459
+ const availableWidth = containerWidth - scrollContainerPadding;
1460
+ const itemWidth = (availableWidth - gaps) / itemsPerRow;
1461
+
1462
+ // Calculate row and approximate scroll position
1463
+ const row = Math.floor(photoIndex / itemsPerRow);
1464
+ const headerHeight = 250;
1465
+ const finalScrollPosition = headerHeight + (row * (itemWidth + 4)) - 150;
1466
+
1467
+ // Use requestAnimationFrame to ensure DOM is updated before scrolling
1468
+ requestAnimationFrame(() => {
1469
+ requestAnimationFrame(() => {
1470
+ photoScrollViewRef.current?.scrollTo({
1471
+ y: Math.max(0, finalScrollPosition),
1472
+ animated: true,
1473
+ });
1474
+ });
1475
+ });
1476
+ }
1477
+ }
1478
+ }
1479
+ }, [lastSelectedFileId, selectMode, viewMode, filteredFiles, containerWidth]);
1480
+
1481
+ // Clear selected file ID after scroll animation completes
1482
+ useEffect(() => {
1483
+ if (lastSelectedFileId && scrollViewRef.current) {
1484
+ const timeoutId = setTimeout(() => {
1485
+ setLastSelectedFileId(null);
1486
+ }, 600); // Allow time for scroll animation to complete
1487
+
1488
+ return () => clearTimeout(timeoutId);
1489
+ }
1490
+ }, [lastSelectedFileId]);
1491
+
1194
1492
  const renderPhotoItem = (photo: FileMetadata, index: number) => {
1195
1493
  const downloadUrl = getSafeDownloadUrl(photo, 'thumb');
1196
1494
 
@@ -1225,8 +1523,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1225
1523
  contentFit="cover"
1226
1524
  transition={120}
1227
1525
  cachePolicy="memory-disk"
1228
- onError={(_: any) => {
1229
- console.warn('Failed to load image preview for photo:', photo.id);
1526
+ onError={() => {
1527
+ // Image preview failed to load
1230
1528
  }}
1231
1529
  accessibilityLabel={photo.filename}
1232
1530
  />
@@ -1270,6 +1568,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1270
1568
 
1271
1569
  return (
1272
1570
  <ScrollView
1571
+ ref={photoScrollViewRef}
1273
1572
  style={styles.scrollView}
1274
1573
  contentContainerStyle={styles.photoScrollContainer}
1275
1574
  refreshControl={
@@ -1621,8 +1920,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1621
1920
  contentFit="contain"
1622
1921
  transition={120}
1623
1922
  cachePolicy="memory-disk"
1624
- onError={(e: any) => {
1625
- console.error('Image failed to load:', (e as any)?.nativeEvent ?? e);
1923
+ onError={() => {
1924
+ // Image failed to load
1626
1925
  }}
1627
1926
  accessibilityLabel={openedFile.filename}
1628
1927
  />
@@ -1741,13 +2040,190 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1741
2040
  </View>
1742
2041
  );
1743
2042
 
1744
- if (loading) {
2043
+ // Professional Skeleton Loading Component with Advanced Shimmer Effect
2044
+ const SkeletonLoader = React.memo(() => {
2045
+ const shimmerAnim = useRef(new Animated.Value(0)).current;
2046
+ const skeletonContainerWidth = containerWidth || 400;
2047
+
2048
+ useEffect(() => {
2049
+ const shimmer = Animated.loop(
2050
+ Animated.timing(shimmerAnim, {
2051
+ toValue: 1,
2052
+ duration: 2000,
2053
+ easing: Easing.linear,
2054
+ useNativeDriver: true,
2055
+ })
2056
+ );
2057
+ shimmer.start();
2058
+ return () => shimmer.stop();
2059
+ }, [shimmerAnim]);
2060
+
2061
+ // Create a sweeping shimmer effect
2062
+ const shimmerTranslateX = shimmerAnim.interpolate({
2063
+ inputRange: [0, 1],
2064
+ outputRange: [-skeletonContainerWidth * 2, skeletonContainerWidth * 2],
2065
+ });
2066
+
2067
+ const SkeletonBox = ({ width, height, borderRadius = 8, style, delay = 0 }: { width: number | string; height: number; borderRadius?: number; style?: any; delay?: number }) => {
2068
+ const delayedTranslateX = shimmerAnim.interpolate({
2069
+ inputRange: [0, 1],
2070
+ outputRange: [-skeletonContainerWidth * 2 + delay, skeletonContainerWidth * 2 + delay],
2071
+ });
2072
+
2073
+ return (
2074
+ <View
2075
+ style={[
2076
+ {
2077
+ width,
2078
+ height,
2079
+ borderRadius,
2080
+ backgroundColor: themeStyles.isDarkTheme ? '#1E1E1E' : '#F5F5F5',
2081
+ overflow: 'hidden',
2082
+ position: 'relative',
2083
+ },
2084
+ style,
2085
+ ]}
2086
+ >
2087
+ {/* Base background */}
2088
+ <View
2089
+ style={{
2090
+ position: 'absolute',
2091
+ top: 0,
2092
+ left: 0,
2093
+ right: 0,
2094
+ bottom: 0,
2095
+ backgroundColor: themeStyles.isDarkTheme ? '#1E1E1E' : '#F5F5F5',
2096
+ }}
2097
+ />
2098
+ {/* Shimmer gradient effect */}
2099
+ <Animated.View
2100
+ style={{
2101
+ position: 'absolute',
2102
+ top: 0,
2103
+ left: 0,
2104
+ width: '100%',
2105
+ height: '100%',
2106
+ transform: [{ translateX: delayedTranslateX }],
2107
+ }}
2108
+ >
2109
+ <View
2110
+ style={{
2111
+ width: skeletonContainerWidth,
2112
+ height: '100%',
2113
+ backgroundColor: themeStyles.isDarkTheme
2114
+ ? 'rgba(255, 255, 255, 0.08)'
2115
+ : 'rgba(255, 255, 255, 0.8)',
2116
+ shadowColor: themeStyles.isDarkTheme ? '#000' : '#FFF',
2117
+ shadowOffset: { width: 0, height: 0 },
2118
+ shadowOpacity: 0.3,
2119
+ shadowRadius: 10,
2120
+ }}
2121
+ />
2122
+ </Animated.View>
2123
+ </View>
2124
+ );
2125
+ };
2126
+
2127
+ // Skeleton file item matching GroupedSection structure
2128
+ const SkeletonFileItem = ({ index }: { index: number }) => (
2129
+ <View
2130
+ style={[
2131
+ {
2132
+ flexDirection: 'row',
2133
+ alignItems: 'center',
2134
+ paddingHorizontal: 16,
2135
+ paddingVertical: 12,
2136
+ backgroundColor: themeStyles.isDarkTheme ? '#121212' : '#FFFFFF',
2137
+ borderBottomWidth: StyleSheet.hairlineWidth,
2138
+ borderBottomColor: themeStyles.borderColor,
2139
+ },
2140
+ ]}
2141
+ >
2142
+ {/* Icon/Image skeleton */}
2143
+ <SkeletonBox width={44} height={44} borderRadius={8} delay={index * 50} />
2144
+
2145
+ {/* Content skeleton */}
2146
+ <View style={{ flex: 1, marginLeft: 12, justifyContent: 'center' }}>
2147
+ <SkeletonBox
2148
+ width={index % 3 === 0 ? '85%' : index % 3 === 1 ? '70%' : '90%'}
2149
+ height={16}
2150
+ style={{ marginBottom: 8 }}
2151
+ delay={index * 50 + 20}
2152
+ />
2153
+ <SkeletonBox
2154
+ width={index % 2 === 0 ? '50%' : '60%'}
2155
+ height={12}
2156
+ delay={index * 50 + 40}
2157
+ />
2158
+ </View>
2159
+ </View>
2160
+ );
2161
+
1745
2162
  return (
1746
- <View style={[styles.container, styles.centerContent, { backgroundColor }]}>
1747
- <ActivityIndicator size="large" color={themeStyles.primaryColor} />
1748
- <Text style={[styles.loadingText, { color: themeStyles.textColor }]}>Loading files...</Text>
2163
+ <View style={[styles.container, { backgroundColor }]}>
2164
+ {/* Header Skeleton */}
2165
+ <View style={[styles.header, { borderBottomColor: themeStyles.borderColor, borderBottomWidth: StyleSheet.hairlineWidth }]}>
2166
+ <SkeletonBox width={44} height={44} borderRadius={12} />
2167
+ <View style={[styles.headerTitleContainer, { flex: 1 }]}>
2168
+ <SkeletonBox width={140} height={20} style={{ marginBottom: 6 }} />
2169
+ <SkeletonBox width={100} height={14} />
2170
+ </View>
2171
+ <SkeletonBox width={44} height={44} borderRadius={12} />
2172
+ </View>
2173
+
2174
+ {/* Controls Bar Skeleton */}
2175
+ <View style={styles.controlsBar}>
2176
+ <SkeletonBox width={100} height={36} borderRadius={18} />
2177
+ <SkeletonBox width={44} height={44} borderRadius={22} />
2178
+ </View>
2179
+
2180
+ {/* Search Bar Skeleton */}
2181
+ <View style={[styles.searchContainer, {
2182
+ backgroundColor: themeStyles.isDarkTheme ? '#1A1A1A' : '#FFFFFF',
2183
+ borderColor: themeStyles.borderColor,
2184
+ borderWidth: StyleSheet.hairlineWidth,
2185
+ }]}>
2186
+ <SkeletonBox width="100%" height={44} borderRadius={12} />
2187
+ </View>
2188
+
2189
+ {/* Stats Container Skeleton */}
2190
+ <View style={[styles.statsContainer, {
2191
+ backgroundColor: themeStyles.isDarkTheme ? '#1A1A1A' : '#FFFFFF',
2192
+ borderColor: themeStyles.borderColor,
2193
+ borderWidth: StyleSheet.hairlineWidth,
2194
+ }]}>
2195
+ {[1, 2, 3].map((i) => (
2196
+ <View key={i} style={styles.statItem}>
2197
+ <SkeletonBox width={50} height={20} style={{ marginBottom: 4 }} delay={i * 30} />
2198
+ <SkeletonBox width={40} height={14} delay={i * 30 + 15} />
2199
+ </View>
2200
+ ))}
2201
+ </View>
2202
+
2203
+ {/* File List Skeleton - Matching GroupedSection */}
2204
+ <ScrollView
2205
+ style={styles.scrollView}
2206
+ contentContainerStyle={styles.scrollContainer}
2207
+ showsVerticalScrollIndicator={false}
2208
+ >
2209
+ <View style={{
2210
+ backgroundColor: themeStyles.isDarkTheme ? '#121212' : '#FFFFFF',
2211
+ borderRadius: 12,
2212
+ overflow: 'hidden',
2213
+ marginHorizontal: 16,
2214
+ marginTop: 8,
2215
+ }}>
2216
+ {[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
2217
+ <SkeletonFileItem key={i} index={i} />
2218
+ ))}
2219
+ </View>
2220
+ </ScrollView>
1749
2221
  </View>
1750
2222
  );
2223
+ });
2224
+
2225
+ if (loading) {
2226
+ return <SkeletonLoader />;
1751
2227
  }
1752
2228
 
1753
2229
  // If a file is opened, show the file viewer
@@ -1762,16 +2238,11 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1762
2238
 
1763
2239
  return (
1764
2240
  <View
2241
+ ref={containerRef}
1765
2242
  style={[
1766
2243
  styles.container,
1767
2244
  isDragging && Platform.OS === 'web' && styles.dragOverlay
1768
2245
  ]}
1769
- {...(Platform.OS === 'web' && user?.id === targetUserId ? {
1770
- onDragOver: handleDragOver,
1771
- onDragEnter: handleDragEnter,
1772
- onDragLeave: handleDragLeave,
1773
- onDrop: handleDrop,
1774
- } : {})}
1775
2246
  >
1776
2247
  <Header
1777
2248
  title={selectMode ? (multiSelect ? `${selectedIds.size}${maxSelection ? '/' + maxSelection : ''} Selected` : 'Select a File') : (viewMode === 'photos' ? 'Photos' : 'File Management')}
@@ -1789,6 +2260,36 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1789
2260
  onPress: confirmMultiSelection,
1790
2261
  disabled: selectedIds.size === 0,
1791
2262
  }
2263
+ ] : !selectMode && selectedIds.size > 0 ? [
2264
+ {
2265
+ key: 'clear',
2266
+ text: 'Clear',
2267
+ onPress: () => setSelectedIds(new Set()),
2268
+ },
2269
+ {
2270
+ key: 'delete',
2271
+ text: `Delete (${selectedIds.size})`,
2272
+ onPress: handleBulkDelete,
2273
+ icon: 'trash',
2274
+ },
2275
+ {
2276
+ key: 'visibility',
2277
+ text: 'Visibility',
2278
+ onPress: () => {
2279
+ // Show visibility options menu
2280
+ Alert.alert(
2281
+ 'Change Visibility',
2282
+ `Change visibility for ${selectedIds.size} file(s)?`,
2283
+ [
2284
+ { text: 'Cancel', style: 'cancel' },
2285
+ { text: 'Private', onPress: () => handleBulkVisibilityChange('private') },
2286
+ { text: 'Public', onPress: () => handleBulkVisibilityChange('public') },
2287
+ { text: 'Unlisted', onPress: () => handleBulkVisibilityChange('unlisted') },
2288
+ ]
2289
+ );
2290
+ },
2291
+ icon: 'eye',
2292
+ }
1792
2293
  ] : undefined}
1793
2294
  onBack={onClose || goBack}
1794
2295
  theme={theme}
@@ -1799,41 +2300,119 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1799
2300
  />
1800
2301
 
1801
2302
  <View style={styles.controlsBar}>
1802
- <View style={[
1803
- styles.viewModeToggle,
1804
- {
2303
+ <ScrollView
2304
+ horizontal
2305
+ showsHorizontalScrollIndicator={false}
2306
+ style={styles.viewModeScroll}
2307
+ >
2308
+ <View style={[
2309
+ styles.viewModeToggle,
2310
+ {
2311
+ backgroundColor: themeStyles.isDarkTheme ? '#181818' : '#FFFFFF',
2312
+ borderWidth: 1,
2313
+ borderColor: themeStyles.isDarkTheme ? '#2A2A2A' : '#E8E9EA',
2314
+ }
2315
+ ]}>
2316
+ <TouchableOpacity
2317
+ style={[
2318
+ styles.viewModeButton,
2319
+ viewMode === 'all' && { backgroundColor: themeStyles.primaryColor }
2320
+ ]}
2321
+ onPress={() => setViewMode('all')}
2322
+ >
2323
+ <Ionicons
2324
+ name="folder"
2325
+ size={18}
2326
+ color={viewMode === 'all' ? '#FFFFFF' : themeStyles.textColor}
2327
+ />
2328
+ </TouchableOpacity>
2329
+ <TouchableOpacity
2330
+ style={[
2331
+ styles.viewModeButton,
2332
+ viewMode === 'photos' && { backgroundColor: themeStyles.primaryColor }
2333
+ ]}
2334
+ onPress={() => setViewMode('photos')}
2335
+ >
2336
+ <Ionicons
2337
+ name="images"
2338
+ size={18}
2339
+ color={viewMode === 'photos' ? '#FFFFFF' : themeStyles.textColor}
2340
+ />
2341
+ </TouchableOpacity>
2342
+ <TouchableOpacity
2343
+ style={[
2344
+ styles.viewModeButton,
2345
+ viewMode === 'videos' && { backgroundColor: themeStyles.primaryColor }
2346
+ ]}
2347
+ onPress={() => setViewMode('videos')}
2348
+ >
2349
+ <Ionicons
2350
+ name="videocam"
2351
+ size={18}
2352
+ color={viewMode === 'videos' ? '#FFFFFF' : themeStyles.textColor}
2353
+ />
2354
+ </TouchableOpacity>
2355
+ <TouchableOpacity
2356
+ style={[
2357
+ styles.viewModeButton,
2358
+ viewMode === 'documents' && { backgroundColor: themeStyles.primaryColor }
2359
+ ]}
2360
+ onPress={() => setViewMode('documents')}
2361
+ >
2362
+ <Ionicons
2363
+ name="document-text"
2364
+ size={18}
2365
+ color={viewMode === 'documents' ? '#FFFFFF' : themeStyles.textColor}
2366
+ />
2367
+ </TouchableOpacity>
2368
+ <TouchableOpacity
2369
+ style={[
2370
+ styles.viewModeButton,
2371
+ viewMode === 'audio' && { backgroundColor: themeStyles.primaryColor }
2372
+ ]}
2373
+ onPress={() => setViewMode('audio')}
2374
+ >
2375
+ <Ionicons
2376
+ name="musical-notes"
2377
+ size={18}
2378
+ color={viewMode === 'audio' ? '#FFFFFF' : themeStyles.textColor}
2379
+ />
2380
+ </TouchableOpacity>
2381
+ </View>
2382
+ </ScrollView>
2383
+ <TouchableOpacity
2384
+ style={[styles.sortButton, {
1805
2385
  backgroundColor: themeStyles.isDarkTheme ? '#181818' : '#FFFFFF',
1806
- borderWidth: 1,
1807
2386
  borderColor: themeStyles.isDarkTheme ? '#2A2A2A' : '#E8E9EA',
1808
- }
1809
- ]}>
1810
- <TouchableOpacity
1811
- style={[
1812
- styles.viewModeButton,
1813
- viewMode === 'all' && { backgroundColor: themeStyles.primaryColor }
1814
- ]}
1815
- onPress={() => setViewMode('all')}
1816
- >
1817
- <Ionicons
1818
- name="folder"
1819
- size={18}
1820
- color={viewMode === 'all' ? '#FFFFFF' : themeStyles.textColor}
1821
- />
1822
- </TouchableOpacity>
1823
- <TouchableOpacity
1824
- style={[
1825
- styles.viewModeButton,
1826
- viewMode === 'photos' && { backgroundColor: themeStyles.primaryColor }
1827
- ]}
1828
- onPress={() => setViewMode('photos')}
1829
- >
1830
- <Ionicons
1831
- name="images"
1832
- size={18}
1833
- color={viewMode === 'photos' ? '#FFFFFF' : themeStyles.textColor}
1834
- />
1835
- </TouchableOpacity>
1836
- </View>
2387
+ }]}
2388
+ onPress={() => {
2389
+ // Cycle through sort options: date -> size -> name -> type -> date
2390
+ const sortOrder: Array<'date' | 'size' | 'name' | 'type'> = ['date', 'size', 'name', 'type'];
2391
+ const currentIndex = sortOrder.indexOf(sortBy);
2392
+ const nextIndex = (currentIndex + 1) % sortOrder.length;
2393
+ setSortBy(sortOrder[nextIndex]);
2394
+ // Toggle order when cycling back to date
2395
+ if (nextIndex === 0) {
2396
+ setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
2397
+ }
2398
+ }}
2399
+ >
2400
+ <Ionicons
2401
+ name={sortOrder === 'asc' ? 'arrow-up' : 'arrow-down'}
2402
+ size={18}
2403
+ color={themeStyles.textColor}
2404
+ />
2405
+ <Ionicons
2406
+ name={
2407
+ sortBy === 'date' ? 'calendar' :
2408
+ sortBy === 'size' ? 'resize' :
2409
+ sortBy === 'name' ? 'text' : 'document'
2410
+ }
2411
+ size={16}
2412
+ color={themeStyles.textColor}
2413
+ style={{ marginLeft: 4 }}
2414
+ />
2415
+ </TouchableOpacity>
1837
2416
  {user?.id === targetUserId && (!selectMode || (selectMode && allowUploadInSelectMode)) && (
1838
2417
  <TouchableOpacity
1839
2418
  style={[styles.uploadButton, { backgroundColor: themeStyles.primaryColor }]}
@@ -1923,6 +2502,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1923
2502
  renderPhotoGrid()
1924
2503
  ) : (
1925
2504
  <ScrollView
2505
+ ref={scrollViewRef}
1926
2506
  style={styles.scrollView}
1927
2507
  contentContainerStyle={styles.scrollContainer}
1928
2508
  refreshControl={
@@ -1990,16 +2570,21 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1990
2570
  {/* Selection bar removed; actions are now in header */}
1991
2571
  {/* Global loadingMore bar removed; now inline in scroll areas */}
1992
2572
 
1993
- {/* Drag and Drop Overlay */}
2573
+ {/* Drag and Drop Overlay - Enhanced */}
1994
2574
  {isDragging && Platform.OS === 'web' && (
1995
2575
  <View style={styles.dragDropOverlay}>
1996
- <View style={styles.dragDropContent}>
1997
- <Ionicons name="cloud-upload" size={64} color={themeStyles.primaryColor} />
2576
+ <View style={[styles.dragDropContent, {
2577
+ backgroundColor: themeStyles.isDarkTheme ? 'rgba(30, 30, 30, 0.98)' : 'rgba(255, 255, 255, 0.98)',
2578
+ borderColor: themeStyles.primaryColor,
2579
+ }]}>
2580
+ <View style={[styles.dragDropIconContainer, { backgroundColor: `${themeStyles.primaryColor}15` }]}>
2581
+ <Ionicons name="cloud-upload" size={64} color={themeStyles.primaryColor} />
2582
+ </View>
1998
2583
  <Text style={[styles.dragDropTitle, { color: themeStyles.primaryColor }]}>
1999
2584
  Drop files to upload
2000
2585
  </Text>
2001
2586
  <Text style={[styles.dragDropSubtitle, { color: themeStyles.isDarkTheme ? '#BBBBBB' : '#666666' }]}>
2002
- Release to upload{uploadProgress ? ` (${uploadProgress.current}/${uploadProgress.total})` : ' multiple files'}
2587
+ Release to upload multiple files
2003
2588
  </Text>
2004
2589
  </View>
2005
2590
  </View>
@@ -2499,36 +3084,151 @@ const styles = StyleSheet.create({
2499
3084
  fontWeight: '600',
2500
3085
  },
2501
3086
 
2502
- // Drag and Drop styles
3087
+ // Drag and Drop styles - Enhanced
2503
3088
  dragDropOverlay: {
2504
3089
  position: 'absolute',
2505
3090
  top: 0,
2506
3091
  left: 0,
2507
3092
  right: 0,
2508
3093
  bottom: 0,
2509
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
3094
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
2510
3095
  justifyContent: 'center',
2511
3096
  alignItems: 'center',
2512
3097
  zIndex: 1000,
2513
3098
  },
2514
3099
  dragDropContent: {
2515
3100
  alignItems: 'center',
2516
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
2517
- padding: 20,
2518
- borderRadius: 14,
2519
- borderWidth: 1,
2520
- borderColor: '#66AFFF',
3101
+ padding: 32,
3102
+ borderRadius: 20,
3103
+ borderWidth: 3,
2521
3104
  borderStyle: 'dashed',
3105
+ minWidth: 280,
3106
+ shadowColor: '#000',
3107
+ shadowOpacity: 0.3,
3108
+ shadowRadius: 20,
3109
+ shadowOffset: { width: 0, height: 10 },
3110
+ elevation: 10,
3111
+ },
3112
+ dragDropIconContainer: {
3113
+ padding: 24,
3114
+ borderRadius: 50,
3115
+ marginBottom: 16,
2522
3116
  },
2523
3117
  dragDropTitle: {
2524
- fontSize: 20,
2525
- fontWeight: 'bold',
2526
- marginTop: 12,
2527
- marginBottom: 6,
3118
+ fontSize: 24,
3119
+ fontWeight: '700',
3120
+ fontFamily: fontFamilies.phuduBold,
3121
+ marginBottom: 8,
2528
3122
  },
2529
3123
  dragDropSubtitle: {
2530
3124
  fontSize: 16,
2531
- textAlign: 'center',
3125
+ fontWeight: '500',
3126
+ fontFamily: fontFamilies.phuduMedium,
3127
+ },
3128
+ // Upload Preview Modal styles
3129
+ uploadPreviewContainer: {
3130
+ flex: 1,
3131
+ },
3132
+ uploadPreviewHeader: {
3133
+ flexDirection: 'row',
3134
+ alignItems: 'center',
3135
+ justifyContent: 'space-between',
3136
+ paddingHorizontal: 16,
3137
+ paddingVertical: 16,
3138
+ borderBottomWidth: 1,
3139
+ },
3140
+ uploadPreviewTitle: {
3141
+ fontSize: 20,
3142
+ fontWeight: '700',
3143
+ fontFamily: fontFamilies.phuduBold,
3144
+ },
3145
+ uploadPreviewList: {
3146
+ flex: 1,
3147
+ padding: 16,
3148
+ },
3149
+ uploadPreviewItem: {
3150
+ flexDirection: 'row',
3151
+ alignItems: 'center',
3152
+ padding: 12,
3153
+ borderRadius: 12,
3154
+ borderWidth: 1,
3155
+ marginBottom: 12,
3156
+ gap: 12,
3157
+ },
3158
+ uploadPreviewThumbnail: {
3159
+ width: 60,
3160
+ height: 60,
3161
+ borderRadius: 8,
3162
+ },
3163
+ uploadPreviewIconContainer: {
3164
+ width: 60,
3165
+ height: 60,
3166
+ borderRadius: 8,
3167
+ alignItems: 'center',
3168
+ justifyContent: 'center',
3169
+ },
3170
+ uploadPreviewInfo: {
3171
+ flex: 1,
3172
+ minWidth: 0,
3173
+ },
3174
+ uploadPreviewName: {
3175
+ fontSize: 16,
3176
+ fontWeight: '600',
3177
+ fontFamily: fontFamilies.phuduSemiBold,
3178
+ marginBottom: 4,
3179
+ },
3180
+ uploadPreviewMeta: {
3181
+ fontSize: 13,
3182
+ fontFamily: fontFamilies.phudu,
3183
+ },
3184
+ uploadPreviewRemove: {
3185
+ padding: 4,
3186
+ },
3187
+ uploadPreviewFooter: {
3188
+ padding: 16,
3189
+ borderTopWidth: 1,
3190
+ },
3191
+ uploadPreviewStats: {
3192
+ flexDirection: 'row',
3193
+ justifyContent: 'space-between',
3194
+ marginBottom: 16,
3195
+ },
3196
+ uploadPreviewStatsText: {
3197
+ fontSize: 15,
3198
+ fontWeight: '600',
3199
+ fontFamily: fontFamilies.phuduSemiBold,
3200
+ },
3201
+ uploadPreviewActions: {
3202
+ flexDirection: 'row',
3203
+ gap: 12,
3204
+ },
3205
+ uploadPreviewCancelButton: {
3206
+ flex: 1,
3207
+ paddingVertical: 14,
3208
+ borderRadius: 12,
3209
+ borderWidth: 1,
3210
+ alignItems: 'center',
3211
+ justifyContent: 'center',
3212
+ },
3213
+ uploadPreviewCancelText: {
3214
+ fontSize: 16,
3215
+ fontWeight: '600',
3216
+ fontFamily: fontFamilies.phuduSemiBold,
3217
+ },
3218
+ uploadPreviewConfirmButton: {
3219
+ flex: 2,
3220
+ flexDirection: 'row',
3221
+ alignItems: 'center',
3222
+ justifyContent: 'center',
3223
+ paddingVertical: 14,
3224
+ borderRadius: 12,
3225
+ gap: 8,
3226
+ },
3227
+ uploadPreviewConfirmText: {
3228
+ color: '#FFFFFF',
3229
+ fontSize: 16,
3230
+ fontWeight: '600',
3231
+ fontFamily: fontFamilies.phuduSemiBold,
2532
3232
  },
2533
3233
 
2534
3234
  // File Viewer styles
@@ -2705,6 +3405,10 @@ const styles = StyleSheet.create({
2705
3405
  paddingBottom: 4,
2706
3406
  gap: 12,
2707
3407
  },
3408
+ viewModeScroll: {
3409
+ flex: 1,
3410
+ maxWidth: '80%',
3411
+ },
2708
3412
  viewModeToggle: {
2709
3413
  flexDirection: 'row',
2710
3414
  borderRadius: 24,
@@ -2720,6 +3424,16 @@ const styles = StyleSheet.create({
2720
3424
  justifyContent: 'center',
2721
3425
  marginHorizontal: 1,
2722
3426
  },
3427
+ sortButton: {
3428
+ flexDirection: 'row',
3429
+ alignItems: 'center',
3430
+ justifyContent: 'center',
3431
+ paddingHorizontal: 12,
3432
+ paddingVertical: 10,
3433
+ borderRadius: 20,
3434
+ borderWidth: 1,
3435
+ minWidth: 44,
3436
+ },
2723
3437
 
2724
3438
  // Photo Grid styles
2725
3439
  photoScrollContainer: {
@@ -2826,6 +3540,18 @@ const styles = StyleSheet.create({
2826
3540
  borderRadius: 8,
2827
3541
  marginBottom: 4,
2828
3542
  },
3543
+ skeletonFileItem: {
3544
+ flexDirection: 'row',
3545
+ alignItems: 'center',
3546
+ paddingHorizontal: 16,
3547
+ paddingVertical: 12,
3548
+ borderBottomWidth: 1,
3549
+ gap: 12,
3550
+ },
3551
+ skeletonFileInfo: {
3552
+ flex: 1,
3553
+ justifyContent: 'center',
3554
+ },
2829
3555
  });
2830
3556
 
2831
3557
  export default FileManagementScreen;