@oxyhq/services 5.13.4 → 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 (170) 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 +83 -30
  4. package/lib/commonjs/core/OxyServices.js.map +1 -1
  5. package/lib/commonjs/core/index.js +0 -7
  6. package/lib/commonjs/core/index.js.map +1 -1
  7. package/lib/commonjs/i18n/locales/en-US.json +222 -6
  8. package/lib/commonjs/index.js +0 -7
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/lib/sonner.js.map +1 -1
  11. package/lib/commonjs/ui/components/GroupedItem.js +24 -22
  12. package/lib/commonjs/ui/components/GroupedItem.js.map +1 -1
  13. package/lib/commonjs/ui/components/OxyProvider.js +35 -14
  14. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  15. package/lib/commonjs/ui/navigation/routes.js +36 -1
  16. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  17. package/lib/commonjs/ui/screens/AccountOverviewScreen.js +150 -5
  18. package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
  19. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +475 -319
  20. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  21. package/lib/commonjs/ui/screens/AccountVerificationScreen.js +217 -0
  22. package/lib/commonjs/ui/screens/AccountVerificationScreen.js.map +1 -0
  23. package/lib/commonjs/ui/screens/FileManagementScreen.js +911 -213
  24. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  25. package/lib/commonjs/ui/screens/HelpSupportScreen.js +131 -0
  26. package/lib/commonjs/ui/screens/HelpSupportScreen.js.map +1 -0
  27. package/lib/commonjs/ui/screens/HistoryViewScreen.js +258 -0
  28. package/lib/commonjs/ui/screens/HistoryViewScreen.js.map +1 -0
  29. package/lib/commonjs/ui/screens/LegalDocumentsScreen.js +211 -0
  30. package/lib/commonjs/ui/screens/LegalDocumentsScreen.js.map +1 -0
  31. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js +0 -1
  32. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  33. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +307 -0
  34. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -0
  35. package/lib/commonjs/ui/screens/ProfileScreen.js +1 -7
  36. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  37. package/lib/commonjs/ui/screens/SavesCollectionsScreen.js +205 -0
  38. package/lib/commonjs/ui/screens/SavesCollectionsScreen.js.map +1 -0
  39. package/lib/commonjs/ui/screens/SearchSettingsScreen.js +239 -0
  40. package/lib/commonjs/ui/screens/SearchSettingsScreen.js.map +1 -0
  41. package/lib/commonjs/ui/screens/SignInScreen.js +14 -29
  42. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  43. package/lib/commonjs/utils/asyncUtils.js +1 -0
  44. package/lib/commonjs/utils/asyncUtils.js.map +1 -1
  45. package/lib/commonjs/utils/cache.js +4 -4
  46. package/lib/commonjs/utils/cache.js.map +1 -1
  47. package/lib/commonjs/utils/index.js +0 -6
  48. package/lib/commonjs/utils/index.js.map +1 -1
  49. package/lib/module/core/HttpClient.js +1 -1
  50. package/lib/module/core/HttpClient.js.map +1 -1
  51. package/lib/module/core/OxyServices.js +82 -29
  52. package/lib/module/core/OxyServices.js.map +1 -1
  53. package/lib/module/core/index.js +1 -1
  54. package/lib/module/core/index.js.map +1 -1
  55. package/lib/module/i18n/locales/en-US.json +222 -6
  56. package/lib/module/index.js +1 -1
  57. package/lib/module/index.js.map +1 -1
  58. package/lib/module/lib/sonner.js.map +1 -1
  59. package/lib/module/ui/components/GroupedItem.js +24 -22
  60. package/lib/module/ui/components/GroupedItem.js.map +1 -1
  61. package/lib/module/ui/components/OxyProvider.js +40 -17
  62. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  63. package/lib/module/ui/navigation/routes.js +36 -1
  64. package/lib/module/ui/navigation/routes.js.map +1 -1
  65. package/lib/module/ui/screens/AccountOverviewScreen.js +151 -6
  66. package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
  67. package/lib/module/ui/screens/AccountSettingsScreen.js +475 -319
  68. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  69. package/lib/module/ui/screens/AccountVerificationScreen.js +212 -0
  70. package/lib/module/ui/screens/AccountVerificationScreen.js.map +1 -0
  71. package/lib/module/ui/screens/FileManagementScreen.js +913 -212
  72. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  73. package/lib/module/ui/screens/HelpSupportScreen.js +126 -0
  74. package/lib/module/ui/screens/HelpSupportScreen.js.map +1 -0
  75. package/lib/module/ui/screens/HistoryViewScreen.js +253 -0
  76. package/lib/module/ui/screens/HistoryViewScreen.js.map +1 -0
  77. package/lib/module/ui/screens/LegalDocumentsScreen.js +206 -0
  78. package/lib/module/ui/screens/LegalDocumentsScreen.js.map +1 -0
  79. package/lib/module/ui/screens/PremiumSubscriptionScreen.js +0 -1
  80. package/lib/module/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  81. package/lib/module/ui/screens/PrivacySettingsScreen.js +302 -0
  82. package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -0
  83. package/lib/module/ui/screens/ProfileScreen.js +1 -7
  84. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  85. package/lib/module/ui/screens/SavesCollectionsScreen.js +200 -0
  86. package/lib/module/ui/screens/SavesCollectionsScreen.js.map +1 -0
  87. package/lib/module/ui/screens/SearchSettingsScreen.js +234 -0
  88. package/lib/module/ui/screens/SearchSettingsScreen.js.map +1 -0
  89. package/lib/module/ui/screens/SignInScreen.js +14 -29
  90. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  91. package/lib/module/utils/asyncUtils.js +1 -0
  92. package/lib/module/utils/asyncUtils.js.map +1 -1
  93. package/lib/module/utils/cache.js +3 -3
  94. package/lib/module/utils/cache.js.map +1 -1
  95. package/lib/module/utils/index.js +1 -1
  96. package/lib/module/utils/index.js.map +1 -1
  97. package/lib/typescript/core/OxyServices.d.ts +30 -24
  98. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  99. package/lib/typescript/core/index.d.ts +1 -1
  100. package/lib/typescript/core/index.d.ts.map +1 -1
  101. package/lib/typescript/index.d.ts +1 -1
  102. package/lib/typescript/index.d.ts.map +1 -1
  103. package/lib/typescript/lib/sonner.d.ts +1 -0
  104. package/lib/typescript/lib/sonner.d.ts.map +1 -1
  105. package/lib/typescript/types/expo-document-picker.d.ts +36 -0
  106. package/lib/typescript/ui/components/GroupedItem.d.ts.map +1 -1
  107. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  108. package/lib/typescript/ui/navigation/routes.d.ts +1 -1
  109. package/lib/typescript/ui/navigation/routes.d.ts.map +1 -1
  110. package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
  111. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  112. package/lib/typescript/ui/screens/AccountVerificationScreen.d.ts +5 -0
  113. package/lib/typescript/ui/screens/AccountVerificationScreen.d.ts.map +1 -0
  114. package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
  115. package/lib/typescript/ui/screens/HelpSupportScreen.d.ts +5 -0
  116. package/lib/typescript/ui/screens/HelpSupportScreen.d.ts.map +1 -0
  117. package/lib/typescript/ui/screens/HistoryViewScreen.d.ts +5 -0
  118. package/lib/typescript/ui/screens/HistoryViewScreen.d.ts.map +1 -0
  119. package/lib/typescript/ui/screens/LegalDocumentsScreen.d.ts +5 -0
  120. package/lib/typescript/ui/screens/LegalDocumentsScreen.d.ts.map +1 -0
  121. package/lib/typescript/ui/screens/PremiumSubscriptionScreen.d.ts.map +1 -1
  122. package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts +5 -0
  123. package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -0
  124. package/lib/typescript/ui/screens/ProfileScreen.d.ts.map +1 -1
  125. package/lib/typescript/ui/screens/SavesCollectionsScreen.d.ts +5 -0
  126. package/lib/typescript/ui/screens/SavesCollectionsScreen.d.ts.map +1 -0
  127. package/lib/typescript/ui/screens/SearchSettingsScreen.d.ts +5 -0
  128. package/lib/typescript/ui/screens/SearchSettingsScreen.d.ts.map +1 -0
  129. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  130. package/lib/typescript/utils/asyncUtils.d.ts.map +1 -1
  131. package/lib/typescript/utils/cache.d.ts +3 -3
  132. package/lib/typescript/utils/cache.d.ts.map +1 -1
  133. package/lib/typescript/utils/index.d.ts +1 -1
  134. package/lib/typescript/utils/index.d.ts.map +1 -1
  135. package/package.json +1 -1
  136. package/src/core/HttpClient.ts +1 -1
  137. package/src/core/OxyServices.ts +80 -30
  138. package/src/core/index.ts +1 -1
  139. package/src/i18n/locales/en-US.json +222 -6
  140. package/src/index.ts +1 -1
  141. package/src/lib/sonner.ts +1 -0
  142. package/src/types/expo-document-picker.d.ts +36 -0
  143. package/src/ui/components/GroupedItem.tsx +23 -21
  144. package/src/ui/components/OxyProvider.tsx +33 -11
  145. package/src/ui/navigation/routes.ts +42 -0
  146. package/src/ui/screens/AccountOverviewScreen.tsx +175 -5
  147. package/src/ui/screens/AccountSettingsScreen.tsx +521 -360
  148. package/src/ui/screens/AccountVerificationScreen.tsx +235 -0
  149. package/src/ui/screens/FileManagementScreen.tsx +934 -208
  150. package/src/ui/screens/HelpSupportScreen.tsx +143 -0
  151. package/src/ui/screens/HistoryViewScreen.tsx +280 -0
  152. package/src/ui/screens/LegalDocumentsScreen.tsx +220 -0
  153. package/src/ui/screens/PremiumSubscriptionScreen.tsx +0 -1
  154. package/src/ui/screens/PrivacySettingsScreen.tsx +332 -0
  155. package/src/ui/screens/ProfileScreen.tsx +1 -8
  156. package/src/ui/screens/SavesCollectionsScreen.tsx +222 -0
  157. package/src/ui/screens/SearchSettingsScreen.tsx +219 -0
  158. package/src/ui/screens/SignInScreen.tsx +19 -35
  159. package/src/utils/asyncUtils.ts +1 -0
  160. package/src/utils/cache.ts +3 -3
  161. package/src/utils/index.ts +1 -1
  162. package/lib/commonjs/ui/components/StepBasedScreen.README.md +0 -337
  163. package/lib/commonjs/ui/components/internal/TextField.md +0 -436
  164. package/lib/commonjs/ui/styles/FONTS.md +0 -126
  165. package/lib/module/ui/components/StepBasedScreen.README.md +0 -337
  166. package/lib/module/ui/components/internal/TextField.md +0 -436
  167. package/lib/module/ui/styles/FONTS.md +0 -126
  168. package/src/ui/components/StepBasedScreen.README.md +0 -337
  169. package/src/ui/components/internal/TextField.md +0 -436
  170. package/src/ui/styles/FONTS.md +0 -126
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
4
- import { View, Text, TouchableOpacity, StyleSheet, ScrollView, ActivityIndicator, Platform, RefreshControl, Modal, TextInput, Image // kept for Image.getSize only
5
- } from 'react-native';
4
+ import { View, Text, TouchableOpacity, StyleSheet, ScrollView, ActivityIndicator, Platform, RefreshControl, Modal, TextInput, Image,
5
+ // kept for Image.getSize only
6
+ Animated, Easing, Alert } from 'react-native';
6
7
  import { Image as ExpoImage } from 'expo-image';
