@oxyhq/services 5.3.11 → 5.4.1

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 (213) hide show
  1. package/README.md +21 -0
  2. package/lib/commonjs/assets/assets/icons/OxyServices.tsx +67 -0
  3. package/lib/commonjs/assets/assets/icons/logo_OxyServices.svg +1 -0
  4. package/lib/commonjs/assets/icons/OxyServices.js +53 -0
  5. package/lib/commonjs/assets/icons/OxyServices.js.map +1 -0
  6. package/lib/commonjs/assets/icons/logo_OxyServices.svg +1 -0
  7. package/lib/commonjs/core/index.js +119 -23
  8. package/lib/commonjs/core/index.js.map +1 -1
  9. package/lib/commonjs/index.js +2 -0
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/lib/sonner.js +15 -11
  12. package/lib/commonjs/lib/sonner.js.map +1 -1
  13. package/lib/commonjs/node/index.js +2 -0
  14. package/lib/commonjs/node/index.js.map +1 -1
  15. package/lib/commonjs/ui/components/GroupedItem.js +109 -0
  16. package/lib/commonjs/ui/components/GroupedItem.js.map +1 -0
  17. package/lib/commonjs/ui/components/GroupedSection.js +33 -0
  18. package/lib/commonjs/ui/components/GroupedSection.js.map +1 -0
  19. package/lib/commonjs/ui/components/OxyProvider.js +95 -112
  20. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  21. package/lib/commonjs/ui/components/ProfileCard.js +124 -0
  22. package/lib/commonjs/ui/components/ProfileCard.js.map +1 -0
  23. package/lib/commonjs/ui/components/QuickActions.js +87 -0
  24. package/lib/commonjs/ui/components/QuickActions.js.map +1 -0
  25. package/lib/commonjs/ui/components/Section.js +36 -0
  26. package/lib/commonjs/ui/components/Section.js.map +1 -0
  27. package/lib/commonjs/ui/components/SectionTitle.js +35 -0
  28. package/lib/commonjs/ui/components/SectionTitle.js.map +1 -0
  29. package/lib/commonjs/ui/components/bottomSheet/index.js +6 -6
  30. package/lib/commonjs/ui/components/index.js +97 -0
  31. package/lib/commonjs/ui/components/index.js.map +1 -0
  32. package/lib/commonjs/ui/navigation/OxyRouter.js +20 -3
  33. package/lib/commonjs/ui/navigation/OxyRouter.js.map +1 -1
  34. package/lib/commonjs/ui/screens/AccountCenterScreen.js +190 -207
  35. package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
  36. package/lib/commonjs/ui/screens/AccountManagementDemo.js +299 -0
  37. package/lib/commonjs/ui/screens/AccountManagementDemo.js.map +1 -0
  38. package/lib/commonjs/ui/screens/AccountOverviewScreen.js +669 -401
  39. package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
  40. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +695 -498
  41. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  42. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +451 -488
  43. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
  44. package/lib/commonjs/ui/screens/AppInfoScreen.js +498 -185
  45. package/lib/commonjs/ui/screens/AppInfoScreen.js.map +1 -1
  46. package/lib/commonjs/ui/screens/BillingManagementScreen.js +636 -0
  47. package/lib/commonjs/ui/screens/BillingManagementScreen.js.map +1 -0
  48. package/lib/commonjs/ui/screens/FileManagementScreen.js +2497 -0
  49. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -0
  50. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js +1620 -0
  51. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js.map +1 -0
  52. package/lib/commonjs/ui/screens/ProfileScreen.js +117 -13
  53. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  54. package/lib/commonjs/ui/screens/SessionManagementScreen.js.map +1 -1
  55. package/lib/commonjs/ui/screens/SignInScreen.js +1 -1
  56. package/lib/commonjs/ui/screens/SignUpScreen.js +1 -1
  57. package/lib/commonjs/utils/polyfills.js +42 -0
  58. package/lib/commonjs/utils/polyfills.js.map +1 -0
  59. package/lib/module/assets/assets/icons/OxyServices.tsx +67 -0
  60. package/lib/module/assets/assets/icons/logo_OxyServices.svg +1 -0
  61. package/lib/module/assets/icons/OxyServices.js +46 -0
  62. package/lib/module/assets/icons/OxyServices.js.map +1 -0
  63. package/lib/module/assets/icons/logo_OxyServices.svg +1 -0
  64. package/lib/module/core/index.js +119 -23
  65. package/lib/module/core/index.js.map +1 -1
  66. package/lib/module/index.js +3 -0
  67. package/lib/module/index.js.map +1 -1
  68. package/lib/module/lib/sonner.js +13 -1
  69. package/lib/module/lib/sonner.js.map +1 -1
  70. package/lib/module/node/index.js +3 -0
  71. package/lib/module/node/index.js.map +1 -1
  72. package/lib/module/ui/components/GroupedItem.js +104 -0
  73. package/lib/module/ui/components/GroupedItem.js.map +1 -0
  74. package/lib/module/ui/components/GroupedSection.js +28 -0
  75. package/lib/module/ui/components/GroupedSection.js.map +1 -0
  76. package/lib/module/ui/components/OxyProvider.js +97 -114
  77. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  78. package/lib/module/ui/components/ProfileCard.js +119 -0
  79. package/lib/module/ui/components/ProfileCard.js.map +1 -0
  80. package/lib/module/ui/components/QuickActions.js +82 -0
  81. package/lib/module/ui/components/QuickActions.js.map +1 -0
  82. package/lib/module/ui/components/Section.js +31 -0
  83. package/lib/module/ui/components/Section.js.map +1 -0
  84. package/lib/module/ui/components/SectionTitle.js +30 -0
  85. package/lib/module/ui/components/SectionTitle.js.map +1 -0
  86. package/lib/module/ui/components/bottomSheet/index.js +2 -5
  87. package/lib/module/ui/components/bottomSheet/index.js.map +1 -1
  88. package/lib/module/ui/components/index.js +18 -0
  89. package/lib/module/ui/components/index.js.map +1 -0
  90. package/lib/module/ui/navigation/OxyRouter.js +20 -3
  91. package/lib/module/ui/navigation/OxyRouter.js.map +1 -1
  92. package/lib/module/ui/screens/AccountCenterScreen.js +191 -208
  93. package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
  94. package/lib/module/ui/screens/AccountManagementDemo.js +296 -0
  95. package/lib/module/ui/screens/AccountManagementDemo.js.map +1 -0
  96. package/lib/module/ui/screens/AccountOverviewScreen.js +671 -403
  97. package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
  98. package/lib/module/ui/screens/AccountSettingsScreen.js +698 -501
  99. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  100. package/lib/module/ui/screens/AccountSwitcherScreen.js +450 -488
  101. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
  102. package/lib/module/ui/screens/AppInfoScreen.js +498 -186
  103. package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
  104. package/lib/module/ui/screens/BillingManagementScreen.js +631 -0
  105. package/lib/module/ui/screens/BillingManagementScreen.js.map +1 -0
  106. package/lib/module/ui/screens/FileManagementScreen.js +2492 -0
  107. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -0
  108. package/lib/module/ui/screens/PremiumSubscriptionScreen.js +1615 -0
  109. package/lib/module/ui/screens/PremiumSubscriptionScreen.js.map +1 -0
  110. package/lib/module/ui/screens/ProfileScreen.js +118 -14
  111. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  112. package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -1
  113. package/lib/module/ui/screens/SignInScreen.js +1 -1
  114. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  115. package/lib/module/ui/screens/SignUpScreen.js +1 -1
  116. package/lib/module/ui/screens/SignUpScreen.js.map +1 -1
  117. package/lib/module/utils/polyfills.js +36 -0
  118. package/lib/module/utils/polyfills.js.map +1 -0
  119. package/lib/typescript/assets/icons/OxyServices.d.ts +29 -0
  120. package/lib/typescript/assets/icons/OxyServices.d.ts.map +1 -0
  121. package/lib/typescript/core/index.d.ts +26 -1
  122. package/lib/typescript/core/index.d.ts.map +1 -1
  123. package/lib/typescript/index.d.ts +1 -0
  124. package/lib/typescript/index.d.ts.map +1 -1
  125. package/lib/typescript/lib/sonner.d.ts +5 -1
  126. package/lib/typescript/lib/sonner.d.ts.map +1 -1
  127. package/lib/typescript/models/interfaces.d.ts +1 -2
  128. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  129. package/lib/typescript/node/index.d.ts +1 -0
  130. package/lib/typescript/node/index.d.ts.map +1 -1
  131. package/lib/typescript/ui/components/GroupedItem.d.ts +17 -0
  132. package/lib/typescript/ui/components/GroupedItem.d.ts.map +1 -0
  133. package/lib/typescript/ui/components/GroupedSection.d.ts +19 -0
  134. package/lib/typescript/ui/components/GroupedSection.d.ts.map +1 -0
  135. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  136. package/lib/typescript/ui/components/ProfileCard.d.ts +20 -0
  137. package/lib/typescript/ui/components/ProfileCard.d.ts.map +1 -0
  138. package/lib/typescript/ui/components/QuickActions.d.ts +15 -0
  139. package/lib/typescript/ui/components/QuickActions.d.ts.map +1 -0
  140. package/lib/typescript/ui/components/Section.d.ts +11 -0
  141. package/lib/typescript/ui/components/Section.d.ts.map +1 -0
  142. package/lib/typescript/ui/components/SectionTitle.d.ts +9 -0
  143. package/lib/typescript/ui/components/SectionTitle.d.ts.map +1 -0
  144. package/lib/typescript/ui/components/bottomSheet/index.d.ts +3 -2
  145. package/lib/typescript/ui/components/bottomSheet/index.d.ts.map +1 -1
  146. package/lib/typescript/ui/components/index.d.ts +13 -0
  147. package/lib/typescript/ui/components/index.d.ts.map +1 -0
  148. package/lib/typescript/ui/navigation/OxyRouter.d.ts.map +1 -1
  149. package/lib/typescript/ui/navigation/types.d.ts +8 -0
  150. package/lib/typescript/ui/navigation/types.d.ts.map +1 -1
  151. package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
  152. package/lib/typescript/ui/screens/AccountManagementDemo.d.ts +8 -0
  153. package/lib/typescript/ui/screens/AccountManagementDemo.d.ts.map +1 -0
  154. package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
  155. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts +1 -4
  156. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  157. package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
  158. package/lib/typescript/ui/screens/AppInfoScreen.d.ts.map +1 -1
  159. package/lib/typescript/ui/screens/BillingManagementScreen.d.ts +5 -0
  160. package/lib/typescript/ui/screens/BillingManagementScreen.d.ts.map +1 -0
  161. package/lib/typescript/ui/screens/FileManagementScreen.d.ts +8 -0
  162. package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -0
  163. package/lib/typescript/ui/screens/PremiumSubscriptionScreen.d.ts +5 -0
  164. package/lib/typescript/ui/screens/PremiumSubscriptionScreen.d.ts.map +1 -0
  165. package/lib/typescript/ui/screens/ProfileScreen.d.ts.map +1 -1
  166. package/lib/typescript/ui/screens/SessionManagementScreen.d.ts.map +1 -1
  167. package/lib/typescript/utils/polyfills.d.ts +6 -0
  168. package/lib/typescript/utils/polyfills.d.ts.map +1 -0
  169. package/package.json +11 -3
  170. package/src/__tests__/polyfills.test.ts +30 -0
  171. package/src/__tests__/setup.ts +43 -0
  172. package/src/__tests__/ui/screens/AccountSettingsScreen.test.tsx +8 -8
  173. package/src/assets/icons/OxyServices.tsx +67 -0
  174. package/src/assets/icons/logo_OxyServices.svg +1 -0
  175. package/src/core/index.ts +127 -19
  176. package/src/index.ts +3 -0
  177. package/src/lib/sonner.ts +10 -1
  178. package/src/models/interfaces.ts +1 -2
  179. package/src/node/index.ts +3 -0
  180. package/src/ui/components/GroupedItem.tsx +118 -0
  181. package/src/ui/components/GroupedSection.tsx +45 -0
  182. package/src/ui/components/OxyProvider.tsx +95 -120
  183. package/src/ui/components/ProfileCard.tsx +129 -0
  184. package/src/ui/components/QuickActions.tsx +90 -0
  185. package/src/ui/components/Section.tsx +37 -0
  186. package/src/ui/components/SectionTitle.tsx +31 -0
  187. package/src/ui/components/bottomSheet/index.tsx +13 -11
  188. package/src/ui/components/index.ts +15 -0
  189. package/src/ui/navigation/OxyRouter.tsx +20 -3
  190. package/src/ui/navigation/types.ts +10 -1
  191. package/src/ui/screens/AccountCenterScreen.tsx +188 -159
  192. package/src/ui/screens/AccountManagementDemo.tsx +297 -0
  193. package/src/ui/screens/AccountOverviewScreen.tsx +474 -310
  194. package/src/ui/screens/AccountSettingsScreen.tsx +648 -463
  195. package/src/ui/screens/AccountSwitcherScreen.tsx +385 -449
  196. package/src/ui/screens/AppInfoScreen.tsx +571 -140
  197. package/src/ui/screens/BillingManagementScreen.tsx +589 -0
  198. package/src/ui/screens/FileManagementScreen.tsx +2513 -0
  199. package/src/ui/screens/PremiumSubscriptionScreen.tsx +1628 -0
  200. package/src/ui/screens/ProfileScreen.tsx +101 -7
  201. package/src/ui/screens/SessionManagementScreen.tsx +1 -0
  202. package/src/ui/screens/SignInScreen.tsx +1 -1
  203. package/src/ui/screens/SignUpScreen.tsx +1 -1
  204. package/src/utils/polyfills.ts +34 -0
  205. package/lib/commonjs/lib/sonner.web.js +0 -17
  206. package/lib/commonjs/lib/sonner.web.js.map +0 -1
  207. package/lib/module/lib/sonner.web.js +0 -4
  208. package/lib/module/lib/sonner.web.js.map +0 -1
  209. package/lib/typescript/__tests__/ui/screens/AccountSettingsScreen.test.d.ts +0 -2
  210. package/lib/typescript/__tests__/ui/screens/AccountSettingsScreen.test.d.ts.map +0 -1
  211. package/lib/typescript/lib/sonner.web.d.ts +0 -2
  212. package/lib/typescript/lib/sonner.web.d.ts.map +0 -1
  213. package/src/lib/sonner.web.ts +0 -1
@@ -0,0 +1,2513 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ StyleSheet,
7
+ ScrollView,
8
+ ActivityIndicator,
9
+ Platform,
10
+ RefreshControl,
11
+ Dimensions,
12
+ Modal,
13
+ TextInput,
14
+ Image,
15
+ } from 'react-native';
16
+ import { BaseScreenProps } from '../navigation/types';
17
+ import { useOxy } from '../context/OxyContext';
18
+ import { fontFamilies } from '../styles/fonts';
19
+ import { toast } from '../../lib/sonner';
20
+ import { Ionicons } from '@expo/vector-icons';
21
+ import { FileMetadata } from '../../models/interfaces';
22
+
23
+ interface FileManagementScreenProps extends BaseScreenProps {
24
+ userId?: string;
25
+ }
26
+
27
+ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
28
+ onClose,
29
+ theme,
30
+ goBack,
31
+ navigate,
32
+ userId,
33
+ containerWidth = 400, // Fallback for when not provided by the router
34
+ }) => {
35
+ const { user, oxyServices } = useOxy();
36
+
37
+ // Debug: log the actual container width
38
+ useEffect(() => {
39
+ console.log('[FileManagementScreen] Container width (full):', containerWidth);
40
+ // Padding structure:
41
+ // - containerWidth = full bottom sheet container width (measured from OxyProvider)
42
+ // - photoScrollContainer adds padding: 16 (32px total horizontal padding)
43
+ // - Available content width = containerWidth - 32
44
+ const availableContentWidth = containerWidth - 32;
45
+ console.log('[FileManagementScreen] Available content width:', availableContentWidth);
46
+ console.log('[FileManagementScreen] Spacing fix applied: 4px uniform gap both horizontal and vertical');
47
+ }, [containerWidth]);
48
+ const [files, setFiles] = useState<FileMetadata[]>([]);
49
+ const [loading, setLoading] = useState(true);
50
+ const [refreshing, setRefreshing] = useState(false);
51
+ const [uploading, setUploading] = useState(false);
52
+ const [uploadProgress, setUploadProgress] = useState<{current: number, total: number} | null>(null);
53
+ const [deleting, setDeleting] = useState<string | null>(null);
54
+ const [selectedFile, setSelectedFile] = useState<FileMetadata | null>(null);
55
+ const [showFileDetails, setShowFileDetails] = useState(false);
56
+ const [openedFile, setOpenedFile] = useState<FileMetadata | null>(null);
57
+ const [fileContent, setFileContent] = useState<string | null>(null);
58
+ const [loadingFileContent, setLoadingFileContent] = useState(false);
59
+ const [showFileDetailsInViewer, setShowFileDetailsInViewer] = useState(false);
60
+ const [viewMode, setViewMode] = useState<'all' | 'photos'>('all');
61
+ const [searchQuery, setSearchQuery] = useState('');
62
+ const [filteredFiles, setFilteredFiles] = useState<FileMetadata[]>([]);
63
+ const [isDragging, setIsDragging] = useState(false);
64
+ const [photoDimensions, setPhotoDimensions] = useState<{[key: string]: {width: number, height: number}}>({});
65
+ const [loadingDimensions, setLoadingDimensions] = useState(false);
66
+ const [hoveredPreview, setHoveredPreview] = useState<string | null>(null);
67
+
68
+ const isDarkTheme = theme === 'dark';
69
+ const textColor = isDarkTheme ? '#FFFFFF' : '#000000';
70
+ const backgroundColor = isDarkTheme ? '#121212' : '#f2f2f2';
71
+ const secondaryBackgroundColor = isDarkTheme ? '#222222' : '#FFFFFF';
72
+ const borderColor = isDarkTheme ? '#444444' : '#E0E0E0';
73
+ const primaryColor = '#007AFF';
74
+ const dangerColor = '#FF3B30';
75
+ const successColor = '#34C759';
76
+
77
+ const targetUserId = userId || user?.id;
78
+
79
+ const loadFiles = useCallback(async (isRefresh = false) => {
80
+ if (!targetUserId) return;
81
+
82
+ try {
83
+ if (isRefresh) {
84
+ setRefreshing(true);
85
+ } else {
86
+ setLoading(true);
87
+ }
88
+
89
+ const response = await oxyServices.listUserFiles(targetUserId);
90
+ setFiles(response.files || []);
91
+ } catch (error: any) {
92
+ console.error('Failed to load files:', error);
93
+ toast.error(error.message || 'Failed to load files');
94
+ } finally {
95
+ setLoading(false);
96
+ setRefreshing(false);
97
+ }
98
+ }, [targetUserId, oxyServices]);
99
+
100
+ // Filter files based on search query and view mode
101
+ useEffect(() => {
102
+ let filteredByMode = files;
103
+
104
+ // Filter by view mode first
105
+ if (viewMode === 'photos') {
106
+ filteredByMode = files.filter(file => file.contentType.startsWith('image/'));
107
+ }
108
+
109
+ // Then filter by search query
110
+ if (!searchQuery.trim()) {
111
+ setFilteredFiles(filteredByMode);
112
+ } else {
113
+ const query = searchQuery.toLowerCase();
114
+ const filtered = filteredByMode.filter(file =>
115
+ file.filename.toLowerCase().includes(query) ||
116
+ file.contentType.toLowerCase().includes(query) ||
117
+ (file.metadata?.description && file.metadata.description.toLowerCase().includes(query))
118
+ );
119
+ setFilteredFiles(filtered);
120
+ }
121
+ }, [files, searchQuery, viewMode]);
122
+
123
+ // Load photo dimensions for justified grid
124
+ const loadPhotoDimensions = useCallback(async (photos: FileMetadata[]) => {
125
+ if (photos.length === 0) return;
126
+
127
+ setLoadingDimensions(true);
128
+ const newDimensions: {[key: string]: {width: number, height: number}} = { ...photoDimensions };
129
+ let hasNewDimensions = false;
130
+
131
+ // Only load dimensions for photos we don't have yet
132
+ const photosToLoad = photos.filter(photo => !newDimensions[photo.id]);
133
+
134
+ if (photosToLoad.length === 0) {
135
+ setLoadingDimensions(false);
136
+ return;
137
+ }
138
+
139
+ try {
140
+ await Promise.all(
141
+ photosToLoad.map(async (photo) => {
142
+ try {
143
+ const downloadUrl = oxyServices.getFileDownloadUrl(photo.id);
144
+
145
+ if (Platform.OS === 'web') {
146
+ const img = new (window as any).Image();
147
+ await new Promise<void>((resolve, reject) => {
148
+ img.onload = () => {
149
+ newDimensions[photo.id] = {
150
+ width: img.naturalWidth,
151
+ height: img.naturalHeight
152
+ };
153
+ hasNewDimensions = true;
154
+ resolve();
155
+ };
156
+ img.onerror = () => {
157
+ // Fallback dimensions for failed loads
158
+ newDimensions[photo.id] = { width: 1, height: 1 };
159
+ hasNewDimensions = true;
160
+ resolve();
161
+ };
162
+ img.src = downloadUrl;
163
+ });
164
+ } else {
165
+ // For mobile, use Image.getSize from react-native
166
+ await new Promise<void>((resolve) => {
167
+ Image.getSize(
168
+ downloadUrl,
169
+ (width: number, height: number) => {
170
+ newDimensions[photo.id] = { width, height };
171
+ hasNewDimensions = true;
172
+ resolve();
173
+ },
174
+ () => {
175
+ // Fallback dimensions
176
+ newDimensions[photo.id] = { width: 1, height: 1 };
177
+ hasNewDimensions = true;
178
+ resolve();
179
+ }
180
+ );
181
+ });
182
+ }
183
+ } catch (error) {
184
+ // Fallback dimensions for any errors
185
+ newDimensions[photo.id] = { width: 1, height: 1 };
186
+ hasNewDimensions = true;
187
+ }
188
+ })
189
+ );
190
+
191
+ if (hasNewDimensions) {
192
+ setPhotoDimensions(newDimensions);
193
+ }
194
+ } catch (error) {
195
+ console.error('Error loading photo dimensions:', error);
196
+ } finally {
197
+ setLoadingDimensions(false);
198
+ }
199
+ }, [oxyServices, photoDimensions]);
200
+
201
+ // Create justified rows from photos with responsive algorithm
202
+ const createJustifiedRows = useCallback((photos: FileMetadata[]) => {
203
+ if (photos.length === 0) return [];
204
+
205
+ const rows: FileMetadata[][] = [];
206
+ const photosPerRow = 3; // Fixed 3 photos per row for consistency
207
+
208
+ for (let i = 0; i < photos.length; i += photosPerRow) {
209
+ const rowPhotos = photos.slice(i, i + photosPerRow);
210
+ rows.push(rowPhotos);
211
+ }
212
+
213
+ return rows;
214
+ }, []);
215
+
216
+ const processFileUploads = async (selectedFiles: File[]) => {
217
+ if (selectedFiles.length === 0) return;
218
+
219
+ try {
220
+ // Show initial progress
221
+ setUploadProgress({ current: 0, total: selectedFiles.length });
222
+
223
+ // Validate file sizes (example: 50MB limit per file)
224
+ const maxSize = 50 * 1024 * 1024; // 50MB
225
+ const oversizedFiles = selectedFiles.filter(file => file.size > maxSize);
226
+
227
+ if (oversizedFiles.length > 0) {
228
+ const fileList = oversizedFiles.map(f => f.name).join('\n');
229
+ window.alert(`File Size Limit\n\nThe following files are too large (max 50MB):\n${fileList}`);
230
+ return;
231
+ }
232
+
233
+ // Option 1: Bulk upload (faster, all-or-nothing) for 5 or fewer files
234
+ if (selectedFiles.length <= 5) {
235
+ const filenames = selectedFiles.map(f => f.name);
236
+ const response = await oxyServices.uploadFiles(
237
+ selectedFiles,
238
+ filenames,
239
+ {
240
+ userId: targetUserId,
241
+ uploadDate: new Date().toISOString(),
242
+ }
243
+ );
244
+
245
+ toast.success(`${response.files.length} file(s) uploaded successfully`);
246
+ // Small delay to ensure backend processing is complete
247
+ setTimeout(async () => {
248
+ await loadFiles();
249
+ }, 500);
250
+ } else {
251
+ // Option 2: Individual uploads for better progress and error handling
252
+ let successCount = 0;
253
+ let failureCount = 0;
254
+ const errors: string[] = [];
255
+
256
+ for (let i = 0; i < selectedFiles.length; i++) {
257
+ const file = selectedFiles[i];
258
+ setUploadProgress({ current: i + 1, total: selectedFiles.length });
259
+
260
+ try {
261
+ await oxyServices.uploadFile(file, file.name, {
262
+ userId: targetUserId,
263
+ uploadDate: new Date().toISOString(),
264
+ });
265
+ successCount++;
266
+ } catch (error: any) {
267
+ failureCount++;
268
+ errors.push(`${file.name}: ${error.message || 'Upload failed'}`);
269
+ }
270
+ }
271
+
272
+ // Show results summary
273
+ if (successCount > 0) {
274
+ toast.success(`${successCount} file(s) uploaded successfully`);
275
+ }
276
+
277
+ if (failureCount > 0) {
278
+ const errorMessage = `${failureCount} file(s) failed to upload${errors.length > 0 ? ':\n' + errors.slice(0, 3).join('\n') + (errors.length > 3 ? '\n...' : '') : ''}`;
279
+ toast.error(errorMessage);
280
+ }
281
+
282
+ // Small delay to ensure backend processing is complete
283
+ setTimeout(async () => {
284
+ await loadFiles();
285
+ }, 500);
286
+ }
287
+ } catch (error: any) {
288
+ console.error('Upload error:', error);
289
+ toast.error(error.message || 'Failed to upload files');
290
+ } finally {
291
+ setUploadProgress(null);
292
+ }
293
+ };
294
+
295
+ const handleFileUpload = async () => {
296
+ try {
297
+ setUploading(true);
298
+ setUploadProgress(null);
299
+
300
+ if (Platform.OS === 'web') {
301
+ // Web file picker implementation
302
+ const input = document.createElement('input');
303
+ input.type = 'file';
304
+ input.multiple = true;
305
+ input.accept = '*/*';
306
+
307
+ input.onchange = async (e: any) => {
308
+ const selectedFiles = Array.from(e.target.files) as File[];
309
+ await processFileUploads(selectedFiles);
310
+ };
311
+
312
+ input.click();
313
+ } else {
314
+ // Mobile - show info that file picker can be added
315
+ const installCommand = 'npm install expo-document-picker';
316
+ 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.`;
317
+
318
+ if (window.confirm(`${message}\n\nWould you like to copy the install command?`)) {
319
+ toast.info(`Install: ${installCommand}`);
320
+ } else {
321
+ toast.info('Mobile file upload requires expo-document-picker');
322
+ }
323
+ }
324
+ } catch (error: any) {
325
+ toast.error(error.message || 'Failed to upload file');
326
+ } finally {
327
+ setUploading(false);
328
+ setUploadProgress(null);
329
+ }
330
+ };
331
+
332
+ const handleFileDelete = async (fileId: string, filename: string) => {
333
+ // Use web-compatible confirmation dialog
334
+ const confirmed = window.confirm(`Are you sure you want to delete "${filename}"? This action cannot be undone.`);
335
+
336
+ if (!confirmed) {
337
+ console.log('Delete cancelled by user');
338
+ return;
339
+ }
340
+
341
+ try {
342
+ console.log('Deleting file:', { fileId, filename });
343
+ console.log('Target user ID:', targetUserId);
344
+ console.log('Current user ID:', user?.id);
345
+ setDeleting(fileId);
346
+
347
+ const result = await oxyServices.deleteFile(fileId);
348
+ console.log('Delete result:', result);
349
+
350
+ toast.success('File deleted successfully');
351
+
352
+ // Reload files after successful deletion
353
+ setTimeout(async () => {
354
+ await loadFiles();
355
+ }, 500);
356
+ } catch (error: any) {
357
+ console.error('Delete error:', error);
358
+ console.error('Error details:', error.response?.data || error.message);
359
+
360
+ // Provide specific error messages
361
+ if (error.message?.includes('File not found') || error.message?.includes('404')) {
362
+ toast.error('File not found. It may have already been deleted.');
363
+ // Still reload files to refresh the list
364
+ setTimeout(async () => {
365
+ await loadFiles();
366
+ }, 500);
367
+ } else if (error.message?.includes('permission') || error.message?.includes('403')) {
368
+ toast.error('You do not have permission to delete this file.');
369
+ } else {
370
+ toast.error(error.message || 'Failed to delete file');
371
+ }
372
+ } finally {
373
+ setDeleting(null);
374
+ }
375
+ };
376
+
377
+ // Drag and drop handlers for web
378
+ const handleDragOver = (e: any) => {
379
+ if (Platform.OS === 'web' && user?.id === targetUserId) {
380
+ e.preventDefault();
381
+ setIsDragging(true);
382
+ }
383
+ };
384
+
385
+ const handleDragLeave = (e: any) => {
386
+ if (Platform.OS === 'web') {
387
+ e.preventDefault();
388
+ setIsDragging(false);
389
+ }
390
+ };
391
+
392
+ const handleDrop = async (e: any) => {
393
+ if (Platform.OS === 'web' && user?.id === targetUserId) {
394
+ e.preventDefault();
395
+ setIsDragging(false);
396
+ setUploading(true);
397
+
398
+ try {
399
+ const files = Array.from(e.dataTransfer.files) as File[];
400
+ await processFileUploads(files);
401
+ } catch (error: any) {
402
+ toast.error(error.message || 'Failed to upload files');
403
+ } finally {
404
+ setUploading(false);
405
+ }
406
+ }
407
+ };
408
+
409
+ const handleFileDownload = async (fileId: string, filename: string) => {
410
+ try {
411
+ if (Platform.OS === 'web') {
412
+ console.log('Downloading file:', { fileId, filename });
413
+
414
+ // Use the public download URL method
415
+ const downloadUrl = oxyServices.getFileDownloadUrl(fileId);
416
+ console.log('Download URL:', downloadUrl);
417
+
418
+ try {
419
+ // Method 1: Try simple link download first
420
+ const link = document.createElement('a');
421
+ link.href = downloadUrl;
422
+ link.download = filename;
423
+ link.target = '_blank';
424
+ document.body.appendChild(link);
425
+ link.click();
426
+ document.body.removeChild(link);
427
+
428
+ toast.success('File download started');
429
+ } catch (linkError) {
430
+ console.warn('Link download failed, trying fetch method:', linkError);
431
+
432
+ // Method 2: Fallback to fetch download
433
+ const response = await fetch(downloadUrl);
434
+ if (!response.ok) {
435
+ if (response.status === 404) {
436
+ throw new Error('File not found. It may have been deleted.');
437
+ } else {
438
+ throw new Error(`Download failed: ${response.status} ${response.statusText}`);
439
+ }
440
+ }
441
+
442
+ const blob = await response.blob();
443
+ const url = window.URL.createObjectURL(blob);
444
+
445
+ const link = document.createElement('a');
446
+ link.href = url;
447
+ link.download = filename;
448
+ document.body.appendChild(link);
449
+ link.click();
450
+ document.body.removeChild(link);
451
+
452
+ // Clean up the blob URL
453
+ window.URL.revokeObjectURL(url);
454
+
455
+ toast.success('File downloaded successfully');
456
+ }
457
+ } else {
458
+ toast.info('File download not implemented for mobile yet');
459
+ }
460
+ } catch (error: any) {
461
+ console.error('Download error:', error);
462
+ toast.error(error.message || 'Failed to download file');
463
+ }
464
+ };
465
+
466
+ const formatFileSize = (bytes: number): string => {
467
+ if (bytes === 0) return '0 Bytes';
468
+ const k = 1024;
469
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
470
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
471
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
472
+ };
473
+
474
+ const getFileIcon = (contentType: string): string => {
475
+ if (contentType.startsWith('image/')) return 'image';
476
+ if (contentType.startsWith('video/')) return 'videocam';
477
+ if (contentType.startsWith('audio/')) return 'musical-notes';
478
+ if (contentType.includes('pdf')) return 'document-text';
479
+ if (contentType.includes('word') || contentType.includes('doc')) return 'document';
480
+ if (contentType.includes('excel') || contentType.includes('sheet')) return 'grid';
481
+ if (contentType.includes('zip') || contentType.includes('archive')) return 'archive';
482
+ return 'document-outline';
483
+ };
484
+
485
+ const handleFileOpen = async (file: FileMetadata) => {
486
+ try {
487
+ setLoadingFileContent(true);
488
+ setOpenedFile(file);
489
+
490
+ // For text files, images, and other viewable content, try to load the content
491
+ if (file.contentType.startsWith('text/') ||
492
+ file.contentType.includes('json') ||
493
+ file.contentType.includes('xml') ||
494
+ file.contentType.includes('javascript') ||
495
+ file.contentType.includes('typescript') ||
496
+ file.contentType.startsWith('image/') ||
497
+ file.contentType.includes('pdf') ||
498
+ file.contentType.startsWith('video/') ||
499
+ file.contentType.startsWith('audio/')) {
500
+
501
+ try {
502
+ const downloadUrl = oxyServices.getFileDownloadUrl(file.id);
503
+ const response = await fetch(downloadUrl);
504
+
505
+ if (response.ok) {
506
+ if (file.contentType.startsWith('image/') ||
507
+ file.contentType.includes('pdf') ||
508
+ file.contentType.startsWith('video/') ||
509
+ file.contentType.startsWith('audio/')) {
510
+ // For images, PDFs, videos, and audio, we'll use the URL directly
511
+ setFileContent(downloadUrl);
512
+ } else {
513
+ // For text files, get the content
514
+ const content = await response.text();
515
+ setFileContent(content);
516
+ }
517
+ } else {
518
+ if (response.status === 404) {
519
+ toast.error('File not found. It may have been deleted.');
520
+ } else {
521
+ toast.error(`Failed to load file: ${response.status} ${response.statusText}`);
522
+ }
523
+ setFileContent(null);
524
+ }
525
+ } catch (error: any) {
526
+ console.error('Failed to load file content:', error);
527
+ if (error.message?.includes('404') || error.message?.includes('not found')) {
528
+ toast.error('File not found. It may have been deleted.');
529
+ } else {
530
+ toast.error('Failed to load file content');
531
+ }
532
+ setFileContent(null);
533
+ }
534
+ } else {
535
+ // For non-viewable files, don't load content
536
+ setFileContent(null);
537
+ }
538
+ } catch (error: any) {
539
+ console.error('Failed to open file:', error);
540
+ toast.error(error.message || 'Failed to open file');
541
+ } finally {
542
+ setLoadingFileContent(false);
543
+ }
544
+ };
545
+
546
+ const handleCloseFile = () => {
547
+ setOpenedFile(null);
548
+ setFileContent(null);
549
+ setShowFileDetailsInViewer(false);
550
+ // Don't reset view mode when closing a file
551
+ };
552
+
553
+ const showFileDetailsModal = (file: FileMetadata) => {
554
+ setSelectedFile(file);
555
+ setShowFileDetails(true);
556
+ };
557
+
558
+ const renderSimplePhotoItem = useCallback((photo: FileMetadata, index: number) => {
559
+ const downloadUrl = oxyServices.getFileDownloadUrl(photo.id);
560
+
561
+ // Calculate photo item width based on actual container size from bottom sheet
562
+ let itemsPerRow = 3; // Default for mobile
563
+ if (containerWidth > 768) itemsPerRow = 4; // Desktop/tablet
564
+ else if (containerWidth > 480) itemsPerRow = 3; // Large mobile
565
+
566
+ // Account for the photoScrollContainer padding (16px on each side = 32px total)
567
+ const scrollContainerPadding = 32; // Total horizontal padding from photoScrollContainer
568
+ const gaps = (itemsPerRow - 1) * 4; // Gap between items (4px)
569
+ const availableWidth = containerWidth - scrollContainerPadding;
570
+ const itemWidth = (availableWidth - gaps) / itemsPerRow;
571
+
572
+ return (
573
+ <TouchableOpacity
574
+ key={photo.id}
575
+ style={[
576
+ styles.simplePhotoItem,
577
+ {
578
+ width: itemWidth,
579
+ height: itemWidth,
580
+ marginRight: (index + 1) % itemsPerRow === 0 ? 0 : 4,
581
+ }
582
+ ]}
583
+ onPress={() => handleFileOpen(photo)}
584
+ activeOpacity={0.8}
585
+ >
586
+ <View style={styles.simplePhotoContainer}>
587
+ {Platform.OS === 'web' ? (
588
+ <img
589
+ src={downloadUrl}
590
+ alt={photo.filename}
591
+ style={{
592
+ width: '100%',
593
+ height: '100%',
594
+ objectFit: 'cover',
595
+ borderRadius: 8,
596
+ transition: 'transform 0.2s ease',
597
+ }}
598
+ loading="lazy"
599
+ onError={(e) => {
600
+ console.error('Photo failed to load:', e);
601
+ }}
602
+ onMouseEnter={(e) => {
603
+ e.currentTarget.style.transform = 'scale(1.05)';
604
+ }}
605
+ onMouseLeave={(e) => {
606
+ e.currentTarget.style.transform = 'scale(1)';
607
+ }}
608
+ />
609
+ ) : (
610
+ <Image
611
+ source={{ uri: downloadUrl }}
612
+ style={styles.simplePhotoImage}
613
+ resizeMode="cover"
614
+ onError={(e) => {
615
+ console.error('Photo failed to load:', e);
616
+ }}
617
+ />
618
+ )}
619
+ </View>
620
+ </TouchableOpacity>
621
+ );
622
+ }, [oxyServices, containerWidth]);
623
+
624
+ const renderJustifiedPhotoItem = useCallback((photo: FileMetadata, width: number, height: number, isLast: boolean) => {
625
+ const downloadUrl = oxyServices.getFileDownloadUrl(photo.id);
626
+
627
+ return (
628
+ <TouchableOpacity
629
+ key={photo.id}
630
+ style={[
631
+ styles.justifiedPhotoItem,
632
+ {
633
+ width,
634
+ height,
635
+ }
636
+ ]}
637
+ onPress={() => handleFileOpen(photo)}
638
+ activeOpacity={0.8}
639
+ >
640
+ <View style={styles.justifiedPhotoContainer}>
641
+ {Platform.OS === 'web' ? (
642
+ <img
643
+ src={downloadUrl}
644
+ alt={photo.filename}
645
+ style={{
646
+ width: '100%',
647
+ height: '100%',
648
+ objectFit: 'cover',
649
+ borderRadius: 6,
650
+ transition: 'transform 0.2s ease, box-shadow 0.2s ease',
651
+ }}
652
+ loading="lazy"
653
+ onError={(e) => {
654
+ console.error('Photo failed to load:', e);
655
+ }}
656
+ onMouseEnter={(e) => {
657
+ e.currentTarget.style.transform = 'scale(1.02)';
658
+ e.currentTarget.style.boxShadow = '0 8px 25px rgba(0,0,0,0.15)';
659
+ e.currentTarget.style.zIndex = '10';
660
+ }}
661
+ onMouseLeave={(e) => {
662
+ e.currentTarget.style.transform = 'scale(1)';
663
+ e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
664
+ e.currentTarget.style.zIndex = '1';
665
+ }}
666
+ />
667
+ ) : (
668
+ <Image
669
+ source={{ uri: downloadUrl }}
670
+ style={styles.justifiedPhotoImage}
671
+ resizeMode="cover"
672
+ onError={(e) => {
673
+ console.error('Photo failed to load:', e);
674
+ }}
675
+ />
676
+ )}
677
+ </View>
678
+ </TouchableOpacity>
679
+ );
680
+ }, [oxyServices]);
681
+
682
+ useEffect(() => {
683
+ loadFiles();
684
+ }, [loadFiles]);
685
+
686
+ const renderFileItem = (file: FileMetadata) => {
687
+ const isImage = file.contentType.startsWith('image/');
688
+ const isPDF = file.contentType.includes('pdf');
689
+ const isVideo = file.contentType.startsWith('video/');
690
+ const isAudio = file.contentType.startsWith('audio/');
691
+ const hasPreview = isImage || isPDF || isVideo;
692
+
693
+ return (
694
+ <View
695
+ key={file.id}
696
+ style={[styles.fileItem, { backgroundColor: secondaryBackgroundColor, borderColor }]}
697
+ >
698
+ <TouchableOpacity
699
+ style={styles.fileContent}
700
+ onPress={() => handleFileOpen(file)}
701
+ >
702
+ {/* Preview Thumbnail */}
703
+ <View style={styles.filePreviewContainer}>
704
+ {hasPreview ? (
705
+ <View
706
+ style={styles.filePreview}
707
+ {...(Platform.OS === 'web' && {
708
+ onMouseEnter: () => setHoveredPreview(file.id),
709
+ onMouseLeave: () => setHoveredPreview(null),
710
+ })}
711
+ >
712
+ {isImage && (
713
+ Platform.OS === 'web' ? (
714
+ <img
715
+ src={oxyServices.getFileDownloadUrl(file.id)}
716
+ style={{
717
+ width: '100%',
718
+ height: '100%',
719
+ objectFit: 'cover',
720
+ borderRadius: 8,
721
+ transition: 'transform 0.2s ease',
722
+ transform: hoveredPreview === file.id ? 'scale(1.05)' : 'scale(1)',
723
+ }}
724
+ onError={(e) => {
725
+ // Show fallback icon if image fails to load
726
+ e.currentTarget.style.display = 'none';
727
+ const fallbackElement = e.currentTarget.parentElement?.querySelector('[data-fallback="true"]');
728
+ if (fallbackElement) {
729
+ (fallbackElement as HTMLElement).style.display = 'flex';
730
+ }
731
+ }}
732
+ />
733
+ ) : (
734
+ <Image
735
+ source={{ uri: oxyServices.getFileDownloadUrl(file.id) }}
736
+ style={styles.previewImage}
737
+ resizeMode="cover"
738
+ onError={() => {
739
+ // For React Native, you might want to set an error state
740
+ console.warn('Failed to load image preview for file:', file.id);
741
+ }}
742
+ />
743
+ )
744
+ )}
745
+ {isPDF && (
746
+ <View style={styles.pdfPreview}>
747
+ <Ionicons name="document" size={32} color={primaryColor} />
748
+ <Text style={[styles.pdfLabel, { color: primaryColor }]}>PDF</Text>
749
+ </View>
750
+ )}
751
+ {isVideo && (
752
+ <View style={styles.videoPreview}>
753
+ <Ionicons name="play-circle" size={32} color={primaryColor} />
754
+ <Text style={[styles.videoLabel, { color: primaryColor }]}>VIDEO</Text>
755
+ </View>
756
+ )}
757
+ {/* Fallback icon (hidden by default for images) */}
758
+ <View
759
+ style={[styles.fallbackIcon, { display: isImage ? 'none' : 'flex' }]}
760
+ {...(Platform.OS === 'web' && { 'data-fallback': 'true' })}
761
+ >
762
+ <Ionicons
763
+ name={getFileIcon(file.contentType) as any}
764
+ size={32}
765
+ color={primaryColor}
766
+ />
767
+ </View>
768
+
769
+ {/* Preview overlay for hover effect */}
770
+ {Platform.OS === 'web' && hoveredPreview === file.id && isImage && (
771
+ <View style={styles.previewOverlay}>
772
+ <Ionicons name="eye" size={24} color="#FFFFFF" />
773
+ </View>
774
+ )}
775
+ </View>
776
+ ) : (
777
+ <View style={styles.fileIconContainer}>
778
+ <Ionicons
779
+ name={getFileIcon(file.contentType) as any}
780
+ size={32}
781
+ color={primaryColor}
782
+ />
783
+ </View>
784
+ )}
785
+ </View>
786
+
787
+ <View style={styles.fileInfo}>
788
+ <Text style={[styles.fileName, { color: textColor }]} numberOfLines={1}>
789
+ {file.filename}
790
+ </Text>
791
+ <Text style={[styles.fileDetails, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
792
+ {formatFileSize(file.length)} • {new Date(file.uploadDate).toLocaleDateString()}
793
+ </Text>
794
+ {file.metadata?.description && (
795
+ <Text
796
+ style={[styles.fileDescription, { color: isDarkTheme ? '#AAAAAA' : '#888888' }]}
797
+ numberOfLines={2}
798
+ >
799
+ {file.metadata.description}
800
+ </Text>
801
+ )}
802
+ </View>
803
+ </TouchableOpacity>
804
+
805
+ <View style={styles.fileActions}>
806
+ {/* Preview button for supported files */}
807
+ {hasPreview && (
808
+ <TouchableOpacity
809
+ style={[styles.actionButton, { backgroundColor: isDarkTheme ? '#333333' : '#F0F0F0' }]}
810
+ onPress={() => handleFileOpen(file)}
811
+ >
812
+ <Ionicons name="eye" size={20} color={primaryColor} />
813
+ </TouchableOpacity>
814
+ )}
815
+
816
+ <TouchableOpacity
817
+ style={[styles.actionButton, { backgroundColor: isDarkTheme ? '#333333' : '#F0F0F0' }]}
818
+ onPress={() => handleFileDownload(file.id, file.filename)}
819
+ >
820
+ <Ionicons name="download" size={20} color={primaryColor} />
821
+ </TouchableOpacity>
822
+
823
+ {/* Always show delete button for debugging */}
824
+ <TouchableOpacity
825
+ style={[styles.actionButton, { backgroundColor: isDarkTheme ? '#400000' : '#FFEBEE' }]}
826
+ onPress={() => {
827
+ handleFileDelete(file.id, file.filename);
828
+ }}
829
+ disabled={deleting === file.id}
830
+ >
831
+ {deleting === file.id ? (
832
+ <ActivityIndicator size="small" color={dangerColor} />
833
+ ) : (
834
+ <Ionicons name="trash" size={20} color={dangerColor} />
835
+ )}
836
+ </TouchableOpacity>
837
+ </View>
838
+ </View>
839
+ );
840
+ };
841
+
842
+ const renderPhotoGrid = useCallback(() => {
843
+ const photos = filteredFiles.filter(file => file.contentType.startsWith('image/'));
844
+
845
+ if (photos.length === 0) {
846
+ return (
847
+ <View style={styles.emptyState}>
848
+ <Ionicons name="images-outline" size={64} color={isDarkTheme ? '#666666' : '#CCCCCC'} />
849
+ <Text style={[styles.emptyStateTitle, { color: textColor }]}>No Photos Yet</Text>
850
+ <Text style={[styles.emptyStateDescription, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
851
+ {user?.id === targetUserId
852
+ ? `Upload photos to get started. You can select multiple photos at once${Platform.OS === 'web' ? ' or drag & drop them here.' : '.'}`
853
+ : "This user hasn't uploaded any photos yet"
854
+ }
855
+ </Text>
856
+ {user?.id === targetUserId && (
857
+ <TouchableOpacity
858
+ style={[styles.emptyStateButton, { backgroundColor: primaryColor }]}
859
+ onPress={handleFileUpload}
860
+ disabled={uploading}
861
+ >
862
+ {uploading ? (
863
+ <ActivityIndicator size="small" color="#FFFFFF" />
864
+ ) : (
865
+ <>
866
+ <Ionicons name="cloud-upload" size={20} color="#FFFFFF" />
867
+ <Text style={styles.emptyStateButtonText}>Upload Photos</Text>
868
+ </>
869
+ )}
870
+ </TouchableOpacity>
871
+ )}
872
+ </View>
873
+ );
874
+ }
875
+
876
+ return (
877
+ <ScrollView
878
+ style={styles.scrollView}
879
+ contentContainerStyle={styles.photoScrollContainer}
880
+ refreshControl={
881
+ <RefreshControl
882
+ refreshing={refreshing}
883
+ onRefresh={() => loadFiles(true)}
884
+ tintColor={primaryColor}
885
+ />
886
+ }
887
+ showsVerticalScrollIndicator={false}
888
+ >
889
+ {loadingDimensions && (
890
+ <View style={styles.dimensionsLoadingIndicator}>
891
+ <ActivityIndicator size="small" color={primaryColor} />
892
+ <Text style={[styles.dimensionsLoadingText, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
893
+ Loading photo layout...
894
+ </Text>
895
+ </View>
896
+ )}
897
+
898
+ <JustifiedPhotoGrid
899
+ photos={photos}
900
+ photoDimensions={photoDimensions}
901
+ loadPhotoDimensions={loadPhotoDimensions}
902
+ createJustifiedRows={createJustifiedRows}
903
+ renderJustifiedPhotoItem={renderJustifiedPhotoItem}
904
+ renderSimplePhotoItem={renderPhotoItem}
905
+ textColor={textColor}
906
+ containerWidth={containerWidth}
907
+ />
908
+ </ScrollView>
909
+ );
910
+ }, [filteredFiles, isDarkTheme, textColor, user?.id, targetUserId, uploading, primaryColor, handleFileUpload, refreshing, loadFiles, loadingDimensions, photoDimensions, loadPhotoDimensions, createJustifiedRows, renderJustifiedPhotoItem]);
911
+
912
+ // Separate component for the photo grid to optimize rendering
913
+ const JustifiedPhotoGrid = React.memo(({
914
+ photos,
915
+ photoDimensions,
916
+ loadPhotoDimensions,
917
+ createJustifiedRows,
918
+ renderJustifiedPhotoItem,
919
+ renderSimplePhotoItem,
920
+ textColor,
921
+ containerWidth
922
+ }: {
923
+ photos: FileMetadata[];
924
+ photoDimensions: {[key: string]: {width: number, height: number}};
925
+ loadPhotoDimensions: (photos: FileMetadata[]) => Promise<void>;
926
+ createJustifiedRows: (photos: FileMetadata[], containerWidth: number) => FileMetadata[][];
927
+ renderJustifiedPhotoItem: (photo: FileMetadata, width: number, height: number, isLast: boolean) => JSX.Element;
928
+ renderSimplePhotoItem: (photo: FileMetadata, index: number) => JSX.Element;
929
+ textColor: string;
930
+ containerWidth: number;
931
+ }) => {
932
+ // Load dimensions for new photos
933
+ React.useEffect(() => {
934
+ loadPhotoDimensions(photos);
935
+ }, [photos.map(p => p.id).join(','), loadPhotoDimensions]);
936
+
937
+ // Group photos by date
938
+ const photosByDate = React.useMemo(() => {
939
+ return photos.reduce((groups: {[key: string]: FileMetadata[]}, photo) => {
940
+ const date = new Date(photo.uploadDate).toDateString();
941
+ if (!groups[date]) {
942
+ groups[date] = [];
943
+ }
944
+ groups[date].push(photo);
945
+ return groups;
946
+ }, {});
947
+ }, [photos]);
948
+
949
+ const sortedDates = React.useMemo(() => {
950
+ return Object.keys(photosByDate).sort((a, b) =>
951
+ new Date(b).getTime() - new Date(a).getTime()
952
+ );
953
+ }, [photosByDate]);
954
+
955
+ return (
956
+ <>
957
+ {sortedDates.map(date => {
958
+ const dayPhotos = photosByDate[date];
959
+ const justifiedRows = createJustifiedRows(dayPhotos, containerWidth);
960
+
961
+ return (
962
+ <View key={date} style={styles.photoDateSection}>
963
+ <Text style={[styles.photoDateHeader, { color: textColor }]}>
964
+ {new Date(date).toLocaleDateString('en-US', {
965
+ weekday: 'long',
966
+ year: 'numeric',
967
+ month: 'long',
968
+ day: 'numeric'
969
+ })}
970
+ </Text>
971
+ <View style={styles.justifiedPhotoGrid}>
972
+ {justifiedRows.map((row, rowIndex) => {
973
+ // Calculate row height based on available width
974
+ const gap = 4;
975
+ let totalAspectRatio = 0;
976
+
977
+ // Calculate total aspect ratio for this row
978
+ row.forEach(photo => {
979
+ const dimensions = photoDimensions[photo.id];
980
+ const aspectRatio = dimensions ?
981
+ (dimensions.width / dimensions.height) :
982
+ 1.33; // Default 4:3 ratio
983
+ totalAspectRatio += aspectRatio;
984
+ });
985
+
986
+ // Calculate the height that makes the row fill the available width
987
+ // Account for photoScrollContainer padding (32px total) and gaps between photos
988
+ const scrollContainerPadding = 32;
989
+ const availableWidth = containerWidth - scrollContainerPadding - (gap * (row.length - 1));
990
+ const calculatedHeight = availableWidth / totalAspectRatio;
991
+
992
+ // Clamp height for visual consistency
993
+ const rowHeight = Math.max(120, Math.min(calculatedHeight, 300));
994
+
995
+ return (
996
+ <View
997
+ key={`row-${rowIndex}`}
998
+ style={[
999
+ styles.justifiedPhotoRow,
1000
+ {
1001
+ height: rowHeight,
1002
+ maxWidth: containerWidth - 32, // Account for scroll container padding
1003
+ gap: 4, // Add horizontal gap between photos in row
1004
+ }
1005
+ ]}
1006
+ >
1007
+ {row.map((photo, photoIndex) => {
1008
+ const dimensions = photoDimensions[photo.id];
1009
+ const aspectRatio = dimensions ?
1010
+ (dimensions.width / dimensions.height) :
1011
+ 1.33; // Default 4:3 ratio
1012
+
1013
+ const photoWidth = rowHeight * aspectRatio;
1014
+ const isLast = photoIndex === row.length - 1;
1015
+
1016
+ return renderJustifiedPhotoItem(
1017
+ photo,
1018
+ photoWidth,
1019
+ rowHeight,
1020
+ isLast
1021
+ );
1022
+ })}
1023
+ </View>
1024
+ );
1025
+ })}
1026
+ </View>
1027
+ </View>
1028
+ );
1029
+ })}
1030
+ </>
1031
+ );
1032
+ });
1033
+
1034
+ const renderPhotoItem = (photo: FileMetadata, index: number) => {
1035
+ const downloadUrl = oxyServices.getFileDownloadUrl(photo.id);
1036
+
1037
+ // Calculate photo item width based on actual container size from bottom sheet
1038
+ let itemsPerRow = 3; // Default for mobile
1039
+ if (containerWidth > 768) itemsPerRow = 6; // Tablet/Desktop
1040
+ else if (containerWidth > 480) itemsPerRow = 4; // Large mobile
1041
+
1042
+ // Account for the photoScrollContainer padding (16px on each side = 32px total)
1043
+ const scrollContainerPadding = 32; // Total horizontal padding from photoScrollContainer
1044
+ const gaps = (itemsPerRow - 1) * 4; // Gap between items
1045
+ const availableWidth = containerWidth - scrollContainerPadding;
1046
+ const itemWidth = (availableWidth - gaps) / itemsPerRow;
1047
+
1048
+ return (
1049
+ <TouchableOpacity
1050
+ key={photo.id}
1051
+ style={[
1052
+ styles.photoItem,
1053
+ {
1054
+ width: itemWidth,
1055
+ height: itemWidth,
1056
+ }
1057
+ ]}
1058
+ onPress={() => handleFileOpen(photo)}
1059
+ activeOpacity={0.8}
1060
+ >
1061
+ <View style={styles.photoContainer}>
1062
+ {Platform.OS === 'web' ? (
1063
+ <img
1064
+ src={downloadUrl}
1065
+ alt={photo.filename}
1066
+ style={{
1067
+ width: '100%',
1068
+ height: '100%',
1069
+ objectFit: 'cover',
1070
+ borderRadius: 8,
1071
+ transition: 'transform 0.2s ease',
1072
+ }}
1073
+ loading="lazy"
1074
+ onError={(e) => {
1075
+ console.error('Photo failed to load:', e);
1076
+ // Could replace with placeholder image
1077
+ }}
1078
+ onMouseEnter={(e) => {
1079
+ e.currentTarget.style.transform = 'scale(1.02)';
1080
+ }}
1081
+ onMouseLeave={(e) => {
1082
+ e.currentTarget.style.transform = 'scale(1)';
1083
+ }}
1084
+ />
1085
+ ) : (
1086
+ <Image
1087
+ source={{ uri: downloadUrl }}
1088
+ style={styles.photoImage}
1089
+ resizeMode="cover"
1090
+ onError={(e) => {
1091
+ console.error('Photo failed to load:', e);
1092
+ }}
1093
+ />
1094
+ )}
1095
+ </View>
1096
+ </TouchableOpacity>
1097
+ );
1098
+ };
1099
+
1100
+ const renderFileDetailsModal = () => (
1101
+ <Modal
1102
+ visible={showFileDetails}
1103
+ animationType="slide"
1104
+ presentationStyle="pageSheet"
1105
+ onRequestClose={() => setShowFileDetails(false)}
1106
+ >
1107
+ <View style={[styles.modalContainer, { backgroundColor }]}>
1108
+ <View style={[styles.modalHeader, { borderBottomColor: borderColor }]}>
1109
+ <TouchableOpacity
1110
+ style={styles.modalCloseButton}
1111
+ onPress={() => setShowFileDetails(false)}
1112
+ >
1113
+ <Ionicons name="close" size={24} color={textColor} />
1114
+ </TouchableOpacity>
1115
+ <Text style={[styles.modalTitle, { color: textColor }]}>File Details</Text>
1116
+ <View style={styles.modalPlaceholder} />
1117
+ </View>
1118
+
1119
+ {selectedFile && (
1120
+ <ScrollView style={styles.modalContent}>
1121
+ <View style={[styles.fileDetailCard, { backgroundColor: secondaryBackgroundColor, borderColor }]}>
1122
+ <View style={styles.fileDetailIcon}>
1123
+ <Ionicons
1124
+ name={getFileIcon(selectedFile.contentType) as any}
1125
+ size={64}
1126
+ color={primaryColor}
1127
+ />
1128
+ </View>
1129
+
1130
+ <Text style={[styles.fileDetailName, { color: textColor }]}>
1131
+ {selectedFile.filename}
1132
+ </Text>
1133
+
1134
+ <View style={styles.fileDetailInfo}>
1135
+ <View style={styles.detailRow}>
1136
+ <Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1137
+ Size:
1138
+ </Text>
1139
+ <Text style={[styles.detailValue, { color: textColor }]}>
1140
+ {formatFileSize(selectedFile.length)}
1141
+ </Text>
1142
+ </View>
1143
+
1144
+ <View style={styles.detailRow}>
1145
+ <Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1146
+ Type:
1147
+ </Text>
1148
+ <Text style={[styles.detailValue, { color: textColor }]}>
1149
+ {selectedFile.contentType}
1150
+ </Text>
1151
+ </View>
1152
+
1153
+ <View style={styles.detailRow}>
1154
+ <Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1155
+ Uploaded:
1156
+ </Text>
1157
+ <Text style={[styles.detailValue, { color: textColor }]}>
1158
+ {new Date(selectedFile.uploadDate).toLocaleString()}
1159
+ </Text>
1160
+ </View>
1161
+
1162
+ {selectedFile.metadata?.description && (
1163
+ <View style={styles.detailRow}>
1164
+ <Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1165
+ Description:
1166
+ </Text>
1167
+ <Text style={[styles.detailValue, { color: textColor }]}>
1168
+ {selectedFile.metadata.description}
1169
+ </Text>
1170
+ </View>
1171
+ )}
1172
+ </View>
1173
+
1174
+ <View style={styles.modalActions}>
1175
+ <TouchableOpacity
1176
+ style={[styles.modalActionButton, { backgroundColor: primaryColor }]}
1177
+ onPress={() => {
1178
+ handleFileDownload(selectedFile.id, selectedFile.filename);
1179
+ setShowFileDetails(false);
1180
+ }}
1181
+ >
1182
+ <Ionicons name="download" size={20} color="#FFFFFF" />
1183
+ <Text style={styles.modalActionText}>Download</Text>
1184
+ </TouchableOpacity>
1185
+
1186
+ {(user?.id === targetUserId) && (
1187
+ <TouchableOpacity
1188
+ style={[styles.modalActionButton, { backgroundColor: dangerColor }]}
1189
+ onPress={() => {
1190
+ setShowFileDetails(false);
1191
+ handleFileDelete(selectedFile.id, selectedFile.filename);
1192
+ }}
1193
+ >
1194
+ <Ionicons name="trash" size={20} color="#FFFFFF" />
1195
+ <Text style={styles.modalActionText}>Delete</Text>
1196
+ </TouchableOpacity>
1197
+ )}
1198
+ </View>
1199
+ </View>
1200
+ </ScrollView>
1201
+ )}
1202
+ </View>
1203
+ </Modal>
1204
+ );
1205
+
1206
+ const renderFileViewer = () => {
1207
+ if (!openedFile) return null;
1208
+
1209
+ const isImage = openedFile.contentType.startsWith('image/');
1210
+ const isText = openedFile.contentType.startsWith('text/') ||
1211
+ openedFile.contentType.includes('json') ||
1212
+ openedFile.contentType.includes('xml') ||
1213
+ openedFile.contentType.includes('javascript') ||
1214
+ openedFile.contentType.includes('typescript');
1215
+ const isPDF = openedFile.contentType.includes('pdf');
1216
+ const isVideo = openedFile.contentType.startsWith('video/');
1217
+ const isAudio = openedFile.contentType.startsWith('audio/');
1218
+
1219
+ return (
1220
+ <View style={[styles.fileViewerContainer, { backgroundColor }]}>
1221
+ {/* File Viewer Header */}
1222
+ <View style={[styles.fileViewerHeader, { borderBottomColor: borderColor }]}>
1223
+ <TouchableOpacity
1224
+ style={styles.backButton}
1225
+ onPress={handleCloseFile}
1226
+ >
1227
+ <Ionicons name="arrow-back" size={24} color={textColor} />
1228
+ </TouchableOpacity>
1229
+ <View style={styles.fileViewerTitleContainer}>
1230
+ <Text style={[styles.fileViewerTitle, { color: textColor }]} numberOfLines={1}>
1231
+ {openedFile.filename}
1232
+ </Text>
1233
+ <Text style={[styles.fileViewerSubtitle, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1234
+ {formatFileSize(openedFile.length)} • {openedFile.contentType}
1235
+ </Text>
1236
+ </View>
1237
+ <View style={styles.fileViewerActions}>
1238
+ <TouchableOpacity
1239
+ style={[styles.actionButton, { backgroundColor: isDarkTheme ? '#333333' : '#F0F0F0' }]}
1240
+ onPress={() => handleFileDownload(openedFile.id, openedFile.filename)}
1241
+ >
1242
+ <Ionicons name="download" size={20} color={primaryColor} />
1243
+ </TouchableOpacity>
1244
+ <TouchableOpacity
1245
+ style={[
1246
+ styles.actionButton,
1247
+ {
1248
+ backgroundColor: showFileDetailsInViewer
1249
+ ? primaryColor
1250
+ : (isDarkTheme ? '#333333' : '#F0F0F0')
1251
+ }
1252
+ ]}
1253
+ onPress={() => setShowFileDetailsInViewer(!showFileDetailsInViewer)}
1254
+ >
1255
+ <Ionicons
1256
+ name={showFileDetailsInViewer ? "chevron-up" : "information-circle"}
1257
+ size={20}
1258
+ color={showFileDetailsInViewer ? "#FFFFFF" : primaryColor}
1259
+ />
1260
+ </TouchableOpacity>
1261
+ </View>
1262
+ </View>
1263
+
1264
+ {/* File Details Section */}
1265
+ {showFileDetailsInViewer && (
1266
+ <View style={[styles.fileDetailsSection, { backgroundColor: secondaryBackgroundColor, borderColor }]}>
1267
+ <View style={styles.fileDetailsSectionHeader}>
1268
+ <Text style={[styles.fileDetailsSectionTitle, { color: textColor }]}>
1269
+ File Details
1270
+ </Text>
1271
+ <TouchableOpacity
1272
+ style={styles.fileDetailsSectionToggle}
1273
+ onPress={() => setShowFileDetailsInViewer(false)}
1274
+ >
1275
+ <Ionicons name="chevron-up" size={20} color={isDarkTheme ? '#BBBBBB' : '#666666'} />
1276
+ </TouchableOpacity>
1277
+ </View>
1278
+
1279
+ <View style={styles.fileDetailInfo}>
1280
+ <View style={styles.detailRow}>
1281
+ <Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1282
+ File Name:
1283
+ </Text>
1284
+ <Text style={[styles.detailValue, { color: textColor }]}>
1285
+ {openedFile.filename}
1286
+ </Text>
1287
+ </View>
1288
+
1289
+ <View style={styles.detailRow}>
1290
+ <Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1291
+ Size:
1292
+ </Text>
1293
+ <Text style={[styles.detailValue, { color: textColor }]}>
1294
+ {formatFileSize(openedFile.length)}
1295
+ </Text>
1296
+ </View>
1297
+
1298
+ <View style={styles.detailRow}>
1299
+ <Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1300
+ Type:
1301
+ </Text>
1302
+ <Text style={[styles.detailValue, { color: textColor }]}>
1303
+ {openedFile.contentType}
1304
+ </Text>
1305
+ </View>
1306
+
1307
+ <View style={styles.detailRow}>
1308
+ <Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1309
+ Uploaded:
1310
+ </Text>
1311
+ <Text style={[styles.detailValue, { color: textColor }]}>
1312
+ {new Date(openedFile.uploadDate).toLocaleString()}
1313
+ </Text>
1314
+ </View>
1315
+
1316
+ {openedFile.metadata?.description && (
1317
+ <View style={styles.detailRow}>
1318
+ <Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1319
+ Description:
1320
+ </Text>
1321
+ <Text style={[styles.detailValue, { color: textColor }]}>
1322
+ {openedFile.metadata.description}
1323
+ </Text>
1324
+ </View>
1325
+ )}
1326
+
1327
+ <View style={styles.detailRow}>
1328
+ <Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1329
+ File ID:
1330
+ </Text>
1331
+ <Text style={[styles.detailValue, { color: textColor, fontSize: 12, fontFamily: Platform.OS === 'web' ? 'monospace' : 'Courier' }]}>
1332
+ {openedFile.id}
1333
+ </Text>
1334
+ </View>
1335
+ </View>
1336
+
1337
+ <View style={styles.fileDetailsActions}>
1338
+ <TouchableOpacity
1339
+ style={[styles.fileDetailsActionButton, { backgroundColor: primaryColor }]}
1340
+ onPress={() => handleFileDownload(openedFile.id, openedFile.filename)}
1341
+ >
1342
+ <Ionicons name="download" size={16} color="#FFFFFF" />
1343
+ <Text style={styles.fileDetailsActionText}>Download</Text>
1344
+ </TouchableOpacity>
1345
+
1346
+ {(user?.id === targetUserId) && (
1347
+ <TouchableOpacity
1348
+ style={[styles.fileDetailsActionButton, { backgroundColor: dangerColor }]}
1349
+ onPress={() => {
1350
+ handleCloseFile();
1351
+ handleFileDelete(openedFile.id, openedFile.filename);
1352
+ }}
1353
+ >
1354
+ <Ionicons name="trash" size={16} color="#FFFFFF" />
1355
+ <Text style={styles.fileDetailsActionText}>Delete</Text>
1356
+ </TouchableOpacity>
1357
+ )}
1358
+ </View>
1359
+ </View>
1360
+ )}
1361
+
1362
+ {/* File Content */}
1363
+ <ScrollView
1364
+ style={[
1365
+ styles.fileViewerContent,
1366
+ showFileDetailsInViewer && styles.fileViewerContentWithDetails
1367
+ ]}
1368
+ contentContainerStyle={styles.fileViewerContentContainer}
1369
+ >
1370
+ {loadingFileContent ? (
1371
+ <View style={styles.fileViewerLoading}>
1372
+ <ActivityIndicator size="large" color={primaryColor} />
1373
+ <Text style={[styles.fileViewerLoadingText, { color: textColor }]}>
1374
+ Loading file content...
1375
+ </Text>
1376
+ </View>
1377
+ ) : isImage && fileContent ? (
1378
+ <View style={styles.imageContainer}>
1379
+ {Platform.OS === 'web' ? (
1380
+ <img
1381
+ src={fileContent}
1382
+ alt={openedFile.filename}
1383
+ style={{
1384
+ maxWidth: '100%',
1385
+ maxHeight: '80vh',
1386
+ objectFit: 'contain',
1387
+ borderRadius: 8,
1388
+ }}
1389
+ onError={(e) => {
1390
+ console.error('Image failed to load:', e);
1391
+ }}
1392
+ />
1393
+ ) : (
1394
+ <Image
1395
+ source={{ uri: fileContent }}
1396
+ style={{
1397
+ width: '100%',
1398
+ height: 400,
1399
+ resizeMode: 'contain',
1400
+ borderRadius: 8,
1401
+ }}
1402
+ onError={(e) => {
1403
+ console.error('Image failed to load:', e);
1404
+ }}
1405
+ />
1406
+ )}
1407
+ </View>
1408
+ ) : isText && fileContent ? (
1409
+ <View style={[styles.textContainer, { backgroundColor: secondaryBackgroundColor, borderColor }]}>
1410
+ <ScrollView style={{ flex: 1 }} nestedScrollEnabled>
1411
+ <Text style={[styles.textContent, { color: textColor }]}>
1412
+ {fileContent}
1413
+ </Text>
1414
+ </ScrollView>
1415
+ </View>
1416
+ ) : isPDF && fileContent && Platform.OS === 'web' ? (
1417
+ <View style={styles.pdfContainer}>
1418
+ <iframe
1419
+ src={fileContent}
1420
+ width="100%"
1421
+ height="600px"
1422
+ style={{ border: 'none', borderRadius: 8 }}
1423
+ title={openedFile.filename}
1424
+ />
1425
+ </View>
1426
+ ) : isVideo && fileContent ? (
1427
+ <View style={styles.mediaContainer}>
1428
+ {Platform.OS === 'web' ? (
1429
+ <video
1430
+ controls
1431
+ style={{
1432
+ width: '100%',
1433
+ maxHeight: '70vh',
1434
+ borderRadius: 8,
1435
+ }}
1436
+ >
1437
+ <source src={fileContent} type={openedFile.contentType} />
1438
+ Your browser does not support the video tag.
1439
+ </video>
1440
+ ) : (
1441
+ <Text style={[styles.unsupportedText, { color: textColor }]}>
1442
+ Video playback not supported on mobile
1443
+ </Text>
1444
+ )}
1445
+ </View>
1446
+ ) : isAudio && fileContent ? (
1447
+ <View style={styles.mediaContainer}>
1448
+ {Platform.OS === 'web' ? (
1449
+ <audio
1450
+ controls
1451
+ style={{
1452
+ width: '100%',
1453
+ borderRadius: 8,
1454
+ }}
1455
+ >
1456
+ <source src={fileContent} type={openedFile.contentType} />
1457
+ Your browser does not support the audio tag.
1458
+ </audio>
1459
+ ) : (
1460
+ <Text style={[styles.unsupportedText, { color: textColor }]}>
1461
+ Audio playback not supported on mobile
1462
+ </Text>
1463
+ )}
1464
+ </View>
1465
+ ) : (
1466
+ <View style={styles.unsupportedFileContainer}>
1467
+ <Ionicons
1468
+ name={getFileIcon(openedFile.contentType) as any}
1469
+ size={64}
1470
+ color={isDarkTheme ? '#666666' : '#CCCCCC'}
1471
+ />
1472
+ <Text style={[styles.unsupportedFileTitle, { color: textColor }]}>
1473
+ Preview Not Available
1474
+ </Text>
1475
+ <Text style={[styles.unsupportedFileDescription, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1476
+ This file type cannot be previewed in the browser.{'\n'}
1477
+ Download the file to view its contents.
1478
+ </Text>
1479
+ <TouchableOpacity
1480
+ style={[styles.downloadButtonLarge, { backgroundColor: primaryColor }]}
1481
+ onPress={() => handleFileDownload(openedFile.id, openedFile.filename)}
1482
+ >
1483
+ <Ionicons name="download" size={20} color="#FFFFFF" />
1484
+ <Text style={styles.downloadButtonText}>Download File</Text>
1485
+ </TouchableOpacity>
1486
+ </View>
1487
+ )}
1488
+ </ScrollView>
1489
+ </View>
1490
+ );
1491
+ };
1492
+
1493
+ const renderEmptyState = () => (
1494
+ <View style={styles.emptyState}>
1495
+ <Ionicons name="folder-open-outline" size={64} color={isDarkTheme ? '#666666' : '#CCCCCC'} />
1496
+ <Text style={[styles.emptyStateTitle, { color: textColor }]}>No Files Yet</Text>
1497
+ <Text style={[styles.emptyStateDescription, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1498
+ {user?.id === targetUserId
1499
+ ? `Upload files to get started. You can select multiple files at once${Platform.OS === 'web' ? ' or drag & drop them here.' : '.'}`
1500
+ : "This user hasn't uploaded any files yet"
1501
+ }
1502
+ </Text>
1503
+ {user?.id === targetUserId && (
1504
+ <TouchableOpacity
1505
+ style={[styles.emptyStateButton, { backgroundColor: primaryColor }]}
1506
+ onPress={handleFileUpload}
1507
+ disabled={uploading}
1508
+ >
1509
+ {uploading ? (
1510
+ <ActivityIndicator size="small" color="#FFFFFF" />
1511
+ ) : (
1512
+ <>
1513
+ <Ionicons name="cloud-upload" size={20} color="#FFFFFF" />
1514
+ <Text style={styles.emptyStateButtonText}>Upload Files</Text>
1515
+ </>
1516
+ )}
1517
+ </TouchableOpacity>
1518
+ )}
1519
+ </View>
1520
+ );
1521
+
1522
+ if (loading) {
1523
+ return (
1524
+ <View style={[styles.container, styles.centerContent, { backgroundColor }]}>
1525
+ <ActivityIndicator size="large" color={primaryColor} />
1526
+ <Text style={[styles.loadingText, { color: textColor }]}>Loading files...</Text>
1527
+ </View>
1528
+ );
1529
+ }
1530
+
1531
+ // If a file is opened, show the file viewer
1532
+ if (openedFile) {
1533
+ return (
1534
+ <>
1535
+ {renderFileViewer()}
1536
+ {renderFileDetailsModal()}
1537
+ </>
1538
+ );
1539
+ }
1540
+
1541
+ return (
1542
+ <View
1543
+ style={[
1544
+ styles.container,
1545
+ { backgroundColor },
1546
+ isDragging && Platform.OS === 'web' && styles.dragOverlay
1547
+ ]}
1548
+ {...(Platform.OS === 'web' && user?.id === targetUserId ? {
1549
+ onDragOver: handleDragOver,
1550
+ onDragLeave: handleDragLeave,
1551
+ onDrop: handleDrop,
1552
+ } : {})}
1553
+ >
1554
+ {/* Header */}
1555
+ <View style={[
1556
+ styles.header,
1557
+ {
1558
+ borderBottomColor: borderColor,
1559
+ backgroundColor: isDarkTheme ? '#1A1A1A' : '#FFFFFF',
1560
+ shadowColor: '#000000',
1561
+ shadowOffset: {
1562
+ width: 0,
1563
+ height: 2,
1564
+ },
1565
+ shadowOpacity: isDarkTheme ? 0.3 : 0.1,
1566
+ shadowRadius: 8,
1567
+ elevation: 4,
1568
+ }
1569
+ ]}>
1570
+ <TouchableOpacity
1571
+ style={[
1572
+ styles.backButton,
1573
+ {
1574
+ backgroundColor: isDarkTheme ? '#2A2A2A' : '#F8F9FA',
1575
+ borderRadius: 12,
1576
+ }
1577
+ ]}
1578
+ onPress={onClose || goBack}
1579
+ >
1580
+ <Ionicons name="arrow-back" size={22} color={textColor} />
1581
+ </TouchableOpacity>
1582
+
1583
+ <View style={styles.headerTitleContainer}>
1584
+ <Text style={[styles.headerTitle, { color: textColor }]}>
1585
+ {viewMode === 'photos' ? 'Photos' : 'File Management'}
1586
+ </Text>
1587
+ <Text style={[styles.headerSubtitle, { color: isDarkTheme ? '#AAAAAA' : '#666666' }]}>
1588
+ {filteredFiles.length} {filteredFiles.length === 1 ? 'item' : 'items'}
1589
+ </Text>
1590
+ </View>
1591
+
1592
+ <View style={styles.headerActions}>
1593
+ {/* View Mode Toggle */}
1594
+ <View style={[
1595
+ styles.viewModeToggle,
1596
+ {
1597
+ backgroundColor: isDarkTheme ? '#2A2A2A' : '#F8F9FA',
1598
+ borderWidth: 1,
1599
+ borderColor: isDarkTheme ? '#3A3A3A' : '#E8E9EA',
1600
+ shadowColor: '#000000',
1601
+ shadowOffset: {
1602
+ width: 0,
1603
+ height: 1,
1604
+ },
1605
+ shadowOpacity: isDarkTheme ? 0.3 : 0.05,
1606
+ shadowRadius: 4,
1607
+ elevation: 2,
1608
+ }
1609
+ ]}>
1610
+ <TouchableOpacity
1611
+ style={[
1612
+ styles.viewModeButton,
1613
+ viewMode === 'all' && {
1614
+ backgroundColor: primaryColor,
1615
+ shadowColor: primaryColor,
1616
+ shadowOffset: {
1617
+ width: 0,
1618
+ height: 2,
1619
+ },
1620
+ shadowOpacity: 0.3,
1621
+ shadowRadius: 4,
1622
+ elevation: 3,
1623
+ }
1624
+ ]}
1625
+ onPress={() => setViewMode('all')}
1626
+ >
1627
+ <Ionicons
1628
+ name="folder"
1629
+ size={18}
1630
+ color={viewMode === 'all' ? '#FFFFFF' : textColor}
1631
+ />
1632
+ </TouchableOpacity>
1633
+ <TouchableOpacity
1634
+ style={[
1635
+ styles.viewModeButton,
1636
+ viewMode === 'photos' && {
1637
+ backgroundColor: primaryColor,
1638
+ shadowColor: primaryColor,
1639
+ shadowOffset: {
1640
+ width: 0,
1641
+ height: 2,
1642
+ },
1643
+ shadowOpacity: 0.3,
1644
+ shadowRadius: 4,
1645
+ elevation: 3,
1646
+ }
1647
+ ]}
1648
+ onPress={() => setViewMode('photos')}
1649
+ >
1650
+ <Ionicons
1651
+ name="images"
1652
+ size={18}
1653
+ color={viewMode === 'photos' ? '#FFFFFF' : textColor}
1654
+ />
1655
+ </TouchableOpacity>
1656
+ </View>
1657
+
1658
+ {user?.id === targetUserId && (
1659
+ <TouchableOpacity
1660
+ style={[
1661
+ styles.uploadButton,
1662
+ {
1663
+ backgroundColor: primaryColor,
1664
+ shadowColor: primaryColor,
1665
+ shadowOffset: {
1666
+ width: 0,
1667
+ height: 3,
1668
+ },
1669
+ shadowOpacity: 0.4,
1670
+ shadowRadius: 6,
1671
+ elevation: 5,
1672
+ transform: uploading ? [{ scale: 0.95 }] : [{ scale: 1 }],
1673
+ }
1674
+ ]}
1675
+ onPress={handleFileUpload}
1676
+ disabled={uploading}
1677
+ >
1678
+ {uploading ? (
1679
+ <View style={styles.uploadProgress}>
1680
+ <ActivityIndicator size="small" color="#FFFFFF" />
1681
+ {uploadProgress && (
1682
+ <Text style={styles.uploadProgressText}>
1683
+ {uploadProgress.current}/{uploadProgress.total}
1684
+ </Text>
1685
+ )}
1686
+ </View>
1687
+ ) : (
1688
+ <Ionicons name="add" size={26} color="#FFFFFF" />
1689
+ )}
1690
+ </TouchableOpacity>
1691
+ )}
1692
+ </View>
1693
+ </View>
1694
+
1695
+ {/* Search Bar */}
1696
+ {files.length > 0 && (viewMode === 'all' || files.some(f => f.contentType.startsWith('image/'))) && (
1697
+ <View style={[
1698
+ styles.searchContainer,
1699
+ {
1700
+ backgroundColor: isDarkTheme ? '#1A1A1A' : '#FFFFFF',
1701
+ borderColor: isDarkTheme ? '#3A3A3A' : '#E8E9EA',
1702
+ shadowColor: '#000000',
1703
+ shadowOffset: {
1704
+ width: 0,
1705
+ height: 1,
1706
+ },
1707
+ shadowOpacity: isDarkTheme ? 0.2 : 0.05,
1708
+ shadowRadius: 4,
1709
+ elevation: 2,
1710
+ }
1711
+ ]}>
1712
+ <Ionicons name="search" size={22} color={isDarkTheme ? '#888888' : '#666666'} />
1713
+ <TextInput
1714
+ style={[styles.searchInput, { color: textColor }]}
1715
+ placeholder={viewMode === 'photos' ? 'Search photos...' : 'Search files...'}
1716
+ placeholderTextColor={isDarkTheme ? '#888888' : '#999999'}
1717
+ value={searchQuery}
1718
+ onChangeText={setSearchQuery}
1719
+ />
1720
+ {searchQuery.length > 0 && (
1721
+ <TouchableOpacity
1722
+ onPress={() => setSearchQuery('')}
1723
+ style={styles.searchClearButton}
1724
+ >
1725
+ <Ionicons name="close-circle" size={22} color={isDarkTheme ? '#888888' : '#666666'} />
1726
+ </TouchableOpacity>
1727
+ )}
1728
+ </View>
1729
+ )}
1730
+
1731
+ {/* File Stats */}
1732
+ {files.length > 0 && (
1733
+ <View style={[
1734
+ styles.statsContainer,
1735
+ {
1736
+ backgroundColor: isDarkTheme ? '#1A1A1A' : '#FFFFFF',
1737
+ borderColor: isDarkTheme ? '#3A3A3A' : '#E8E9EA',
1738
+ shadowColor: '#000000',
1739
+ shadowOffset: {
1740
+ width: 0,
1741
+ height: 1,
1742
+ },
1743
+ shadowOpacity: isDarkTheme ? 0.2 : 0.05,
1744
+ shadowRadius: 4,
1745
+ elevation: 2,
1746
+ }
1747
+ ]}>
1748
+ <View style={styles.statItem}>
1749
+ <Text style={[styles.statValue, { color: textColor }]}>{filteredFiles.length}</Text>
1750
+ <Text style={[styles.statLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1751
+ {searchQuery.length > 0 ? 'Found' : (filteredFiles.length === 1 ? (viewMode === 'photos' ? 'Photo' : 'File') : (viewMode === 'photos' ? 'Photos' : 'Files'))}
1752
+ </Text>
1753
+ </View>
1754
+ <View style={styles.statItem}>
1755
+ <Text style={[styles.statValue, { color: textColor }]}>
1756
+ {formatFileSize(filteredFiles.reduce((total, file) => total + file.length, 0))}
1757
+ </Text>
1758
+ <Text style={[styles.statLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1759
+ {searchQuery.length > 0 ? 'Size' : 'Total Size'}
1760
+ </Text>
1761
+ </View>
1762
+ {searchQuery.length > 0 && (
1763
+ <View style={styles.statItem}>
1764
+ <Text style={[styles.statValue, { color: textColor }]}>{files.length}</Text>
1765
+ <Text style={[styles.statLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1766
+ Total
1767
+ </Text>
1768
+ </View>
1769
+ )}
1770
+ </View>
1771
+ )}
1772
+
1773
+ {/* File List */}
1774
+ {viewMode === 'photos' ? (
1775
+ renderPhotoGrid()
1776
+ ) : (
1777
+ <ScrollView
1778
+ style={styles.scrollView}
1779
+ contentContainerStyle={styles.scrollContainer}
1780
+ refreshControl={
1781
+ <RefreshControl
1782
+ refreshing={refreshing}
1783
+ onRefresh={() => loadFiles(true)}
1784
+ tintColor={primaryColor}
1785
+ />
1786
+ }
1787
+ >
1788
+ {filteredFiles.length === 0 && searchQuery.length > 0 ? (
1789
+ <View style={styles.emptyState}>
1790
+ <Ionicons name="search" size={64} color={isDarkTheme ? '#666666' : '#CCCCCC'} />
1791
+ <Text style={[styles.emptyStateTitle, { color: textColor }]}>No Results Found</Text>
1792
+ <Text style={[styles.emptyStateDescription, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1793
+ No files match your search for "{searchQuery}"
1794
+ </Text>
1795
+ <TouchableOpacity
1796
+ style={[styles.emptyStateButton, { backgroundColor: primaryColor }]}
1797
+ onPress={() => setSearchQuery('')}
1798
+ >
1799
+ <Ionicons name="refresh" size={20} color="#FFFFFF" />
1800
+ <Text style={styles.emptyStateButtonText}>Clear Search</Text>
1801
+ </TouchableOpacity>
1802
+ </View>
1803
+ ) : filteredFiles.length === 0 ? renderEmptyState() : (
1804
+ <>
1805
+ {filteredFiles.map(renderFileItem)}
1806
+ </>
1807
+ )}
1808
+ </ScrollView>
1809
+ )}
1810
+
1811
+ {renderFileDetailsModal()}
1812
+
1813
+ {/* Drag and Drop Overlay */}
1814
+ {isDragging && Platform.OS === 'web' && (
1815
+ <View style={styles.dragDropOverlay}>
1816
+ <View style={styles.dragDropContent}>
1817
+ <Ionicons name="cloud-upload" size={64} color={primaryColor} />
1818
+ <Text style={[styles.dragDropTitle, { color: primaryColor }]}>
1819
+ Drop files to upload
1820
+ </Text>
1821
+ <Text style={[styles.dragDropSubtitle, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
1822
+ Release to upload{uploadProgress ? ` (${uploadProgress.current}/${uploadProgress.total})` : ' multiple files'}
1823
+ </Text>
1824
+ </View>
1825
+ </View>
1826
+ )}
1827
+ </View>
1828
+ );
1829
+ };
1830
+
1831
+ const styles = StyleSheet.create({
1832
+ container: {
1833
+ flex: 1,
1834
+ },
1835
+ dragOverlay: {
1836
+ backgroundColor: 'rgba(0, 122, 255, 0.1)',
1837
+ borderWidth: 2,
1838
+ borderColor: '#007AFF',
1839
+ borderStyle: 'dashed',
1840
+ },
1841
+ centerContent: {
1842
+ justifyContent: 'center',
1843
+ alignItems: 'center',
1844
+ },
1845
+ header: {
1846
+ flexDirection: 'row',
1847
+ alignItems: 'center',
1848
+ justifyContent: 'space-between',
1849
+ paddingHorizontal: 24,
1850
+ paddingVertical: 20,
1851
+ borderBottomWidth: 1,
1852
+ position: 'relative',
1853
+ },
1854
+ backButton: {
1855
+ padding: 12,
1856
+ borderRadius: 12,
1857
+ alignItems: 'center',
1858
+ justifyContent: 'center',
1859
+ minWidth: 44,
1860
+ minHeight: 44,
1861
+ },
1862
+ headerTitleContainer: {
1863
+ flex: 1,
1864
+ alignItems: 'center',
1865
+ justifyContent: 'center',
1866
+ marginHorizontal: 16,
1867
+ },
1868
+ headerTitle: {
1869
+ fontSize: 22,
1870
+ fontWeight: '700',
1871
+ fontFamily: fontFamilies.phuduBold,
1872
+ letterSpacing: -0.5,
1873
+ lineHeight: 28,
1874
+ },
1875
+ headerSubtitle: {
1876
+ fontSize: 13,
1877
+ fontWeight: '500',
1878
+ fontFamily: fontFamilies.phuduMedium,
1879
+ marginTop: 2,
1880
+ letterSpacing: 0.2,
1881
+ },
1882
+ uploadButton: {
1883
+ width: 44,
1884
+ height: 44,
1885
+ borderRadius: 22,
1886
+ alignItems: 'center',
1887
+ justifyContent: 'center',
1888
+ },
1889
+ uploadProgress: {
1890
+ alignItems: 'center',
1891
+ justifyContent: 'center',
1892
+ },
1893
+ uploadProgressText: {
1894
+ color: '#FFFFFF',
1895
+ fontSize: 10,
1896
+ fontWeight: '600',
1897
+ marginTop: 2,
1898
+ },
1899
+ searchContainer: {
1900
+ flexDirection: 'row',
1901
+ alignItems: 'center',
1902
+ paddingHorizontal: 20,
1903
+ paddingVertical: 16,
1904
+ marginHorizontal: 24,
1905
+ marginTop: 20,
1906
+ borderRadius: 16,
1907
+ borderWidth: 1,
1908
+ gap: 16,
1909
+ },
1910
+ searchInput: {
1911
+ flex: 1,
1912
+ fontSize: 16,
1913
+ fontFamily: fontFamilies.phudu,
1914
+ lineHeight: 20,
1915
+ },
1916
+ searchClearButton: {
1917
+ padding: 4,
1918
+ borderRadius: 12,
1919
+ alignItems: 'center',
1920
+ justifyContent: 'center',
1921
+ },
1922
+ searchIcon: {
1923
+ marginRight: 8,
1924
+ },
1925
+ statsContainer: {
1926
+ flexDirection: 'row',
1927
+ paddingHorizontal: 24,
1928
+ paddingVertical: 20,
1929
+ marginHorizontal: 24,
1930
+ marginTop: 20,
1931
+ borderRadius: 16,
1932
+ borderWidth: 1,
1933
+ },
1934
+ statItem: {
1935
+ flex: 1,
1936
+ alignItems: 'center',
1937
+ paddingVertical: 4,
1938
+ },
1939
+ statValue: {
1940
+ fontSize: 28,
1941
+ fontWeight: '800',
1942
+ fontFamily: fontFamilies.phuduBold,
1943
+ letterSpacing: -0.5,
1944
+ lineHeight: 32,
1945
+ },
1946
+ statLabel: {
1947
+ fontSize: 15,
1948
+ fontWeight: '500',
1949
+ fontFamily: fontFamilies.phuduMedium,
1950
+ marginTop: 4,
1951
+ letterSpacing: 0.3,
1952
+ },
1953
+ scrollView: {
1954
+ flex: 1,
1955
+ },
1956
+ scrollContainer: {
1957
+ padding: 20,
1958
+ },
1959
+ fileItem: {
1960
+ flexDirection: 'row',
1961
+ alignItems: 'center',
1962
+ padding: 16,
1963
+ marginBottom: 12,
1964
+ borderRadius: 12,
1965
+ borderWidth: 1,
1966
+ },
1967
+ fileContent: {
1968
+ flexDirection: 'row',
1969
+ alignItems: 'center',
1970
+ flex: 1,
1971
+ },
1972
+ fileIconContainer: {
1973
+ width: 50,
1974
+ height: 50,
1975
+ alignItems: 'center',
1976
+ justifyContent: 'center',
1977
+ marginRight: 12,
1978
+ },
1979
+ filePreviewContainer: {
1980
+ width: 60,
1981
+ height: 60,
1982
+ marginRight: 12,
1983
+ },
1984
+ filePreview: {
1985
+ width: '100%',
1986
+ height: '100%',
1987
+ borderRadius: 8,
1988
+ backgroundColor: '#F5F5F5',
1989
+ alignItems: 'center',
1990
+ justifyContent: 'center',
1991
+ overflow: 'hidden',
1992
+ position: 'relative',
1993
+ },
1994
+ previewImage: {
1995
+ width: '100%',
1996
+ height: '100%',
1997
+ borderRadius: 8,
1998
+ },
1999
+ pdfPreview: {
2000
+ alignItems: 'center',
2001
+ justifyContent: 'center',
2002
+ width: '100%',
2003
+ height: '100%',
2004
+ backgroundColor: '#FF6B6B20',
2005
+ },
2006
+ pdfLabel: {
2007
+ fontSize: 8,
2008
+ fontWeight: 'bold',
2009
+ marginTop: 2,
2010
+ },
2011
+ videoPreview: {
2012
+ alignItems: 'center',
2013
+ justifyContent: 'center',
2014
+ width: '100%',
2015
+ height: '100%',
2016
+ backgroundColor: '#4ECDC420',
2017
+ },
2018
+ videoLabel: {
2019
+ fontSize: 8,
2020
+ fontWeight: 'bold',
2021
+ marginTop: 2,
2022
+ },
2023
+ fallbackIcon: {
2024
+ position: 'absolute',
2025
+ top: 0,
2026
+ left: 0,
2027
+ right: 0,
2028
+ bottom: 0,
2029
+ alignItems: 'center',
2030
+ justifyContent: 'center',
2031
+ backgroundColor: '#F5F5F5',
2032
+ borderRadius: 8,
2033
+ },
2034
+ previewOverlay: {
2035
+ position: 'absolute',
2036
+ top: 0,
2037
+ left: 0,
2038
+ right: 0,
2039
+ bottom: 0,
2040
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
2041
+ alignItems: 'center',
2042
+ justifyContent: 'center',
2043
+ borderRadius: 8,
2044
+ },
2045
+ fileInfo: {
2046
+ flex: 1,
2047
+ },
2048
+ fileName: {
2049
+ fontSize: 16,
2050
+ fontWeight: '600',
2051
+ marginBottom: 4,
2052
+ },
2053
+ fileDetails: {
2054
+ fontSize: 14,
2055
+ marginBottom: 2,
2056
+ },
2057
+ fileDescription: {
2058
+ fontSize: 12,
2059
+ fontStyle: 'italic',
2060
+ },
2061
+ fileActions: {
2062
+ flexDirection: 'row',
2063
+ gap: 8,
2064
+ },
2065
+ actionButton: {
2066
+ width: 40,
2067
+ height: 40,
2068
+ borderRadius: 20,
2069
+ alignItems: 'center',
2070
+ justifyContent: 'center',
2071
+ },
2072
+ emptyState: {
2073
+ alignItems: 'center',
2074
+ paddingVertical: 60,
2075
+ paddingHorizontal: 40,
2076
+ },
2077
+ emptyStateTitle: {
2078
+ fontSize: 24,
2079
+ fontWeight: 'bold',
2080
+ fontFamily: fontFamilies.phuduBold,
2081
+ marginTop: 16,
2082
+ marginBottom: 8,
2083
+ },
2084
+ emptyStateDescription: {
2085
+ fontSize: 16,
2086
+ textAlign: 'center',
2087
+ lineHeight: 24,
2088
+ marginBottom: 32,
2089
+ },
2090
+ emptyStateButton: {
2091
+ flexDirection: 'row',
2092
+ alignItems: 'center',
2093
+ paddingHorizontal: 24,
2094
+ paddingVertical: 12,
2095
+ borderRadius: 24,
2096
+ gap: 8,
2097
+ },
2098
+ emptyStateButtonText: {
2099
+ color: '#FFFFFF',
2100
+ fontSize: 16,
2101
+ fontWeight: '600',
2102
+ },
2103
+ loadingText: {
2104
+ fontSize: 16,
2105
+ marginTop: 16,
2106
+ },
2107
+
2108
+ // Modal styles
2109
+ modalContainer: {
2110
+ flex: 1,
2111
+ },
2112
+ modalHeader: {
2113
+ flexDirection: 'row',
2114
+ alignItems: 'center',
2115
+ justifyContent: 'space-between',
2116
+ paddingHorizontal: 20,
2117
+ paddingVertical: 16,
2118
+ borderBottomWidth: 1,
2119
+ },
2120
+ modalCloseButton: {
2121
+ padding: 8,
2122
+ },
2123
+ modalTitle: {
2124
+ fontSize: 18,
2125
+ fontWeight: '600',
2126
+ fontFamily: fontFamilies.phuduSemiBold,
2127
+ },
2128
+ modalPlaceholder: {
2129
+ width: 40,
2130
+ },
2131
+ modalContent: {
2132
+ flex: 1,
2133
+ padding: 20,
2134
+ },
2135
+ fileDetailCard: {
2136
+ padding: 24,
2137
+ borderRadius: 16,
2138
+ borderWidth: 1,
2139
+ alignItems: 'center',
2140
+ },
2141
+ fileDetailIcon: {
2142
+ marginBottom: 16,
2143
+ },
2144
+ fileDetailName: {
2145
+ fontSize: 20,
2146
+ fontWeight: 'bold',
2147
+ fontFamily: fontFamilies.phuduBold,
2148
+ textAlign: 'center',
2149
+ marginBottom: 24,
2150
+ },
2151
+ fileDetailInfo: {
2152
+ width: '100%',
2153
+ marginBottom: 32,
2154
+ },
2155
+ detailRow: {
2156
+ flexDirection: 'row',
2157
+ justifyContent: 'space-between',
2158
+ alignItems: 'flex-start',
2159
+ marginBottom: 12,
2160
+ flexWrap: 'wrap',
2161
+ },
2162
+ detailLabel: {
2163
+ fontSize: 16,
2164
+ fontWeight: '500',
2165
+ flex: 1,
2166
+ minWidth: 100,
2167
+ },
2168
+ detailValue: {
2169
+ fontSize: 16,
2170
+ flex: 2,
2171
+ textAlign: 'right',
2172
+ },
2173
+ modalActions: {
2174
+ flexDirection: 'row',
2175
+ gap: 12,
2176
+ width: '100%',
2177
+ },
2178
+ modalActionButton: {
2179
+ flex: 1,
2180
+ flexDirection: 'row',
2181
+ alignItems: 'center',
2182
+ justifyContent: 'center',
2183
+ paddingVertical: 16,
2184
+ borderRadius: 12,
2185
+ gap: 8,
2186
+ },
2187
+ modalActionText: {
2188
+ color: '#FFFFFF',
2189
+ fontSize: 16,
2190
+ fontWeight: '600',
2191
+ },
2192
+
2193
+ // Drag and Drop styles
2194
+ dragDropOverlay: {
2195
+ position: 'absolute',
2196
+ top: 0,
2197
+ left: 0,
2198
+ right: 0,
2199
+ bottom: 0,
2200
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
2201
+ justifyContent: 'center',
2202
+ alignItems: 'center',
2203
+ zIndex: 1000,
2204
+ },
2205
+ dragDropContent: {
2206
+ alignItems: 'center',
2207
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
2208
+ padding: 40,
2209
+ borderRadius: 20,
2210
+ borderWidth: 3,
2211
+ borderColor: '#007AFF',
2212
+ borderStyle: 'dashed',
2213
+ },
2214
+ dragDropTitle: {
2215
+ fontSize: 24,
2216
+ fontWeight: 'bold',
2217
+ marginTop: 16,
2218
+ marginBottom: 8,
2219
+ },
2220
+ dragDropSubtitle: {
2221
+ fontSize: 16,
2222
+ textAlign: 'center',
2223
+ },
2224
+
2225
+ // File Viewer styles
2226
+ fileViewerContainer: {
2227
+ flex: 1,
2228
+ },
2229
+ fileViewerHeader: {
2230
+ flexDirection: 'row',
2231
+ alignItems: 'center',
2232
+ paddingHorizontal: 20,
2233
+ paddingVertical: 16,
2234
+ borderBottomWidth: 1,
2235
+ },
2236
+ fileViewerTitleContainer: {
2237
+ flex: 1,
2238
+ marginHorizontal: 16,
2239
+ },
2240
+ fileViewerTitle: {
2241
+ fontSize: 18,
2242
+ fontWeight: '600',
2243
+ fontFamily: fontFamilies.phuduSemiBold,
2244
+ marginBottom: 2,
2245
+ },
2246
+ fileViewerSubtitle: {
2247
+ fontSize: 14,
2248
+ },
2249
+ fileViewerActions: {
2250
+ flexDirection: 'row',
2251
+ gap: 8,
2252
+ },
2253
+ fileViewerContent: {
2254
+ flex: 1,
2255
+ },
2256
+ fileViewerContentWithDetails: {
2257
+ paddingBottom: 20,
2258
+ },
2259
+ fileViewerContentContainer: {
2260
+ flexGrow: 1,
2261
+ padding: 20,
2262
+ },
2263
+ fileViewerLoading: {
2264
+ flex: 1,
2265
+ justifyContent: 'center',
2266
+ alignItems: 'center',
2267
+ },
2268
+ fileViewerLoadingText: {
2269
+ fontSize: 16,
2270
+ marginTop: 16,
2271
+ },
2272
+ imageContainer: {
2273
+ alignItems: 'center',
2274
+ justifyContent: 'center',
2275
+ flex: 1,
2276
+ },
2277
+ textContainer: {
2278
+ flex: 1,
2279
+ borderRadius: 12,
2280
+ borderWidth: 1,
2281
+ padding: 16,
2282
+ minHeight: 200,
2283
+ maxHeight: '80%',
2284
+ },
2285
+ textContent: {
2286
+ fontSize: 14,
2287
+ fontFamily: Platform.OS === 'web' ? 'monospace' : 'Courier',
2288
+ lineHeight: 20,
2289
+ },
2290
+ unsupportedFileContainer: {
2291
+ flex: 1,
2292
+ justifyContent: 'center',
2293
+ alignItems: 'center',
2294
+ paddingVertical: 60,
2295
+ paddingHorizontal: 40,
2296
+ },
2297
+ unsupportedFileTitle: {
2298
+ fontSize: 24,
2299
+ fontWeight: 'bold',
2300
+ fontFamily: fontFamilies.phuduBold,
2301
+ marginTop: 16,
2302
+ marginBottom: 8,
2303
+ textAlign: 'center',
2304
+ },
2305
+ unsupportedFileDescription: {
2306
+ fontSize: 16,
2307
+ textAlign: 'center',
2308
+ lineHeight: 24,
2309
+ marginBottom: 32,
2310
+ },
2311
+ downloadButtonLarge: {
2312
+ flexDirection: 'row',
2313
+ alignItems: 'center',
2314
+ paddingHorizontal: 24,
2315
+ paddingVertical: 16,
2316
+ borderRadius: 24,
2317
+ gap: 8,
2318
+ },
2319
+ downloadButtonText: {
2320
+ color: '#FFFFFF',
2321
+ fontSize: 16,
2322
+ fontWeight: '600',
2323
+ },
2324
+ pdfContainer: {
2325
+ flex: 1,
2326
+ alignItems: 'center',
2327
+ justifyContent: 'center',
2328
+ },
2329
+ mediaContainer: {
2330
+ flex: 1,
2331
+ alignItems: 'center',
2332
+ justifyContent: 'center',
2333
+ padding: 20,
2334
+ },
2335
+ unsupportedText: {
2336
+ fontSize: 16,
2337
+ textAlign: 'center',
2338
+ fontStyle: 'italic',
2339
+ },
2340
+
2341
+ // File Details in Viewer styles
2342
+ fileDetailsSection: {
2343
+ margin: 16,
2344
+ marginTop: 0,
2345
+ padding: 20,
2346
+ borderRadius: 12,
2347
+ borderWidth: 1,
2348
+ },
2349
+ fileDetailsSectionTitle: {
2350
+ fontSize: 18,
2351
+ fontWeight: '600',
2352
+ fontFamily: fontFamilies.phuduSemiBold,
2353
+ flex: 1,
2354
+ },
2355
+ fileDetailsSectionHeader: {
2356
+ flexDirection: 'row',
2357
+ alignItems: 'center',
2358
+ justifyContent: 'space-between',
2359
+ marginBottom: 16,
2360
+ },
2361
+ fileDetailsSectionToggle: {
2362
+ padding: 4,
2363
+ },
2364
+ fileDetailsActions: {
2365
+ flexDirection: 'row',
2366
+ gap: 12,
2367
+ marginTop: 16,
2368
+ },
2369
+ fileDetailsActionButton: {
2370
+ flex: 1,
2371
+ flexDirection: 'row',
2372
+ alignItems: 'center',
2373
+ justifyContent: 'center',
2374
+ paddingVertical: 12,
2375
+ borderRadius: 8,
2376
+ gap: 6,
2377
+ },
2378
+ fileDetailsActionText: {
2379
+ color: '#FFFFFF',
2380
+ fontSize: 14,
2381
+ fontWeight: '600',
2382
+ },
2383
+
2384
+ // Header styles
2385
+ headerActions: {
2386
+ flexDirection: 'row',
2387
+ alignItems: 'center',
2388
+ gap: 16,
2389
+ },
2390
+ viewModeToggle: {
2391
+ flexDirection: 'row',
2392
+ borderRadius: 24,
2393
+ padding: 3,
2394
+ overflow: 'hidden',
2395
+ },
2396
+ viewModeButton: {
2397
+ paddingHorizontal: 14,
2398
+ paddingVertical: 10,
2399
+ borderRadius: 20,
2400
+ minWidth: 44,
2401
+ alignItems: 'center',
2402
+ justifyContent: 'center',
2403
+ marginHorizontal: 1,
2404
+ },
2405
+
2406
+ // Photo Grid styles
2407
+ photoScrollContainer: {
2408
+ padding: 16,
2409
+ },
2410
+ photoDateSection: {
2411
+ marginBottom: 24,
2412
+ },
2413
+ photoDateHeader: {
2414
+ fontSize: 18,
2415
+ fontWeight: '600',
2416
+ fontFamily: fontFamilies.phuduSemiBold,
2417
+ marginBottom: 12,
2418
+ paddingHorizontal: 4,
2419
+ },
2420
+ photoGrid: {
2421
+ flexDirection: 'row',
2422
+ flexWrap: 'wrap',
2423
+ gap: 4,
2424
+ justifyContent: 'flex-start',
2425
+ },
2426
+ photoItem: {
2427
+ borderRadius: 8,
2428
+ overflow: 'hidden',
2429
+ },
2430
+ photoContainer: {
2431
+ width: '100%',
2432
+ height: '100%',
2433
+ position: 'relative',
2434
+ borderRadius: 8,
2435
+ overflow: 'hidden',
2436
+ },
2437
+ photoImage: {
2438
+ width: '100%',
2439
+ height: '100%',
2440
+ },
2441
+
2442
+ // Justified Grid styles
2443
+ dimensionsLoadingIndicator: {
2444
+ flexDirection: 'row',
2445
+ alignItems: 'center',
2446
+ justifyContent: 'center',
2447
+ paddingVertical: 16,
2448
+ gap: 8,
2449
+ },
2450
+ dimensionsLoadingText: {
2451
+ fontSize: 14,
2452
+ fontStyle: 'italic',
2453
+ },
2454
+ justifiedPhotoGrid: {
2455
+ gap: 4,
2456
+ },
2457
+ justifiedPhotoRow: {
2458
+ flexDirection: 'row',
2459
+ },
2460
+ justifiedPhotoItem: {
2461
+ borderRadius: 6,
2462
+ overflow: 'hidden',
2463
+ position: 'relative',
2464
+ },
2465
+ justifiedPhotoContainer: {
2466
+ width: '100%',
2467
+ height: '100%',
2468
+ position: 'relative',
2469
+ borderRadius: 6,
2470
+ overflow: 'hidden',
2471
+ backgroundColor: '#F5F5F5',
2472
+ },
2473
+ justifiedPhotoImage: {
2474
+ width: '100%',
2475
+ height: '100%',
2476
+ borderRadius: 6,
2477
+ },
2478
+
2479
+ // Simple Photo Grid styles
2480
+ simplePhotoItem: {
2481
+ borderRadius: 8,
2482
+ overflow: 'hidden',
2483
+ backgroundColor: '#F5F5F5',
2484
+ },
2485
+ simplePhotoContainer: {
2486
+ width: '100%',
2487
+ height: '100%',
2488
+ position: 'relative',
2489
+ borderRadius: 8,
2490
+ overflow: 'hidden',
2491
+ },
2492
+ simplePhotoImage: {
2493
+ width: '100%',
2494
+ height: '100%',
2495
+ borderRadius: 8,
2496
+ },
2497
+
2498
+ // Loading skeleton styles
2499
+ photoSkeletonGrid: {
2500
+ flexDirection: 'row',
2501
+ flexWrap: 'wrap',
2502
+ gap: 4,
2503
+ marginTop: 20,
2504
+ },
2505
+ photoSkeletonItem: {
2506
+ width: '32%',
2507
+ aspectRatio: 1,
2508
+ borderRadius: 8,
2509
+ marginBottom: 4,
2510
+ },
2511
+ });
2512
+
2513
+ export default FileManagementScreen;