7
8
  import { useOxy } from '../context/OxyContext';
8
9
  import { fontFamilies } from '../styles/fonts';
@@ -43,18 +44,6 @@ const FileManagementScreen = ({
43
44
  user,
44
45
  oxyServices
45
46
  } = useOxy();
46
-
47
- // Debug: log the actual container width
48
- useEffect(() => {
49
- console.log('[FileManagementScreen] Container width (full):', containerWidth);
50
- // Padding structure:
51
- // - containerWidth = full bottom sheet container width (measured from OxyProvider)
52
- // - photoScrollContainer adds padding: 16 (32px total horizontal padding)
53
- // - Available content width = containerWidth - 32
54
- const availableContentWidth = containerWidth - 32;
55
- console.log('[FileManagementScreen] Available content width:', availableContentWidth);
56
- console.log('[FileManagementScreen] Spacing fix applied: 4px uniform gap both horizontal and vertical');
57
- }, [containerWidth]);
58
47
  const files = useFiles();
59
48
  const uploading = useUploadingStore();
60
49
  const uploadProgress = useUploadAggregateProgress();
@@ -77,18 +66,46 @@ const FileManagementScreen = ({
77
66
  const [showFileDetailsInViewer, setShowFileDetailsInViewer] = useState(false);
78
67
  const [viewMode, setViewMode] = useState('all');
79
68
  const [searchQuery, setSearchQuery] = useState('');
80
- // Derived filtered files (avoid setState loops)
69
+ const [sortBy, setSortBy] = useState('date');
70
+ const [sortOrder, setSortOrder] = useState('desc');
71
+ const [pendingFiles, setPendingFiles] = useState([]);
72
+ const [showUploadPreview, setShowUploadPreview] = useState(false);
73
+ // Derived filtered and sorted files (avoid setState loops)
81
74
  const filteredFiles = useMemo(() => {
82
75
  let filteredByMode = files;
83
76
  if (viewMode === 'photos') {
84
77
  filteredByMode = files.filter(file => file.contentType.startsWith('image/'));
78
+ } else if (viewMode === 'videos') {
79
+ filteredByMode = files.filter(file => file.contentType.startsWith('video/'));
80
+ } else if (viewMode === 'documents') {
81
+ filteredByMode = files.filter(file => file.contentType.includes('pdf') || file.contentType.includes('document') || file.contentType.includes('text') || file.contentType.includes('msword') || file.contentType.includes('excel') || file.contentType.includes('spreadsheet') || file.contentType.includes('presentation') || file.contentType.includes('powerpoint'));
82
+ } else if (viewMode === 'audio') {
83
+ filteredByMode = files.filter(file => file.contentType.startsWith('audio/'));
85
84
  }
86
- if (!searchQuery.trim()) {
87
- return filteredByMode;
85
+ let filtered = filteredByMode;
86
+ if (searchQuery.trim()) {
87
+ const query = searchQuery.toLowerCase();
88
+ filtered = filteredByMode.filter(file => file.filename.toLowerCase().includes(query) || file.contentType.toLowerCase().includes(query) || file.metadata?.description && file.metadata.description.toLowerCase().includes(query));
88
89
  }
89
- const query = searchQuery.toLowerCase();
90
- return filteredByMode.filter(file => file.filename.toLowerCase().includes(query) || file.contentType.toLowerCase().includes(query) || file.metadata?.description && file.metadata.description.toLowerCase().includes(query));
91
- }, [files, searchQuery, viewMode]);
90
+
91
+ // Sort files
92
+ const sorted = [...filtered].sort((a, b) => {
93
+ let comparison = 0;
94
+ if (sortBy === 'date') {
95
+ const dateA = new Date(a.uploadDate || 0).getTime();
96
+ const dateB = new Date(b.uploadDate || 0).getTime();
97
+ comparison = dateA - dateB;
98
+ } else if (sortBy === 'size') {
99
+ comparison = (a.length || 0) - (b.length || 0);
100
+ } else if (sortBy === 'name') {
101
+ comparison = (a.filename || '').localeCompare(b.filename || '');
102
+ } else if (sortBy === 'type') {
103
+ comparison = (a.contentType || '').localeCompare(b.contentType || '');
104
+ }
105
+ return sortOrder === 'asc' ? comparison : -comparison;
106
+ });
107
+ return sorted;
108
+ }, [files, searchQuery, viewMode, sortBy, sortOrder]);
92
109
  const [isDragging, setIsDragging] = useState(false);
93
110
  const [photoDimensions, setPhotoDimensions] = useState({});
94
111
  const [loadingDimensions, setLoadingDimensions] = useState(false);
@@ -97,13 +114,19 @@ const FileManagementScreen = ({
97
114
  const MIN_BANNER_MS = 600;
98
115
  // Selection state
99
116
  const [selectedIds, setSelectedIds] = useState(new Set(initialSelectedIds));
117
+ const [lastSelectedFileId, setLastSelectedFileId] = useState(null);
118
+ const scrollViewRef = useRef(null);
119
+ const photoScrollViewRef = useRef(null);
120
+ const itemRefs = useRef(new Map()); // Track item positions
121
+ const containerRef = useRef(null); // Ref for drag and drop container
100
122
  useEffect(() => {
101
123
  if (initialSelectedIds && initialSelectedIds.length) {
102
124
  setSelectedIds(new Set(initialSelectedIds));
103
125
  }
104
126
  }, [initialSelectedIds]);
105
127
  const toggleSelect = useCallback(async file => {
106
- if (!selectMode) return;
128
+ // Allow selection in regular mode for bulk operations
129
+ // if (!selectMode) return;
107
130
  if (disabledMimeTypes.length) {
108
131
  const blocked = disabledMimeTypes.some(mt => file.contentType === mt || file.contentType.startsWith(mt.endsWith('/') ? mt : mt + '/'));
109
132
  if (blocked) {
@@ -117,20 +140,19 @@ const FileManagementScreen = ({
117
140
  if (fileVisibility !== defaultVisibility) {
118
141
  try {
119
142
  await oxyServices.assetUpdateVisibility(file.id, defaultVisibility);
120
- console.log(`Updated file ${file.id} visibility from ${fileVisibility} to ${defaultVisibility}`);
121
143
  } catch (error) {
122
- console.error('Failed to update file visibility:', error);
123
144
  // Continue anyway - selection shouldn't fail if visibility update fails
124
145
  }
125
146
  }
126
147
 
148
+ // Track the selected file for scrolling
149
+ setLastSelectedFileId(file.id);
150
+
127
151
  // Link file to entity if linkContext is provided
128
152
  if (linkContext) {
129
153
  try {
130
154
  await oxyServices.assetLink(file.id, linkContext.app, linkContext.entityType, linkContext.entityId, defaultVisibility, linkContext.webhookUrl);
131
- console.log(`Linked file ${file.id} to ${linkContext.app}/${linkContext.entityType}/${linkContext.entityId}`);
132
155
  } catch (error) {
133
- console.error('Failed to link file:', error);
134
156
  // Continue anyway - selection shouldn't fail if linking fails
135
157
  }
136
158
  }
@@ -173,9 +195,8 @@ const FileManagementScreen = ({
173
195
  if (fileVisibility !== defaultVisibility) {
174
196
  try {
175
197
  await oxyServices.assetUpdateVisibility(file.id, defaultVisibility);
176
- console.log(`Updated file ${file.id} visibility from ${fileVisibility} to ${defaultVisibility}`);
177
198
  } catch (error) {
178
- console.error(`Failed to update visibility for ${file.id}:`, error);
199
+ // Visibility update failed, continue with selection
179
200
  }
180
201
  }
181
202
 
@@ -183,9 +204,8 @@ const FileManagementScreen = ({
183
204
  if (linkContext) {
184
205
  try {
185
206
  await oxyServices.assetLink(file.id, linkContext.app, linkContext.entityType, linkContext.entityId, defaultVisibility, linkContext.webhookUrl);
186
- console.log(`Linked file ${file.id} to ${linkContext.app}/${linkContext.entityType}/${linkContext.entityId}`);
187
207
  } catch (error) {
188
- console.error(`Failed to link file ${file.id}:`, error);
208
+ // File linking failed, continue with selection
189
209
  }
190
210
  }
191
211
  });
@@ -317,7 +337,6 @@ const FileManagementScreen = ({
317
337
  }));
318
338
  }
319
339
  } catch (error) {
320
- console.error('Failed to load files:', error);
321
340
  toast.error(error.message || 'Failed to load files');
322
341
  } finally {
323
342
  setLoading(false);
@@ -412,7 +431,7 @@ const FileManagementScreen = ({
412
431
  setPhotoDimensions(newDimensions);
413
432
  }
414
433
  } catch (error) {
415
- console.error('Error loading photo dimensions:', error);
434
+ // Photo dimensions loading failed, continue without dimensions
416
435
  } finally {
417
436
  setLoadingDimensions(false);
418
437
  }
@@ -516,85 +535,170 @@ const FileManagementScreen = ({
516
535
  loadFiles('silent');
517
536
  }, 1200);
518
537
  } catch (error) {
519
- console.error('Upload error:', error);
520
538
  toast.error(error.message || 'Failed to upload files');
521
539
  } finally {
522
540
  storeSetUploadProgress(null);
523
541
  }
524
542
  };
543
+ const handleFileSelection = useCallback(async selectedFiles => {
544
+ const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
545
+ const processedFiles = [];
546
+ for (const file of selectedFiles) {
547
+ // Validate file size
548
+ if (file.size > MAX_FILE_SIZE) {
549
+ toast.error(`"${file.name}" is too large. Maximum file size is ${formatFileSize(MAX_FILE_SIZE)}`);
550
+ continue;
551
+ }
552
+
553
+ // Generate preview for images
554
+ let preview;
555
+ if (file.type.startsWith('image/')) {
556
+ preview = URL.createObjectURL(file);
557
+ }
558
+ processedFiles.push({
559
+ file,
560
+ preview,
561
+ size: file.size,
562
+ name: file.name,
563
+ type: file.type
564
+ });
565
+ }
566
+ if (processedFiles.length === 0) return;
567
+
568
+ // Show preview modal for user to review files before upload
569
+ setPendingFiles(processedFiles);
570
+ setShowUploadPreview(true);
571
+ }, []);
572
+ const handleConfirmUpload = async () => {
573
+ if (pendingFiles.length === 0) return;
574
+ setShowUploadPreview(false);
575
+ uploadStartRef.current = Date.now();
576
+ storeSetUploading(true);
577
+ storeSetUploadProgress(null);
578
+ try {
579
+ const filesToUpload = pendingFiles.map(pf => pf.file);
580
+ storeSetUploadProgress({
581
+ current: 0,
582
+ total: filesToUpload.length
583
+ });
584
+ await processFileUploads(filesToUpload);
585
+
586
+ // Cleanup preview URLs
587
+ pendingFiles.forEach(pf => {
588
+ if (pf.preview) {
589
+ URL.revokeObjectURL(pf.preview);
590
+ }
591
+ });
592
+ setPendingFiles([]);
593
+ endUpload();
594
+ } catch (error) {
595
+ toast.error(error.message || 'Failed to upload files');
596
+ endUpload();
597
+ }
598
+ };
599
+ const handleCancelUpload = () => {
600
+ // Cleanup preview URLs
601
+ pendingFiles.forEach(pf => {
602
+ if (pf.preview) {
603
+ URL.revokeObjectURL(pf.preview);
604
+ }
605
+ });
606
+ setPendingFiles([]);
607
+ setShowUploadPreview(false);
608
+ };
609
+ const removePendingFile = index => {
610
+ const file = pendingFiles[index];
611
+ if (file.preview) {
612
+ URL.revokeObjectURL(file.preview);
613
+ }
614
+ const updated = pendingFiles.filter((_, i) => i !== index);
615
+ setPendingFiles(updated);
616
+ if (updated.length === 0) {
617
+ setShowUploadPreview(false);
618
+ }
619
+ };
525
620
  const handleFileUpload = async () => {
526
621
  try {
527
- uploadStartRef.current = Date.now();
528
- storeSetUploading(true);
529
- storeSetUploadProgress(null);
530
622
  if (Platform.OS === 'web') {
531
- // Web file picker implementation
623
+ // Enhanced web file picker
532
624
  const input = document.createElement('input');
533
625
  input.type = 'file';
534
626
  input.multiple = true;
535
627
  input.accept = '*/*';
536
- // Fallback: if the user cancels the dialog (no onchange fires or 0 files), hide banner
537
628
  const cancellationTimer = setTimeout(() => {
538
629
  const state = useFileStore.getState();
539
630
  if (state.uploading && uploadStartRef.current && !state.uploadProgress) {
540
- // No selection happened; treat as cancel
541
631
  endUpload();
542
632
  }
543
- }, 1500); // allow enough time for user to pick
544
-
633
+ }, 1500);
545
634
  input.onchange = async e => {
546
635
  clearTimeout(cancellationTimer);
547
636
  const selectedFiles = Array.from(e.target.files || []);
548
637
  if (selectedFiles.length === 0) {
549
- // User explicitly canceled (some browsers still fire onchange with empty list)
550
638
  endUpload();
551
639
  return;
552
640
  }
553
- storeSetUploadProgress({
554
- current: 0,
555
- total: selectedFiles.length
556
- });
557
- await processFileUploads(selectedFiles);
558
- endUpload();
641
+ await handleFileSelection(selectedFiles);
559
642
  };
560
643
  input.click();
561
644
  } else {
562
- // Mobile - show info that file picker can be added
563
- const installCommand = 'npm install expo-document-picker';
564
- 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.`;
565
- if (window.confirm(`${message}\n\nWould you like to copy the install command?`)) {
566
- toast.info(`Install: ${installCommand}`);
567
- } else {
568
- toast.info('Mobile file upload requires expo-document-picker');
645
+ // Mobile file picker with expo-document-picker
646
+ try {
647
+ // Dynamically import to avoid breaking if not installed
648
+ const DocumentPicker = await import('expo-document-picker').catch(() => null);
649
+ if (!DocumentPicker) {
650
+ toast.error('File picker not available. Please install expo-document-picker');
651
+ return;
652
+ }
653
+ const result = await DocumentPicker.getDocumentAsync({
654
+ type: '*/*',
655
+ multiple: true,
656
+ copyToCacheDirectory: true
657
+ });
658
+ if (result.canceled) {
659
+ return;
660
+ }
661
+
662
+ // Convert expo document picker results to File-like objects
663
+ const files = [];
664
+ for (const doc of result.assets) {
665
+ if (doc.file) {
666
+ // expo-document-picker provides a File-like object
667
+ files.push(doc.file);
668
+ } else if (doc.uri) {
669
+ // Fallback: fetch and create Blob
670
+ const response = await fetch(doc.uri);
671
+ const blob = await response.blob();
672
+ const file = new File([blob], doc.name || 'file', {
673
+ type: doc.mimeType || 'application/octet-stream'
674
+ });
675
+ files.push(file);
676
+ }
677
+ }
678
+ if (files.length > 0) {
679
+ await handleFileSelection(files);
680
+ }
681
+ } catch (error) {
682
+ if (error.message?.includes('expo-document-picker')) {
683
+ toast.error('File picker not available. Please install expo-document-picker');
684
+ } else {
685
+ toast.error(error.message || 'Failed to select files');
686
+ }
569
687
  }
570
688
  }
571
689
  } catch (error) {
572
- toast.error(error.message || 'Failed to upload file');
573
- } finally {
574
- // IMPORTANT: Do NOT call endUpload here.
575
- // We only want to hide the banner after the actual upload(s) complete.
576
- // The input.onchange handler invokes processFileUploads then calls endUpload().
577
- // Calling endUpload here caused the banner to disappear while files were still uploading.
578
- storeSetUploadProgress(null); // keep clearing any stale progress
690
+ toast.error(error.message || 'Failed to open file picker');
579
691
  }
580
692
  };
581
693
  const handleFileDelete = async (fileId, filename) => {
582
694
  // Use web-compatible confirmation dialog
583
695
  const confirmed = window.confirm(`Are you sure you want to delete "${filename}"? This action cannot be undone.`);
584
696
  if (!confirmed) {
585
- console.log('Delete cancelled by user');
586
697
  return;
587
698
  }
588
699
  try {
589
- console.log('Deleting file:', {
590
- fileId,
591
- filename
592
- });
593
- console.log('Target user ID:', targetUserId);
594
- console.log('Current user ID:', user?.id);
595
700
  storeSetDeleting(fileId);
596
- const result = await oxyServices.deleteFile(fileId);
597
- console.log('Delete result:', result);
701
+ await oxyServices.deleteFile(fileId);
598
702
  toast.success('File deleted successfully');
599
703
 
600
704
  // Reload files after successful deletion
@@ -603,9 +707,6 @@ const FileManagementScreen = ({
603
707
  // Silent background reconcile
604
708
  setTimeout(() => loadFiles('silent'), 800);
605
709
  } catch (error) {
606
- console.error('Delete error:', error);
607
- console.error('Error details:', error.response?.data || error.message);
608
-
609
710
  // Provide specific error messages
610
711
  if (error.message?.includes('File not found') || error.message?.includes('404')) {
611
712
  toast.error('File not found. It may have already been deleted.');
@@ -620,90 +721,151 @@ const FileManagementScreen = ({
620
721
  storeSetDeleting(null);
621
722
  }
622
723
  };
623
-
624
- // Drag and drop handlers for web
625
- const handleDragOver = e => {
626
- if (Platform.OS === 'web' && user?.id === targetUserId) {
627
- e.preventDefault();
628
- setIsDragging(true);
629
- }
630
- };
631
- const handleDragEnter = e => {
632
- if (Platform.OS === 'web' && user?.id === targetUserId) {
633
- e.preventDefault();
634
- setIsDragging(true);
724
+ const handleBulkDelete = useCallback(async () => {
725
+ if (selectedIds.size === 0) return;
726
+ const fileMap = {};
727
+ files.forEach(f => {
728
+ fileMap[f.id] = f;
729
+ });
730
+ const selectedFiles = Array.from(selectedIds).map(id => fileMap[id]).filter(Boolean);
731
+ const confirmed = window.confirm(`Are you sure you want to delete ${selectedFiles.length} file(s)? This action cannot be undone.`);
732
+ if (!confirmed) return;
733
+ try {
734
+ const deletePromises = Array.from(selectedIds).map(async fileId => {
735
+ try {
736
+ await oxyServices.deleteFile(fileId);
737
+ useFileStore.getState().removeFile(fileId);
738
+ return {
739
+ success: true,
740
+ fileId
741
+ };
742
+ } catch (error) {
743
+ return {
744
+ success: false,
745
+ fileId,
746
+ error
747
+ };
748
+ }
749
+ });
750
+ const results = await Promise.allSettled(deletePromises);
751
+ const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
752
+ const failed = results.length - successful;
753
+ if (successful > 0) {
754
+ toast.success(`${successful} file(s) deleted successfully`);
755
+ }
756
+ if (failed > 0) {
757
+ toast.error(`${failed} file(s) failed to delete`);
758
+ }
759
+ setSelectedIds(new Set());
760
+ setTimeout(() => loadFiles('silent'), 800);
761
+ } catch (error) {
762
+ toast.error(error.message || 'Failed to delete files');
635
763
  }
636
- };
637
- const handleDragLeave = e => {
638
- if (Platform.OS === 'web') {
639
- e.preventDefault();
640
- setIsDragging(false);
764
+ }, [selectedIds, files, oxyServices, loadFiles]);
765
+ const handleBulkVisibilityChange = useCallback(async visibility => {
766
+ if (selectedIds.size === 0) return;
767
+ try {
768
+ const updatePromises = Array.from(selectedIds).map(async fileId => {
769
+ try {
770
+ await oxyServices.assetUpdateVisibility(fileId, visibility);
771
+ return {
772
+ success: true,
773
+ fileId
774
+ };
775
+ } catch (error) {
776
+ return {
777
+ success: false,
778
+ fileId,
779
+ error
780
+ };
781
+ }
782
+ });
783
+ const results = await Promise.allSettled(updatePromises);
784
+ const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
785
+ const failed = results.length - successful;
786
+ if (successful > 0) {
787
+ toast.success(`${successful} file(s) visibility updated to ${visibility}`);
788
+ // Update file metadata in store
789
+ Array.from(selectedIds).forEach(fileId => {
790
+ useFileStore.getState().updateFile(fileId, {
791
+ metadata: {
792
+ ...files.find(f => f.id === fileId)?.metadata,
793
+ visibility
794
+ }
795
+ });
796
+ });
797
+ }
798
+ if (failed > 0) {
799
+ toast.error(`${failed} file(s) failed to update visibility`);
800
+ }
801
+ setTimeout(() => loadFiles('silent'), 800);
802
+ } catch (error) {
803
+ toast.error(error.message || 'Failed to update visibility');
641
804
  }
642
- };
805
+ }, [selectedIds, oxyServices, files, loadFiles]);
643
806
 
644
- // Global drag listeners (web) to catch drags outside component bounds
807
+ // Global drag listeners (web) - attach to document for reliable drag and drop
645
808
  useEffect(() => {
646
809
  if (Platform.OS !== 'web' || user?.id !== targetUserId) return;
647
- const onDocDragEnter = e => {
648
- if (e?.dataTransfer?.types?.includes('Files')) setIsDragging(true);
810
+ let dragCounter = 0; // Track drag enter/leave to handle nested elements
811
+
812
+ const onDragEnter = e => {
813
+ dragCounter++;
814
+ if (e?.dataTransfer?.types?.includes('Files')) {
815
+ e.preventDefault();
816
+ e.stopPropagation();
817
+ setIsDragging(true);
818
+ }
649
819
  };
650
- const onDocDragOver = e => {
820
+ const onDragOver = e => {
651
821
  if (e?.dataTransfer?.types?.includes('Files')) {
652
822
  e.preventDefault();
823
+ e.stopPropagation();
824
+ // Keep dragging state true while over document
653
825
  setIsDragging(true);
654
826
  }
655
827
  };
656
- const onDocDrop = e => {
828
+ const onDrop = async e => {
829
+ dragCounter = 0;
830
+ setIsDragging(false);
657
831
  if (e?.dataTransfer?.files?.length) {
658
832
  e.preventDefault();
659
- setIsDragging(false);
833
+ e.stopPropagation();
834
+ try {
835
+ const files = Array.from(e.dataTransfer.files);
836
+ if (files.length > 0) {
837
+ await handleFileSelection(files);
838
+ }
839
+ } catch (error) {
840
+ toast.error(error.message || 'Failed to upload files');
841
+ }
660
842
  }
661
843
  };
662
- const onDocDragLeave = e => {
663
- if (!e.relatedTarget && e.screenX === 0 && e.screenY === 0) setIsDragging(false);
844
+ const onDragLeave = e => {
845
+ dragCounter--;
846
+ // Only hide drag overlay if we're actually leaving the document (drag counter reaches 0)
847
+ if (dragCounter === 0) {
848
+ setIsDragging(false);
849
+ }
664
850
  };
665
- document.addEventListener('dragenter', onDocDragEnter);
666
- document.addEventListener('dragover', onDocDragOver);
667
- document.addEventListener('drop', onDocDrop);
668
- document.addEventListener('dragleave', onDocDragLeave);
851
+
852
+ // Attach to document for global drag detection
853
+ document.addEventListener('dragenter', onDragEnter, false);
854
+ document.addEventListener('dragover', onDragOver, false);
855
+ document.addEventListener('drop', onDrop, false);
856
+ document.addEventListener('dragleave', onDragLeave, false);
669
857
  return () => {
670
- document.removeEventListener('dragenter', onDocDragEnter);
671
- document.removeEventListener('dragover', onDocDragOver);
672
- document.removeEventListener('drop', onDocDrop);
673
- document.removeEventListener('dragleave', onDocDragLeave);
858
+ document.removeEventListener('dragenter', onDragEnter, false);
859
+ document.removeEventListener('dragover', onDragOver, false);
860
+ document.removeEventListener('drop', onDrop, false);
861
+ document.removeEventListener('dragleave', onDragLeave, false);
674
862
  };
675
- }, [user?.id, targetUserId]);
676
- const handleDrop = async e => {
677
- if (Platform.OS === 'web' && user?.id === targetUserId) {
678
- e.preventDefault();
679
- setIsDragging(false);
680
- uploadStartRef.current = Date.now();
681
- storeSetUploading(true);
682
- try {
683
- const files = Array.from(e.dataTransfer.files);
684
- if (files.length > 0) storeSetUploadProgress({
685
- current: 0,
686
- total: files.length
687
- });
688
- await processFileUploads(files);
689
- } catch (error) {
690
- toast.error(error.message || 'Failed to upload files');
691
- } finally {
692
- endUpload();
693
- }
694
- }
695
- };
863
+ }, [user?.id, targetUserId, handleFileSelection]);
696
864
  const handleFileDownload = async (fileId, filename) => {
697
865
  try {
698
866
  if (Platform.OS === 'web') {
699
- console.log('Downloading file:', {
700
- fileId,
701
- filename
702
- });
703
-
704
867
  // Use the public download URL method
705
868
  const downloadUrl = oxyServices.getFileDownloadUrl(fileId);
706
- console.log('Download URL:', downloadUrl);
707
869
  try {
708
870
  // Method 1: Try simple link download first
709
871
  const link = document.createElement('a');
@@ -715,8 +877,6 @@ const FileManagementScreen = ({
715
877
  document.body.removeChild(link);
716
878
  toast.success('File download started');
717
879
  } catch (linkError) {
718
- console.warn('Link download failed, trying fetch method:', linkError);
719
-
720
880
  // Method 2: Fallback to authenticated download
721
881
  const blob = await oxyServices.getFileContentAsBlob(fileId);
722
882
  const url = window.URL.createObjectURL(blob);
@@ -735,7 +895,6 @@ const FileManagementScreen = ({
735
895
  toast.info('File download not implemented for mobile yet');
736
896
  }
737
897
  } catch (error) {
738
- console.error('Download error:', error);
739
898
  toast.error(error.message || 'Failed to download file');
740
899
  }
741
900
  };
@@ -778,7 +937,6 @@ const FileManagementScreen = ({
778
937
  setFileContent(content);
779
938
  }
780
939
  } catch (error) {
781
- console.error('Failed to load file content:', error);
782
940
  if (error.message?.includes('404') || error.message?.includes('not found')) {
783
941
  toast.error('File not found. It may have been deleted.');
784
942
  } else {
@@ -791,7 +949,6 @@ const FileManagementScreen = ({
791
949
  setFileContent(null);
792
950
  }
793
951
  } catch (error) {
794
- console.error('Failed to open file:', error);
795
952
  toast.error(error.message || 'Failed to open file');
796
953
  } finally {
797
954
  setLoadingFileContent(false);
@@ -842,8 +999,8 @@ const FileManagementScreen = ({
842
999
  contentFit: "cover",
843
1000
  transition: 120,
844
1001
  cachePolicy: "memory-disk",
845
- onError: e => {
846
- console.error('Photo failed to load:', e?.nativeEvent ?? e);
1002
+ onError: () => {
1003
+ // Photo failed to load, will show placeholder
847
1004
  },
848
1005
  accessibilityLabel: photo.filename
849
1006
  }), selectMode && /*#__PURE__*/_jsx(View, {
@@ -883,8 +1040,8 @@ const FileManagementScreen = ({
883
1040
  contentFit: "cover",
884
1041
  transition: 120,
885
1042
  cachePolicy: "memory-disk",
886
- onError: e => {
887
- console.error('Photo failed to load:', e?.nativeEvent ?? e);
1043
+ onError: () => {
1044
+ // Photo failed to load, will show placeholder
888
1045
  },
889
1046
  accessibilityLabel: photo.filename
890
1047
  }), selectMode && /*#__PURE__*/_jsx(View, {
@@ -943,8 +1100,8 @@ const FileManagementScreen = ({
943
1100
  contentFit: "cover",
944
1101
  transition: 120,
945
1102
  cachePolicy: "memory-disk",
946
- onError: _ => {
947
- console.warn('Failed to load image preview.');
1103
+ onError: () => {
1104
+ // Image preview failed to load
948
1105
  },
949
1106
  accessibilityLabel: file.filename
950
1107
  }), isPDF && /*#__PURE__*/_jsxs(View, {
@@ -1082,7 +1239,14 @@ const FileManagementScreen = ({
1082
1239
 
1083
1240
  // GroupedSection-based file items (for 'all' view) replacing legacy flat list look
1084
1241
  const groupedFileItems = useMemo(() => {
1085
- return filteredFiles.filter(f => true).sort((a, b) => new Date(b.uploadDate).getTime() - new Date(a.uploadDate).getTime()).map(file => {
1242
+ // filteredFiles is already sorted, so just use it directly
1243
+ const sortedFiles = filteredFiles;
1244
+
1245
+ // Store file positions for scrolling
1246
+ sortedFiles.forEach((file, index) => {
1247
+ itemRefs.current.set(file.id, index);
1248
+ });
1249
+ return sortedFiles.map(file => {
1086
1250
  const isImage = file.contentType.startsWith('image/');
1087
1251
  const isVideo = file.contentType.startsWith('video/');
1088
1252
  const hasPreview = isImage || isVideo;
@@ -1097,13 +1261,29 @@ const FileManagementScreen = ({
1097
1261
  title: file.filename,
1098
1262
  subtitle: `${formatFileSize(file.length)} • ${new Date(file.uploadDate).toLocaleDateString()}`,
1099
1263
  theme: theme,
1100
- onPress: () => handleFileOpen(file),
1264
+ onPress: () => {
1265
+ // Support selection in regular mode with long press or if already selecting
1266
+ if (!selectMode && selectedIds.size > 0) {
1267
+ // If already in selection mode (some files selected), toggle selection
1268
+ toggleSelect(file);
1269
+ } else {
1270
+ handleFileOpen(file);
1271
+ }
1272
+ },
1273
+ onLongPress: !selectMode ? () => {
1274
+ // Enable selection mode on long press
1275
+ if (selectedIds.size === 0) {
1276
+ setSelectedIds(new Set([file.id]));
1277
+ } else {
1278
+ toggleSelect(file);
1279
+ }
1280
+ } : undefined,
1101
1281
  showChevron: false,
1102
1282
  dense: true,
1103
1283
  multiRow: !!file.metadata?.description,
1104
- selected: selectMode && isSelected,
1105
- // Hide action buttons when selecting
1106
- customContent: !selectMode ? /*#__PURE__*/_jsxs(View, {
1284
+ selected: (selectMode || selectedIds.size > 0) && isSelected,
1285
+ // Hide action buttons when selecting (in selectMode or bulk operations mode)
1286
+ customContent: !selectMode && selectedIds.size === 0 ? /*#__PURE__*/_jsxs(View, {
1107
1287
  style: styles.groupedActions,
1108
1288
  children: [(isImage || isVideo || file.contentType.includes('pdf')) && /*#__PURE__*/_jsx(TouchableOpacity, {
1109
1289
  style: [styles.groupedActionBtn, {
@@ -1151,6 +1331,88 @@ const FileManagementScreen = ({
1151
1331
  };
1152
1332
  });
1153
1333
  }, [filteredFiles, theme, themeStyles, deleting, handleFileDownload, handleFileDelete, handleFileOpen, getSafeDownloadUrl, selectMode, selectedIds]);
1334
+
1335
+ // Scroll to selected file after selection
1336
+ useEffect(() => {
1337
+ if (lastSelectedFileId && selectMode) {
1338
+ if (viewMode === 'all' && scrollViewRef.current) {
1339
+ // Find the index of the selected file
1340
+ const itemIndex = itemRefs.current.get(lastSelectedFileId);
1341
+ if (itemIndex !== undefined && itemIndex >= 0) {
1342
+ // Estimate item height (GroupedItem with dense mode is approximately 60-70px)
1343
+ // Account for description rows which add extra height
1344
+ const baseItemHeight = 65;
1345
+ const descriptionHeight = 30; // Approximate height for description
1346
+ // Use filteredFiles which is already sorted according to user's selection
1347
+ const sortedFiles = filteredFiles;
1348
+
1349
+ // Calculate total height up to this item
1350
+ let scrollPosition = 0;
1351
+ for (let i = 0; i <= itemIndex && i < sortedFiles.length; i++) {
1352
+ const file = sortedFiles[i];
1353
+ scrollPosition += baseItemHeight;
1354
+ if (file.metadata?.description) {
1355
+ scrollPosition += descriptionHeight;
1356
+ }
1357
+ }
1358
+
1359
+ // Add header, controls, search, and stats height (approximately 250px)
1360
+ const headerHeight = 250;
1361
+ const finalScrollPosition = headerHeight + scrollPosition - 150; // Offset to show item near top
1362
+
1363
+ // Use requestAnimationFrame to ensure DOM is updated before scrolling
1364
+ requestAnimationFrame(() => {
1365
+ requestAnimationFrame(() => {
1366
+ scrollViewRef.current?.scrollTo({
1367
+ y: Math.max(0, finalScrollPosition),
1368
+ animated: true
1369
+ });
1370
+ });
1371
+ });
1372
+ }
1373
+ } else if (viewMode === 'photos' && photoScrollViewRef.current) {
1374
+ // For photo grid, find the photo index
1375
+ const photos = filteredFiles.filter(file => file.contentType.startsWith('image/'));
1376
+ const photoIndex = photos.findIndex(p => p.id === lastSelectedFileId);
1377
+ if (photoIndex >= 0) {
1378
+ // Estimate photo item height based on grid layout
1379
+ // Calculate items per row
1380
+ let itemsPerRow = 3;
1381
+ if (containerWidth > 768) itemsPerRow = 6;else if (containerWidth > 480) itemsPerRow = 4;
1382
+ const scrollContainerPadding = 32;
1383
+ const gaps = (itemsPerRow - 1) * 4;
1384
+ const availableWidth = containerWidth - scrollContainerPadding;
1385
+ const itemWidth = (availableWidth - gaps) / itemsPerRow;
1386
+
1387
+ // Calculate row and approximate scroll position
1388
+ const row = Math.floor(photoIndex / itemsPerRow);
1389
+ const headerHeight = 250;
1390
+ const finalScrollPosition = headerHeight + row * (itemWidth + 4) - 150;
1391
+
1392
+ // Use requestAnimationFrame to ensure DOM is updated before scrolling
1393
+ requestAnimationFrame(() => {
1394
+ requestAnimationFrame(() => {
1395
+ photoScrollViewRef.current?.scrollTo({
1396
+ y: Math.max(0, finalScrollPosition),
1397
+ animated: true
1398
+ });
1399
+ });
1400
+ });
1401
+ }
1402
+ }
1403
+ }
1404
+ }, [lastSelectedFileId, selectMode, viewMode, filteredFiles, containerWidth]);
1405
+
1406
+ // Clear selected file ID after scroll animation completes
1407
+ useEffect(() => {
1408
+ if (lastSelectedFileId && scrollViewRef.current) {
1409
+ const timeoutId = setTimeout(() => {
1410
+ setLastSelectedFileId(null);
1411
+ }, 600); // Allow time for scroll animation to complete
1412
+
1413
+ return () => clearTimeout(timeoutId);
1414
+ }
1415
+ }, [lastSelectedFileId]);
1154
1416
  const renderPhotoItem = (photo, index) => {
1155
1417
  const downloadUrl = getSafeDownloadUrl(photo, 'thumb');
1156
1418
 
@@ -1181,8 +1443,8 @@ const FileManagementScreen = ({
1181
1443
  contentFit: "cover",
1182
1444
  transition: 120,
1183
1445
  cachePolicy: "memory-disk",
1184
- onError: _ => {
1185
- console.warn('Failed to load image preview for photo:', photo.id);
1446
+ onError: () => {
1447
+ // Image preview failed to load
1186
1448
  },
1187
1449
  accessibilityLabel: photo.filename
1188
1450
  })
@@ -1231,6 +1493,7 @@ const FileManagementScreen = ({
1231
1493
  });
1232
1494
  }
1233
1495
  return /*#__PURE__*/_jsxs(ScrollView, {
1496
+ ref: photoScrollViewRef,
1234
1497
  style: styles.scrollView,
1235
1498
  contentContainerStyle: styles.photoScrollContainer,
1236
1499
  refreshControl: /*#__PURE__*/_jsx(RefreshControl, {
@@ -1654,8 +1917,8 @@ const FileManagementScreen = ({
1654
1917
  contentFit: "contain",
1655
1918
  transition: 120,
1656
1919
  cachePolicy: "memory-disk",
1657
- onError: e => {
1658
- console.error('Image failed to load:', e?.nativeEvent ?? e);
1920
+ onError: () => {
1921
+ // Image failed to load
1659
1922
  },
1660
1923
  accessibilityLabel: openedFile.filename
1661
1924
  })
@@ -1796,21 +2059,219 @@ const FileManagementScreen = ({
1796
2059
  })
1797
2060
  })]
1798
2061
  });
1799
- if (loading) {
2062
+
2063
+ // Professional Skeleton Loading Component with Advanced Shimmer Effect
2064
+ const SkeletonLoader = /*#__PURE__*/React.memo(() => {
2065
+ const shimmerAnim = useRef(new Animated.Value(0)).current;
2066
+ const skeletonContainerWidth = containerWidth || 400;
2067
+ useEffect(() => {
2068
+ const shimmer = Animated.loop(Animated.timing(shimmerAnim, {
2069
+ toValue: 1,
2070
+ duration: 2000,
2071
+ easing: Easing.linear,
2072
+ useNativeDriver: true
2073
+ }));
2074
+ shimmer.start();
2075
+ return () => shimmer.stop();
2076
+ }, [shimmerAnim]);
2077
+
2078
+ // Create a sweeping shimmer effect
2079
+ const shimmerTranslateX = shimmerAnim.interpolate({
2080
+ inputRange: [0, 1],
2081
+ outputRange: [-skeletonContainerWidth * 2, skeletonContainerWidth * 2]
2082
+ });
2083
+ const SkeletonBox = ({
2084
+ width,
2085
+ height,
2086
+ borderRadius = 8,
2087
+ style,
2088
+ delay = 0
2089
+ }) => {
2090
+ const delayedTranslateX = shimmerAnim.interpolate({
2091
+ inputRange: [0, 1],
2092
+ outputRange: [-skeletonContainerWidth * 2 + delay, skeletonContainerWidth * 2 + delay]
2093
+ });
2094
+ return /*#__PURE__*/_jsxs(View, {
2095
+ style: [{
2096
+ width,
2097
+ height,
2098
+ borderRadius,
2099
+ backgroundColor: themeStyles.isDarkTheme ? '#1E1E1E' : '#F5F5F5',
2100
+ overflow: 'hidden',
2101
+ position: 'relative'
2102
+ }, style],
2103
+ children: [/*#__PURE__*/_jsx(View, {
2104
+ style: {
2105
+ position: 'absolute',
2106
+ top: 0,
2107
+ left: 0,
2108
+ right: 0,
2109
+ bottom: 0,
2110
+ backgroundColor: themeStyles.isDarkTheme ? '#1E1E1E' : '#F5F5F5'
2111
+ }
2112
+ }), /*#__PURE__*/_jsx(Animated.View, {
2113
+ style: {
2114
+ position: 'absolute',
2115
+ top: 0,
2116
+ left: 0,
2117
+ width: '100%',
2118
+ height: '100%',
2119
+ transform: [{
2120
+ translateX: delayedTranslateX
2121
+ }]
2122
+ },
2123
+ children: /*#__PURE__*/_jsx(View, {
2124
+ style: {
2125
+ width: skeletonContainerWidth,
2126
+ height: '100%',
2127
+ backgroundColor: themeStyles.isDarkTheme ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.8)',
2128
+ shadowColor: themeStyles.isDarkTheme ? '#000' : '#FFF',
2129
+ shadowOffset: {
2130
+ width: 0,
2131
+ height: 0
2132
+ },
2133
+ shadowOpacity: 0.3,
2134
+ shadowRadius: 10
2135
+ }
2136
+ })
2137
+ })]
2138
+ });
2139
+ };
2140
+
2141
+ // Skeleton file item matching GroupedSection structure
2142
+ const SkeletonFileItem = ({
2143
+ index
2144
+ }) => /*#__PURE__*/_jsxs(View, {
2145
+ style: [{
2146
+ flexDirection: 'row',
2147
+ alignItems: 'center',
2148
+ paddingHorizontal: 16,
2149
+ paddingVertical: 12,
2150
+ backgroundColor: themeStyles.isDarkTheme ? '#121212' : '#FFFFFF',
2151
+ borderBottomWidth: StyleSheet.hairlineWidth,
2152
+ borderBottomColor: themeStyles.borderColor
2153
+ }],
2154
+ children: [/*#__PURE__*/_jsx(SkeletonBox, {
2155
+ width: 44,
2156
+ height: 44,
2157
+ borderRadius: 8,
2158
+ delay: index * 50
2159
+ }), /*#__PURE__*/_jsxs(View, {
2160
+ style: {
2161
+ flex: 1,
2162
+ marginLeft: 12,
2163
+ justifyContent: 'center'
2164
+ },
2165
+ children: [/*#__PURE__*/_jsx(SkeletonBox, {
2166
+ width: index % 3 === 0 ? '85%' : index % 3 === 1 ? '70%' : '90%',
2167
+ height: 16,
2168
+ style: {
2169
+ marginBottom: 8
2170
+ },
2171
+ delay: index * 50 + 20
2172
+ }), /*#__PURE__*/_jsx(SkeletonBox, {
2173
+ width: index % 2 === 0 ? '50%' : '60%',
2174
+ height: 12,
2175
+ delay: index * 50 + 40
2176
+ })]
2177
+ })]
2178
+ });
1800
2179
  return /*#__PURE__*/_jsxs(View, {
1801
- style: [styles.container, styles.centerContent, {
2180
+ style: [styles.container, {
1802
2181
  backgroundColor
1803
2182
  }],
1804
- children: [/*#__PURE__*/_jsx(ActivityIndicator, {
1805
- size: "large",
1806
- color: themeStyles.primaryColor
1807
- }), /*#__PURE__*/_jsx(Text, {
1808
- style: [styles.loadingText, {
1809
- color: themeStyles.textColor
2183
+ children: [/*#__PURE__*/_jsxs(View, {
2184
+ style: [styles.header, {
2185
+ borderBottomColor: themeStyles.borderColor,
2186
+ borderBottomWidth: StyleSheet.hairlineWidth
2187
+ }],
2188
+ children: [/*#__PURE__*/_jsx(SkeletonBox, {
2189
+ width: 44,
2190
+ height: 44,
2191
+ borderRadius: 12
2192
+ }), /*#__PURE__*/_jsxs(View, {
2193
+ style: [styles.headerTitleContainer, {
2194
+ flex: 1
2195
+ }],
2196
+ children: [/*#__PURE__*/_jsx(SkeletonBox, {
2197
+ width: 140,
2198
+ height: 20,
2199
+ style: {
2200
+ marginBottom: 6
2201
+ }
2202
+ }), /*#__PURE__*/_jsx(SkeletonBox, {
2203
+ width: 100,
2204
+ height: 14
2205
+ })]
2206
+ }), /*#__PURE__*/_jsx(SkeletonBox, {
2207
+ width: 44,
2208
+ height: 44,
2209
+ borderRadius: 12
2210
+ })]
2211
+ }), /*#__PURE__*/_jsxs(View, {
2212
+ style: styles.controlsBar,
2213
+ children: [/*#__PURE__*/_jsx(SkeletonBox, {
2214
+ width: 100,
2215
+ height: 36,
2216
+ borderRadius: 18
2217
+ }), /*#__PURE__*/_jsx(SkeletonBox, {
2218
+ width: 44,
2219
+ height: 44,
2220
+ borderRadius: 22
2221
+ })]
2222
+ }), /*#__PURE__*/_jsx(View, {
2223
+ style: [styles.searchContainer, {
2224
+ backgroundColor: themeStyles.isDarkTheme ? '#1A1A1A' : '#FFFFFF',
2225
+ borderColor: themeStyles.borderColor,
2226
+ borderWidth: StyleSheet.hairlineWidth
2227
+ }],
2228
+ children: /*#__PURE__*/_jsx(SkeletonBox, {
2229
+ width: "100%",
2230
+ height: 44,
2231
+ borderRadius: 12
2232
+ })
2233
+ }), /*#__PURE__*/_jsx(View, {
2234
+ style: [styles.statsContainer, {
2235
+ backgroundColor: themeStyles.isDarkTheme ? '#1A1A1A' : '#FFFFFF',
2236
+ borderColor: themeStyles.borderColor,
2237
+ borderWidth: StyleSheet.hairlineWidth
1810
2238
  }],
1811
- children: "Loading files..."
2239
+ children: [1, 2, 3].map(i => /*#__PURE__*/_jsxs(View, {
2240
+ style: styles.statItem,
2241
+ children: [/*#__PURE__*/_jsx(SkeletonBox, {
2242
+ width: 50,
2243
+ height: 20,
2244
+ style: {
2245
+ marginBottom: 4
2246
+ },
2247
+ delay: i * 30
2248
+ }), /*#__PURE__*/_jsx(SkeletonBox, {
2249
+ width: 40,
2250
+ height: 14,
2251
+ delay: i * 30 + 15
2252
+ })]
2253
+ }, i))
2254
+ }), /*#__PURE__*/_jsx(ScrollView, {
2255
+ style: styles.scrollView,
2256
+ contentContainerStyle: styles.scrollContainer,
2257
+ showsVerticalScrollIndicator: false,
2258
+ children: /*#__PURE__*/_jsx(View, {
2259
+ style: {
2260
+ backgroundColor: themeStyles.isDarkTheme ? '#121212' : '#FFFFFF',
2261
+ borderRadius: 12,
2262
+ overflow: 'hidden',
2263
+ marginHorizontal: 16,
2264
+ marginTop: 8
2265
+ },
2266
+ children: [1, 2, 3, 4, 5, 6, 7, 8].map(i => /*#__PURE__*/_jsx(SkeletonFileItem, {
2267
+ index: i
2268
+ }, i))
2269
+ })
1812
2270
  })]
1813
2271
  });
2272
+ });
2273
+ if (loading) {
2274
+ return /*#__PURE__*/_jsx(SkeletonLoader, {});
1814
2275
  }
1815
2276
 
1816
2277
  // If a file is opened, show the file viewer
@@ -1820,13 +2281,8 @@ const FileManagementScreen = ({
1820
2281
  });
1821
2282
  }
1822
2283
  return /*#__PURE__*/_jsxs(View, {
2284
+ ref: containerRef,
1823
2285
  style: [styles.container, isDragging && Platform.OS === 'web' && styles.dragOverlay],
1824
- ...(Platform.OS === 'web' && user?.id === targetUserId ? {
1825
- onDragOver: handleDragOver,
1826
- onDragEnter: handleDragEnter,
1827
- onDragLeave: handleDragLeave,
1828
- onDrop: handleDrop
1829
- } : {}),
1830
2286
  children: [/*#__PURE__*/_jsx(Header, {
1831
2287
  title: selectMode ? multiSelect ? `${selectedIds.size}${maxSelection ? '/' + maxSelection : ''} Selected` : 'Select a File' : viewMode === 'photos' ? 'Photos' : 'File Management',
1832
2288
  subtitle: selectMode ? multiSelect ? `${filteredFiles.length} available` : 'Tap to select' : `${filteredFiles.length} ${filteredFiles.length === 1 ? 'item' : 'items'}`,
@@ -1840,6 +2296,35 @@ const FileManagementScreen = ({
1840
2296
  text: 'Confirm',
1841
2297
  onPress: confirmMultiSelection,
1842
2298
  disabled: selectedIds.size === 0
2299
+ }] : !selectMode && selectedIds.size > 0 ? [{
2300
+ key: 'clear',
2301
+ text: 'Clear',
2302
+ onPress: () => setSelectedIds(new Set())
2303
+ }, {
2304
+ key: 'delete',
2305
+ text: `Delete (${selectedIds.size})`,
2306
+ onPress: handleBulkDelete,
2307
+ icon: 'trash'
2308
+ }, {
2309
+ key: 'visibility',
2310
+ text: 'Visibility',
2311
+ onPress: () => {
2312
+ // Show visibility options menu
2313
+ Alert.alert('Change Visibility', `Change visibility for ${selectedIds.size} file(s)?`, [{
2314
+ text: 'Cancel',
2315
+ style: 'cancel'
2316
+ }, {
2317
+ text: 'Private',
2318
+ onPress: () => handleBulkVisibilityChange('private')
2319
+ }, {
2320
+ text: 'Public',
2321
+ onPress: () => handleBulkVisibilityChange('public')
2322
+ }, {
2323
+ text: 'Unlisted',
2324
+ onPress: () => handleBulkVisibilityChange('unlisted')
2325
+ }]);
2326
+ },
2327
+ icon: 'eye'
1843
2328
  }] : undefined,
1844
2329
  onBack: onClose || goBack,
1845
2330
  theme: theme,
@@ -1849,32 +2334,95 @@ const FileManagementScreen = ({
1849
2334
  titleAlignment: "left"
1850
2335
  }), /*#__PURE__*/_jsxs(View, {
1851
2336
  style: styles.controlsBar,
1852
- children: [/*#__PURE__*/_jsxs(View, {
1853
- style: [styles.viewModeToggle, {
2337
+ children: [/*#__PURE__*/_jsx(ScrollView, {
2338
+ horizontal: true,
2339
+ showsHorizontalScrollIndicator: false,
2340
+ style: styles.viewModeScroll,
2341
+ children: /*#__PURE__*/_jsxs(View, {
2342
+ style: [styles.viewModeToggle, {
2343
+ backgroundColor: themeStyles.isDarkTheme ? '#181818' : '#FFFFFF',
2344
+ borderWidth: 1,
2345
+ borderColor: themeStyles.isDarkTheme ? '#2A2A2A' : '#E8E9EA'
2346
+ }],
2347
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
2348
+ style: [styles.viewModeButton, viewMode === 'all' && {
2349
+ backgroundColor: themeStyles.primaryColor
2350
+ }],
2351
+ onPress: () => setViewMode('all'),
2352
+ children: /*#__PURE__*/_jsx(Ionicons, {
2353
+ name: "folder",
2354
+ size: 18,
2355
+ color: viewMode === 'all' ? '#FFFFFF' : themeStyles.textColor
2356
+ })
2357
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
2358
+ style: [styles.viewModeButton, viewMode === 'photos' && {
2359
+ backgroundColor: themeStyles.primaryColor
2360
+ }],
2361
+ onPress: () => setViewMode('photos'),
2362
+ children: /*#__PURE__*/_jsx(Ionicons, {
2363
+ name: "images",
2364
+ size: 18,
2365
+ color: viewMode === 'photos' ? '#FFFFFF' : themeStyles.textColor
2366
+ })
2367
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
2368
+ style: [styles.viewModeButton, viewMode === 'videos' && {
2369
+ backgroundColor: themeStyles.primaryColor
2370
+ }],
2371
+ onPress: () => setViewMode('videos'),
2372
+ children: /*#__PURE__*/_jsx(Ionicons, {
2373
+ name: "videocam",
2374
+ size: 18,
2375
+ color: viewMode === 'videos' ? '#FFFFFF' : themeStyles.textColor
2376
+ })
2377
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
2378
+ style: [styles.viewModeButton, viewMode === 'documents' && {
2379
+ backgroundColor: themeStyles.primaryColor
2380
+ }],
2381
+ onPress: () => setViewMode('documents'),
2382
+ children: /*#__PURE__*/_jsx(Ionicons, {
2383
+ name: "document-text",
2384
+ size: 18,
2385
+ color: viewMode === 'documents' ? '#FFFFFF' : themeStyles.textColor
2386
+ })
2387
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
2388
+ style: [styles.viewModeButton, viewMode === 'audio' && {
2389
+ backgroundColor: themeStyles.primaryColor
2390
+ }],
2391
+ onPress: () => setViewMode('audio'),
2392
+ children: /*#__PURE__*/_jsx(Ionicons, {
2393
+ name: "musical-notes",
2394
+ size: 18,
2395
+ color: viewMode === 'audio' ? '#FFFFFF' : themeStyles.textColor
2396
+ })
2397
+ })]
2398
+ })
2399
+ }), /*#__PURE__*/_jsxs(TouchableOpacity, {
2400
+ style: [styles.sortButton, {
1854
2401
  backgroundColor: themeStyles.isDarkTheme ? '#181818' : '#FFFFFF',
1855
- borderWidth: 1,
1856
2402
  borderColor: themeStyles.isDarkTheme ? '#2A2A2A' : '#E8E9EA'
1857
2403
  }],
1858
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
1859
- style: [styles.viewModeButton, viewMode === 'all' && {
1860
- backgroundColor: themeStyles.primaryColor
1861
- }],
1862
- onPress: () => setViewMode('all'),
1863
- children: /*#__PURE__*/_jsx(Ionicons, {
1864
- name: "folder",
1865
- size: 18,
1866
- color: viewMode === 'all' ? '#FFFFFF' : themeStyles.textColor
1867
- })
1868
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
1869
- style: [styles.viewModeButton, viewMode === 'photos' && {
1870
- backgroundColor: themeStyles.primaryColor
1871
- }],
1872
- onPress: () => setViewMode('photos'),
1873
- children: /*#__PURE__*/_jsx(Ionicons, {
1874
- name: "images",
1875
- size: 18,
1876
- color: viewMode === 'photos' ? '#FFFFFF' : themeStyles.textColor
1877
- })
2404
+ onPress: () => {
2405
+ // Cycle through sort options: date -> size -> name -> type -> date
2406
+ const sortOrder = ['date', 'size', 'name', 'type'];
2407
+ const currentIndex = sortOrder.indexOf(sortBy);
2408
+ const nextIndex = (currentIndex + 1) % sortOrder.length;
2409
+ setSortBy(sortOrder[nextIndex]);
2410
+ // Toggle order when cycling back to date
2411
+ if (nextIndex === 0) {
2412
+ setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
2413
+ }
2414
+ },
2415
+ children: [/*#__PURE__*/_jsx(Ionicons, {
2416
+ name: sortOrder === 'asc' ? 'arrow-up' : 'arrow-down',
2417
+ size: 18,
2418
+ color: themeStyles.textColor
2419
+ }), /*#__PURE__*/_jsx(Ionicons, {
2420
+ name: sortBy === 'date' ? 'calendar' : sortBy === 'size' ? 'resize' : sortBy === 'name' ? 'text' : 'document',
2421
+ size: 16,
2422
+ color: themeStyles.textColor,
2423
+ style: {
2424
+ marginLeft: 4
2425
+ }
1878
2426
  })]
1879
2427
  }), user?.id === targetUserId && (!selectMode || selectMode && allowUploadInSelectMode) && /*#__PURE__*/_jsx(TouchableOpacity, {
1880
2428
  style: [styles.uploadButton, {
@@ -1969,6 +2517,7 @@ const FileManagementScreen = ({
1969
2517
  })]
1970
2518
  })]
1971
2519
  }), viewMode === 'photos' ? renderPhotoGrid() : /*#__PURE__*/_jsx(ScrollView, {
2520
+ ref: scrollViewRef,
1972
2521
  style: styles.scrollView,
1973
2522
  contentContainerStyle: styles.scrollContainer,
1974
2523
  refreshControl: /*#__PURE__*/_jsx(RefreshControl, {
@@ -2067,21 +2616,29 @@ const FileManagementScreen = ({
2067
2616
  }), isDragging && Platform.OS === 'web' && /*#__PURE__*/_jsx(View, {
2068
2617
  style: styles.dragDropOverlay,
2069
2618
  children: /*#__PURE__*/_jsxs(View, {
2070
- style: styles.dragDropContent,
2071
- children: [/*#__PURE__*/_jsx(Ionicons, {
2072
- name: "cloud-upload",
2073
- size: 64,
2074
- color: themeStyles.primaryColor
2619
+ style: [styles.dragDropContent, {
2620
+ backgroundColor: themeStyles.isDarkTheme ? 'rgba(30, 30, 30, 0.98)' : 'rgba(255, 255, 255, 0.98)',
2621
+ borderColor: themeStyles.primaryColor
2622
+ }],
2623
+ children: [/*#__PURE__*/_jsx(View, {
2624
+ style: [styles.dragDropIconContainer, {
2625
+ backgroundColor: `${themeStyles.primaryColor}15`
2626
+ }],
2627
+ children: /*#__PURE__*/_jsx(Ionicons, {
2628
+ name: "cloud-upload",
2629
+ size: 64,
2630
+ color: themeStyles.primaryColor
2631
+ })
2075
2632
  }), /*#__PURE__*/_jsx(Text, {
2076
2633
  style: [styles.dragDropTitle, {
2077
2634
  color: themeStyles.primaryColor
2078
2635
  }],
2079
2636
  children: "Drop files to upload"
2080
- }), /*#__PURE__*/_jsxs(Text, {
2637
+ }), /*#__PURE__*/_jsx(Text, {
2081
2638
  style: [styles.dragDropSubtitle, {
2082
2639
  color: themeStyles.isDarkTheme ? '#BBBBBB' : '#666666'
2083
2640
  }],
2084
- children: ["Release to upload", uploadProgress ? ` (${uploadProgress.current}/${uploadProgress.total})` : ' multiple files']
2641
+ children: "Release to upload multiple files"
2085
2642
  })]
2086
2643
  })
2087
2644
  })]
@@ -2580,36 +3137,154 @@ const styles = StyleSheet.create({
2580
3137
  fontSize: 16,
2581
3138
  fontWeight: '600'
2582
3139
  },
2583
- // Drag and Drop styles
3140
+ // Drag and Drop styles - Enhanced
2584
3141
  dragDropOverlay: {
2585
3142
  position: 'absolute',
2586
3143
  top: 0,
2587
3144
  left: 0,
2588
3145
  right: 0,
2589
3146
  bottom: 0,
2590
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
3147
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
2591
3148
  justifyContent: 'center',
2592
3149
  alignItems: 'center',
2593
3150
  zIndex: 1000
2594
3151
  },
2595
3152
  dragDropContent: {
2596
3153
  alignItems: 'center',
2597
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
2598
- padding: 20,
2599
- borderRadius: 14,
2600
- borderWidth: 1,
2601
- borderColor: '#66AFFF',
2602
- borderStyle: 'dashed'
3154
+ padding: 32,
3155
+ borderRadius: 20,
3156
+ borderWidth: 3,
3157
+ borderStyle: 'dashed',
3158
+ minWidth: 280,
3159
+ shadowColor: '#000',
3160
+ shadowOpacity: 0.3,
3161
+ shadowRadius: 20,
3162
+ shadowOffset: {
3163
+ width: 0,
3164
+ height: 10
3165
+ },
3166
+ elevation: 10
3167
+ },
3168
+ dragDropIconContainer: {
3169
+ padding: 24,
3170
+ borderRadius: 50,
3171
+ marginBottom: 16
2603
3172
  },
2604
3173
  dragDropTitle: {
2605
- fontSize: 20,
2606
- fontWeight: 'bold',
2607
- marginTop: 12,
2608
- marginBottom: 6
3174
+ fontSize: 24,
3175
+ fontWeight: '700',
3176
+ fontFamily: fontFamilies.phuduBold,
3177
+ marginBottom: 8
2609
3178
  },
2610
3179
  dragDropSubtitle: {
2611
3180
  fontSize: 16,
2612
- textAlign: 'center'
3181
+ fontWeight: '500',
3182
+ fontFamily: fontFamilies.phuduMedium
3183
+ },
3184
+ // Upload Preview Modal styles
3185
+ uploadPreviewContainer: {
3186
+ flex: 1
3187
+ },
3188
+ uploadPreviewHeader: {
3189
+ flexDirection: 'row',
3190
+ alignItems: 'center',
3191
+ justifyContent: 'space-between',
3192
+ paddingHorizontal: 16,
3193
+ paddingVertical: 16,
3194
+ borderBottomWidth: 1
3195
+ },
3196
+ uploadPreviewTitle: {
3197
+ fontSize: 20,
3198
+ fontWeight: '700',
3199
+ fontFamily: fontFamilies.phuduBold
3200
+ },
3201
+ uploadPreviewList: {
3202
+ flex: 1,
3203
+ padding: 16
3204
+ },
3205
+ uploadPreviewItem: {
3206
+ flexDirection: 'row',
3207
+ alignItems: 'center',
3208
+ padding: 12,
3209
+ borderRadius: 12,
3210
+ borderWidth: 1,
3211
+ marginBottom: 12,
3212
+ gap: 12
3213
+ },
3214
+ uploadPreviewThumbnail: {
3215
+ width: 60,
3216
+ height: 60,
3217
+ borderRadius: 8
3218
+ },
3219
+ uploadPreviewIconContainer: {
3220
+ width: 60,
3221
+ height: 60,
3222
+ borderRadius: 8,
3223
+ alignItems: 'center',
3224
+ justifyContent: 'center'
3225
+ },
3226
+ uploadPreviewInfo: {
3227
+ flex: 1,
3228
+ minWidth: 0
3229
+ },
3230
+ uploadPreviewName: {
3231
+ fontSize: 16,
3232
+ fontWeight: '600',
3233
+ fontFamily: fontFamilies.phuduSemiBold,
3234
+ marginBottom: 4
3235
+ },
3236
+ uploadPreviewMeta: {
3237
+ fontSize: 13,
3238
+ fontFamily: fontFamilies.phudu
3239
+ },
3240
+ uploadPreviewRemove: {
3241
+ padding: 4
3242
+ },
3243
+ uploadPreviewFooter: {
3244
+ padding: 16,
3245
+ borderTopWidth: 1
3246
+ },
3247
+ uploadPreviewStats: {
3248
+ flexDirection: 'row',
3249
+ justifyContent: 'space-between',
3250
+ marginBottom: 16
3251
+ },
3252
+ uploadPreviewStatsText: {
3253
+ fontSize: 15,
3254
+ fontWeight: '600',
3255
+ fontFamily: fontFamilies.phuduSemiBold
3256
+ },
3257
+ uploadPreviewActions: {
3258
+ flexDirection: 'row',
3259
+ gap: 12
3260
+ },
3261
+ uploadPreviewCancelButton: {
3262
+ flex: 1,
3263
+ paddingVertical: 14,
3264
+ borderRadius: 12,
3265
+ borderWidth: 1,
3266
+ alignItems: 'center',
3267
+ justifyContent: 'center'
3268
+ },
3269
+ uploadPreviewCancelText: {
3270
+ fontSize: 16,
3271
+ fontWeight: '600',
3272
+ fontFamily: fontFamilies.phuduSemiBold
3273
+ },
3274
+ uploadPreviewConfirmButton: {
3275
+ flex: 2,
3276
+ flexDirection: 'row',
3277
+ alignItems: 'center',
3278
+ justifyContent: 'center',
3279
+ paddingVertical: 14,
3280
+ borderRadius: 12,
3281
+ gap: 8
3282
+ },
3283
+ uploadPreviewConfirmText: {
3284
+ color: '#FFFFFF',
3285
+ fontSize: 16,
3286
+ fontWeight: '600',
3287
+ fontFamily: fontFamilies.phuduSemiBold
2613
3288
  },
2614
3289
  // File Viewer styles
2615
3290
  fileViewerContainer: {
@@ -2783,6 +3458,10 @@ const styles = StyleSheet.create({
2783
3458
  paddingBottom: 4,
2784
3459
  gap: 12
2785
3460
  },
3461
+ viewModeScroll: {
3462
+ flex: 1,
3463
+ maxWidth: '80%'
3464
+ },
2786
3465
  viewModeToggle: {
2787
3466
  flexDirection: 'row',
2788
3467
  borderRadius: 24,
@@ -2798,6 +3477,16 @@ const styles = StyleSheet.create({
2798
3477
  justifyContent: 'center',
2799
3478
  marginHorizontal: 1
2800
3479
  },
3480
+ sortButton: {
3481
+ flexDirection: 'row',
3482
+ alignItems: 'center',
3483
+ justifyContent: 'center',
3484
+ paddingHorizontal: 12,
3485
+ paddingVertical: 10,
3486
+ borderRadius: 20,
3487
+ borderWidth: 1,
3488
+ minWidth: 44
3489
+ },
2801
3490
  // Photo Grid styles
2802
3491
  photoScrollContainer: {
2803
3492
  padding: 10
@@ -2899,6 +3588,18 @@ const styles = StyleSheet.create({
2899
3588
  aspectRatio: 1,
2900
3589
  borderRadius: 8,
2901
3590
  marginBottom: 4
3591
+ },
3592
+ skeletonFileItem: {
3593
+ flexDirection: 'row',
3594
+ alignItems: 'center',
3595
+ paddingHorizontal: 16,
3596
+ paddingVertical: 12,
3597
+ borderBottomWidth: 1,
3598
+ gap: 12
3599
+ },
3600
+ skeletonFileInfo: {
3601
+ flex: 1,
3602
+ justifyContent: 'center'
2902
3603
  }
2903
3604
  });
2904
3605
  export default FileManagementScreen;