UNPKG

@oxyhq/services

Version:

Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀

1,188 lines (1,158 loc) • 90.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _OxyContext = require("../context/OxyContext"); var _fonts = require("../styles/fonts"); var _sonner = require("../../lib/sonner"); var _vectorIcons = require("@expo/vector-icons"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } const FileManagementScreen = ({ onClose, theme, goBack, navigate, userId, containerWidth = 400 // Fallback for when not provided by the router }) => { const { user, oxyServices } = (0, _OxyContext.useOxy)(); // Debug: log the actual container width (0, _react.useEffect)(() => { console.log('[FileManagementScreen] Container width (full):', containerWidth); // Padding structure: // - containerWidth = full bottom sheet container width (measured from OxyProvider) // - photoScrollContainer adds padding: 16 (32px total horizontal padding) // - Available content width = containerWidth - 32 const availableContentWidth = containerWidth - 32; console.log('[FileManagementScreen] Available content width:', availableContentWidth); console.log('[FileManagementScreen] Spacing fix applied: 4px uniform gap both horizontal and vertical'); }, [containerWidth]); const [files, setFiles] = (0, _react.useState)([]); const [loading, setLoading] = (0, _react.useState)(true); const [refreshing, setRefreshing] = (0, _react.useState)(false); const [uploading, setUploading] = (0, _react.useState)(false); const [uploadProgress, setUploadProgress] = (0, _react.useState)(null); const [deleting, setDeleting] = (0, _react.useState)(null); const [selectedFile, setSelectedFile] = (0, _react.useState)(null); const [showFileDetails, setShowFileDetails] = (0, _react.useState)(false); const [openedFile, setOpenedFile] = (0, _react.useState)(null); const [fileContent, setFileContent] = (0, _react.useState)(null); const [loadingFileContent, setLoadingFileContent] = (0, _react.useState)(false); const [showFileDetailsInViewer, setShowFileDetailsInViewer] = (0, _react.useState)(false); const [viewMode, setViewMode] = (0, _react.useState)('all'); const [searchQuery, setSearchQuery] = (0, _react.useState)(''); const [filteredFiles, setFilteredFiles] = (0, _react.useState)([]); const [isDragging, setIsDragging] = (0, _react.useState)(false); const [photoDimensions, setPhotoDimensions] = (0, _react.useState)({}); const [loadingDimensions, setLoadingDimensions] = (0, _react.useState)(false); const [hoveredPreview, setHoveredPreview] = (0, _react.useState)(null); const isDarkTheme = theme === 'dark'; const textColor = isDarkTheme ? '#FFFFFF' : '#000000'; const backgroundColor = isDarkTheme ? '#121212' : '#f2f2f2'; const secondaryBackgroundColor = isDarkTheme ? '#222222' : '#FFFFFF'; const borderColor = isDarkTheme ? '#444444' : '#E0E0E0'; const primaryColor = '#007AFF'; const dangerColor = '#FF3B30'; const successColor = '#34C759'; const targetUserId = userId || user?.id; const loadFiles = (0, _react.useCallback)(async (isRefresh = false) => { if (!targetUserId) return; try { if (isRefresh) { setRefreshing(true); } else { setLoading(true); } const response = await oxyServices.listUserFiles(targetUserId); setFiles(response.files || []); } catch (error) { console.error('Failed to load files:', error); _sonner.toast.error(error.message || 'Failed to load files'); } finally { setLoading(false); setRefreshing(false); } }, [targetUserId, oxyServices]); // Filter files based on search query and view mode (0, _react.useEffect)(() => { let filteredByMode = files; // Filter by view mode first if (viewMode === 'photos') { filteredByMode = files.filter(file => file.contentType.startsWith('image/')); } // Then filter by search query if (!searchQuery.trim()) { setFilteredFiles(filteredByMode); } else { const query = searchQuery.toLowerCase(); const filtered = filteredByMode.filter(file => file.filename.toLowerCase().includes(query) || file.contentType.toLowerCase().includes(query) || file.metadata?.description && file.metadata.description.toLowerCase().includes(query)); setFilteredFiles(filtered); } }, [files, searchQuery, viewMode]); // Load photo dimensions for justified grid const loadPhotoDimensions = (0, _react.useCallback)(async photos => { if (photos.length === 0) return; setLoadingDimensions(true); const newDimensions = { ...photoDimensions }; let hasNewDimensions = false; // Only load dimensions for photos we don't have yet const photosToLoad = photos.filter(photo => !newDimensions[photo.id]); if (photosToLoad.length === 0) { setLoadingDimensions(false); return; } try { await Promise.all(photosToLoad.map(async photo => { try { const downloadUrl = oxyServices.getFileDownloadUrl(photo.id); if (_reactNative.Platform.OS === 'web') { const img = new window.Image(); await new Promise((resolve, reject) => { img.onload = () => { newDimensions[photo.id] = { width: img.naturalWidth, height: img.naturalHeight }; hasNewDimensions = true; resolve(); }; img.onerror = () => { // Fallback dimensions for failed loads newDimensions[photo.id] = { width: 1, height: 1 }; hasNewDimensions = true; resolve(); }; img.src = downloadUrl; }); } else { // For mobile, use Image.getSize from react-native await new Promise(resolve => { _reactNative.Image.getSize(downloadUrl, (width, height) => { newDimensions[photo.id] = { width, height }; hasNewDimensions = true; resolve(); }, () => { // Fallback dimensions newDimensions[photo.id] = { width: 1, height: 1 }; hasNewDimensions = true; resolve(); }); }); } } catch (error) { // Fallback dimensions for any errors newDimensions[photo.id] = { width: 1, height: 1 }; hasNewDimensions = true; } })); if (hasNewDimensions) { setPhotoDimensions(newDimensions); } } catch (error) { console.error('Error loading photo dimensions:', error); } finally { setLoadingDimensions(false); } }, [oxyServices, photoDimensions]); // Create justified rows from photos with responsive algorithm const createJustifiedRows = (0, _react.useCallback)(photos => { if (photos.length === 0) return []; const rows = []; const photosPerRow = 3; // Fixed 3 photos per row for consistency for (let i = 0; i < photos.length; i += photosPerRow) { const rowPhotos = photos.slice(i, i + photosPerRow); rows.push(rowPhotos); } return rows; }, []); const processFileUploads = async selectedFiles => { if (selectedFiles.length === 0) return; try { // Show initial progress setUploadProgress({ current: 0, total: selectedFiles.length }); // Validate file sizes (example: 50MB limit per file) const maxSize = 50 * 1024 * 1024; // 50MB const oversizedFiles = selectedFiles.filter(file => file.size > maxSize); if (oversizedFiles.length > 0) { const fileList = oversizedFiles.map(f => f.name).join('\n'); window.alert(`File Size Limit\n\nThe following files are too large (max 50MB):\n${fileList}`); return; } // Option 1: Bulk upload (faster, all-or-nothing) for 5 or fewer files if (selectedFiles.length <= 5) { const filenames = selectedFiles.map(f => f.name); const response = await oxyServices.uploadFiles(selectedFiles, filenames, { userId: targetUserId, uploadDate: new Date().toISOString() }); _sonner.toast.success(`${response.files.length} file(s) uploaded successfully`); // Small delay to ensure backend processing is complete setTimeout(async () => { await loadFiles(); }, 500); } else { // Option 2: Individual uploads for better progress and error handling let successCount = 0; let failureCount = 0; const errors = []; for (let i = 0; i < selectedFiles.length; i++) { const file = selectedFiles[i]; setUploadProgress({ current: i + 1, total: selectedFiles.length }); try { await oxyServices.uploadFile(file, file.name, { userId: targetUserId, uploadDate: new Date().toISOString() }); successCount++; } catch (error) { failureCount++; errors.push(`${file.name}: ${error.message || 'Upload failed'}`); } } // Show results summary if (successCount > 0) { _sonner.toast.success(`${successCount} file(s) uploaded successfully`); } if (failureCount > 0) { const errorMessage = `${failureCount} file(s) failed to upload${errors.length > 0 ? ':\n' + errors.slice(0, 3).join('\n') + (errors.length > 3 ? '\n...' : '') : ''}`; _sonner.toast.error(errorMessage); } // Small delay to ensure backend processing is complete setTimeout(async () => { await loadFiles(); }, 500); } } catch (error) { console.error('Upload error:', error); _sonner.toast.error(error.message || 'Failed to upload files'); } finally { setUploadProgress(null); } }; const handleFileUpload = async () => { try { setUploading(true); setUploadProgress(null); if (_reactNative.Platform.OS === 'web') { // Web file picker implementation const input = document.createElement('input'); input.type = 'file'; input.multiple = true; input.accept = '*/*'; input.onchange = async e => { const selectedFiles = Array.from(e.target.files); await processFileUploads(selectedFiles); }; input.click(); } else { // Mobile - show info that file picker can be added const installCommand = 'npm install expo-document-picker'; 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.`; if (window.confirm(`${message}\n\nWould you like to copy the install command?`)) { _sonner.toast.info(`Install: ${installCommand}`); } else { _sonner.toast.info('Mobile file upload requires expo-document-picker'); } } } catch (error) { _sonner.toast.error(error.message || 'Failed to upload file'); } finally { setUploading(false); setUploadProgress(null); } }; const handleFileDelete = async (fileId, filename) => { // Use web-compatible confirmation dialog const confirmed = window.confirm(`Are you sure you want to delete "${filename}"? This action cannot be undone.`); if (!confirmed) { console.log('Delete cancelled by user'); return; } try { console.log('Deleting file:', { fileId, filename }); console.log('Target user ID:', targetUserId); console.log('Current user ID:', user?.id); setDeleting(fileId); const result = await oxyServices.deleteFile(fileId); console.log('Delete result:', result); _sonner.toast.success('File deleted successfully'); // Reload files after successful deletion setTimeout(async () => { await loadFiles(); }, 500); } catch (error) { console.error('Delete error:', error); console.error('Error details:', error.response?.data || error.message); // Provide specific error messages if (error.message?.includes('File not found') || error.message?.includes('404')) { _sonner.toast.error('File not found. It may have already been deleted.'); // Still reload files to refresh the list setTimeout(async () => { await loadFiles(); }, 500); } else if (error.message?.includes('permission') || error.message?.includes('403')) { _sonner.toast.error('You do not have permission to delete this file.'); } else { _sonner.toast.error(error.message || 'Failed to delete file'); } } finally { setDeleting(null); } }; // Drag and drop handlers for web const handleDragOver = e => { if (_reactNative.Platform.OS === 'web' && user?.id === targetUserId) { e.preventDefault(); setIsDragging(true); } }; const handleDragLeave = e => { if (_reactNative.Platform.OS === 'web') { e.preventDefault(); setIsDragging(false); } }; const handleDrop = async e => { if (_reactNative.Platform.OS === 'web' && user?.id === targetUserId) { e.preventDefault(); setIsDragging(false); setUploading(true); try { const files = Array.from(e.dataTransfer.files); await processFileUploads(files); } catch (error) { _sonner.toast.error(error.message || 'Failed to upload files'); } finally { setUploading(false); } } }; const handleFileDownload = async (fileId, filename) => { try { if (_reactNative.Platform.OS === 'web') { console.log('Downloading file:', { fileId, filename }); // Use the public download URL method const downloadUrl = oxyServices.getFileDownloadUrl(fileId); console.log('Download URL:', downloadUrl); try { // Method 1: Try simple link download first const link = document.createElement('a'); link.href = downloadUrl; link.download = filename; link.target = '_blank'; document.body.appendChild(link); link.click(); document.body.removeChild(link); _sonner.toast.success('File download started'); } catch (linkError) { console.warn('Link download failed, trying fetch method:', linkError); // Method 2: Fallback to fetch download const response = await fetch(downloadUrl); if (!response.ok) { if (response.status === 404) { throw new Error('File not found. It may have been deleted.'); } else { throw new Error(`Download failed: ${response.status} ${response.statusText}`); } } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); // Clean up the blob URL window.URL.revokeObjectURL(url); _sonner.toast.success('File downloaded successfully'); } } else { _sonner.toast.info('File download not implemented for mobile yet'); } } catch (error) { console.error('Download error:', error); _sonner.toast.error(error.message || 'Failed to download file'); } }; const formatFileSize = bytes => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const getFileIcon = contentType => { if (contentType.startsWith('image/')) return 'image'; if (contentType.startsWith('video/')) return 'videocam'; if (contentType.startsWith('audio/')) return 'musical-notes'; if (contentType.includes('pdf')) return 'document-text'; if (contentType.includes('word') || contentType.includes('doc')) return 'document'; if (contentType.includes('excel') || contentType.includes('sheet')) return 'grid'; if (contentType.includes('zip') || contentType.includes('archive')) return 'archive'; return 'document-outline'; }; const handleFileOpen = async file => { try { setLoadingFileContent(true); setOpenedFile(file); // For text files, images, and other viewable content, try to load the content if (file.contentType.startsWith('text/') || file.contentType.includes('json') || file.contentType.includes('xml') || file.contentType.includes('javascript') || file.contentType.includes('typescript') || file.contentType.startsWith('image/') || file.contentType.includes('pdf') || file.contentType.startsWith('video/') || file.contentType.startsWith('audio/')) { try { const downloadUrl = oxyServices.getFileDownloadUrl(file.id); const response = await fetch(downloadUrl); if (response.ok) { if (file.contentType.startsWith('image/') || file.contentType.includes('pdf') || file.contentType.startsWith('video/') || file.contentType.startsWith('audio/')) { // For images, PDFs, videos, and audio, we'll use the URL directly setFileContent(downloadUrl); } else { // For text files, get the content const content = await response.text(); setFileContent(content); } } else { if (response.status === 404) { _sonner.toast.error('File not found. It may have been deleted.'); } else { _sonner.toast.error(`Failed to load file: ${response.status} ${response.statusText}`); } setFileContent(null); } } catch (error) { console.error('Failed to load file content:', error); if (error.message?.includes('404') || error.message?.includes('not found')) { _sonner.toast.error('File not found. It may have been deleted.'); } else { _sonner.toast.error('Failed to load file content'); } setFileContent(null); } } else { // For non-viewable files, don't load content setFileContent(null); } } catch (error) { console.error('Failed to open file:', error); _sonner.toast.error(error.message || 'Failed to open file'); } finally { setLoadingFileContent(false); } }; const handleCloseFile = () => { setOpenedFile(null); setFileContent(null); setShowFileDetailsInViewer(false); // Don't reset view mode when closing a file }; const showFileDetailsModal = file => { setSelectedFile(file); setShowFileDetails(true); }; const renderSimplePhotoItem = (0, _react.useCallback)((photo, index) => { const downloadUrl = oxyServices.getFileDownloadUrl(photo.id); // Calculate photo item width based on actual container size from bottom sheet let itemsPerRow = 3; // Default for mobile if (containerWidth > 768) itemsPerRow = 4; // Desktop/tablet else if (containerWidth > 480) itemsPerRow = 3; // Large mobile // Account for the photoScrollContainer padding (16px on each side = 32px total) const scrollContainerPadding = 32; // Total horizontal padding from photoScrollContainer const gaps = (itemsPerRow - 1) * 4; // Gap between items (4px) const availableWidth = containerWidth - scrollContainerPadding; const itemWidth = (availableWidth - gaps) / itemsPerRow; return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: [styles.simplePhotoItem, { width: itemWidth, height: itemWidth, marginRight: (index + 1) % itemsPerRow === 0 ? 0 : 4 }], onPress: () => handleFileOpen(photo), activeOpacity: 0.8, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.simplePhotoContainer, children: _reactNative.Platform.OS === 'web' ? /*#__PURE__*/(0, _jsxRuntime.jsx)("img", { src: downloadUrl, alt: photo.filename, style: { width: '100%', height: '100%', objectFit: 'cover', borderRadius: 8, transition: 'transform 0.2s ease' }, loading: "lazy", onError: e => { console.error('Photo failed to load:', e); }, onMouseEnter: e => { e.currentTarget.style.transform = 'scale(1.05)'; }, onMouseLeave: e => { e.currentTarget.style.transform = 'scale(1)'; } }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, { source: { uri: downloadUrl }, style: styles.simplePhotoImage, resizeMode: "cover", onError: e => { console.error('Photo failed to load:', e); } }) }) }, photo.id); }, [oxyServices, containerWidth]); const renderJustifiedPhotoItem = (0, _react.useCallback)((photo, width, height, isLast) => { const downloadUrl = oxyServices.getFileDownloadUrl(photo.id); return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: [styles.justifiedPhotoItem, { width, height }], onPress: () => handleFileOpen(photo), activeOpacity: 0.8, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.justifiedPhotoContainer, children: _reactNative.Platform.OS === 'web' ? /*#__PURE__*/(0, _jsxRuntime.jsx)("img", { src: downloadUrl, alt: photo.filename, style: { width: '100%', height: '100%', objectFit: 'cover', borderRadius: 6, transition: 'transform 0.2s ease, box-shadow 0.2s ease' }, loading: "lazy", onError: e => { console.error('Photo failed to load:', e); }, onMouseEnter: e => { e.currentTarget.style.transform = 'scale(1.02)'; e.currentTarget.style.boxShadow = '0 8px 25px rgba(0,0,0,0.15)'; e.currentTarget.style.zIndex = '10'; }, onMouseLeave: e => { e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)'; e.currentTarget.style.zIndex = '1'; } }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, { source: { uri: downloadUrl }, style: styles.justifiedPhotoImage, resizeMode: "cover", onError: e => { console.error('Photo failed to load:', e); } }) }) }, photo.id); }, [oxyServices]); (0, _react.useEffect)(() => { loadFiles(); }, [loadFiles]); const renderFileItem = file => { const isImage = file.contentType.startsWith('image/'); const isPDF = file.contentType.includes('pdf'); const isVideo = file.contentType.startsWith('video/'); const isAudio = file.contentType.startsWith('audio/'); const hasPreview = isImage || isPDF || isVideo; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.fileItem, { backgroundColor: secondaryBackgroundColor, borderColor }], children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, { style: styles.fileContent, onPress: () => handleFileOpen(file), children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.filePreviewContainer, children: hasPreview ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.filePreview, ...(_reactNative.Platform.OS === 'web' && { onMouseEnter: () => setHoveredPreview(file.id), onMouseLeave: () => setHoveredPreview(null) }), children: [isImage && (_reactNative.Platform.OS === 'web' ? /*#__PURE__*/(0, _jsxRuntime.jsx)("img", { src: oxyServices.getFileDownloadUrl(file.id), style: { width: '100%', height: '100%', objectFit: 'cover', borderRadius: 8, transition: 'transform 0.2s ease', transform: hoveredPreview === file.id ? 'scale(1.05)' : 'scale(1)' }, onError: e => { // Show fallback icon if image fails to load e.currentTarget.style.display = 'none'; const fallbackElement = e.currentTarget.parentElement?.querySelector('[data-fallback="true"]'); if (fallbackElement) { fallbackElement.style.display = 'flex'; } } }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, { source: { uri: oxyServices.getFileDownloadUrl(file.id) }, style: styles.previewImage, resizeMode: "cover", onError: () => { // For React Native, you might want to set an error state console.warn('Failed to load image preview for file:', file.id); } })), isPDF && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.pdfPreview, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "document", size: 32, color: primaryColor }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.pdfLabel, { color: primaryColor }], children: "PDF" })] }), isVideo && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.videoPreview, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "play-circle", size: 32, color: primaryColor }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.videoLabel, { color: primaryColor }], children: "VIDEO" })] }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.fallbackIcon, { display: isImage ? 'none' : 'flex' }], ...(_reactNative.Platform.OS === 'web' && { 'data-fallback': 'true' }), children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: getFileIcon(file.contentType), size: 32, color: primaryColor }) }), _reactNative.Platform.OS === 'web' && hoveredPreview === file.id && isImage && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.previewOverlay, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "eye", size: 24, color: "#FFFFFF" }) })] }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.fileIconContainer, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: getFileIcon(file.contentType), size: 32, color: primaryColor }) }) }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.fileInfo, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.fileName, { color: textColor }], numberOfLines: 1, children: file.filename }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, { style: [styles.fileDetails, { color: isDarkTheme ? '#BBBBBB' : '#666666' }], children: [formatFileSize(file.length), " \u2022 ", new Date(file.uploadDate).toLocaleDateString()] }), file.metadata?.description && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.fileDescription, { color: isDarkTheme ? '#AAAAAA' : '#888888' }], numberOfLines: 2, children: file.metadata.description })] })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.fileActions, children: [hasPreview && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: [styles.actionButton, { backgroundColor: isDarkTheme ? '#333333' : '#F0F0F0' }], onPress: () => handleFileOpen(file), children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "eye", size: 20, color: primaryColor }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: [styles.actionButton, { backgroundColor: isDarkTheme ? '#333333' : '#F0F0F0' }], onPress: () => handleFileDownload(file.id, file.filename), children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "download", size: 20, color: primaryColor }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: [styles.actionButton, { backgroundColor: isDarkTheme ? '#400000' : '#FFEBEE' }], onPress: () => { handleFileDelete(file.id, file.filename); }, disabled: deleting === file.id, children: deleting === file.id ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "small", color: dangerColor }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "trash", size: 20, color: dangerColor }) })] })] }, file.id); }; const renderPhotoGrid = (0, _react.useCallback)(() => { const photos = filteredFiles.filter(file => file.contentType.startsWith('image/')); if (photos.length === 0) { return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.emptyState, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "images-outline", size: 64, color: isDarkTheme ? '#666666' : '#CCCCCC' }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.emptyStateTitle, { color: textColor }], children: "No Photos Yet" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.emptyStateDescription, { color: isDarkTheme ? '#BBBBBB' : '#666666' }], children: user?.id === targetUserId ? `Upload photos to get started. You can select multiple photos at once${_reactNative.Platform.OS === 'web' ? ' or drag & drop them here.' : '.'}` : "This user hasn't uploaded any photos yet" }), user?.id === targetUserId && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: [styles.emptyStateButton, { backgroundColor: primaryColor }], onPress: handleFileUpload, disabled: uploading, children: uploading ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "small", color: "#FFFFFF" }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "cloud-upload", size: 20, color: "#FFFFFF" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: styles.emptyStateButtonText, children: "Upload Photos" })] }) })] }); } return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, { style: styles.scrollView, contentContainerStyle: styles.photoScrollContainer, refreshControl: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.RefreshControl, { refreshing: refreshing, onRefresh: () => loadFiles(true), tintColor: primaryColor }), showsVerticalScrollIndicator: false, children: [loadingDimensions && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.dimensionsLoadingIndicator, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "small", color: primaryColor }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.dimensionsLoadingText, { color: isDarkTheme ? '#BBBBBB' : '#666666' }], children: "Loading photo layout..." })] }), /*#__PURE__*/(0, _jsxRuntime.jsx)(JustifiedPhotoGrid, { photos: photos, photoDimensions: photoDimensions, loadPhotoDimensions: loadPhotoDimensions, createJustifiedRows: createJustifiedRows, renderJustifiedPhotoItem: renderJustifiedPhotoItem, renderSimplePhotoItem: renderPhotoItem, textColor: textColor, containerWidth: containerWidth })] }); }, [filteredFiles, isDarkTheme, textColor, user?.id, targetUserId, uploading, primaryColor, handleFileUpload, refreshing, loadFiles, loadingDimensions, photoDimensions, loadPhotoDimensions, createJustifiedRows, renderJustifiedPhotoItem]); // Separate component for the photo grid to optimize rendering const JustifiedPhotoGrid = /*#__PURE__*/_react.default.memo(({ photos, photoDimensions, loadPhotoDimensions, createJustifiedRows, renderJustifiedPhotoItem, renderSimplePhotoItem, textColor, containerWidth }) => { // Load dimensions for new photos _react.default.useEffect(() => { loadPhotoDimensions(photos); }, [photos.map(p => p.id).join(','), loadPhotoDimensions]); // Group photos by date const photosByDate = _react.default.useMemo(() => { return photos.reduce((groups, photo) => { const date = new Date(photo.uploadDate).toDateString(); if (!groups[date]) { groups[date] = []; } groups[date].push(photo); return groups; }, {}); }, [photos]); const sortedDates = _react.default.useMemo(() => { return Object.keys(photosByDate).sort((a, b) => new Date(b).getTime() - new Date(a).getTime()); }, [photosByDate]); return /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, { children: sortedDates.map(date => { const dayPhotos = photosByDate[date]; const justifiedRows = createJustifiedRows(dayPhotos, containerWidth); return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.photoDateSection, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.photoDateHeader, { color: textColor }], children: new Date(date).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.justifiedPhotoGrid, children: justifiedRows.map((row, rowIndex) => { // Calculate row height based on available width const gap = 4; let totalAspectRatio = 0; // Calculate total aspect ratio for this row row.forEach(photo => { const dimensions = photoDimensions[photo.id]; const aspectRatio = dimensions ? dimensions.width / dimensions.height : 1.33; // Default 4:3 ratio totalAspectRatio += aspectRatio; }); // Calculate the height that makes the row fill the available width // Account for photoScrollContainer padding (32px total) and gaps between photos const scrollContainerPadding = 32; const availableWidth = containerWidth - scrollContainerPadding - gap * (row.length - 1); const calculatedHeight = availableWidth / totalAspectRatio; // Clamp height for visual consistency const rowHeight = Math.max(120, Math.min(calculatedHeight, 300)); return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.justifiedPhotoRow, { height: rowHeight, maxWidth: containerWidth - 32, // Account for scroll container padding gap: 4 // Add horizontal gap between photos in row }], children: row.map((photo, photoIndex) => { const dimensions = photoDimensions[photo.id]; const aspectRatio = dimensions ? dimensions.width / dimensions.height : 1.33; // Default 4:3 ratio const photoWidth = rowHeight * aspectRatio; const isLast = photoIndex === row.length - 1; return renderJustifiedPhotoItem(photo, photoWidth, rowHeight, isLast); }) }, `row-${rowIndex}`); }) })] }, date); }) }); }); const renderPhotoItem = (photo, index) => { const downloadUrl = oxyServices.getFileDownloadUrl(photo.id); // Calculate photo item width based on actual container size from bottom sheet let itemsPerRow = 3; // Default for mobile if (containerWidth > 768) itemsPerRow = 6; // Tablet/Desktop else if (containerWidth > 480) itemsPerRow = 4; // Large mobile // Account for the photoScrollContainer padding (16px on each side = 32px total) const scrollContainerPadding = 32; // Total horizontal padding from photoScrollContainer const gaps = (itemsPerRow - 1) * 4; // Gap between items const availableWidth = containerWidth - scrollContainerPadding; const itemWidth = (availableWidth - gaps) / itemsPerRow; return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: [styles.photoItem, { width: itemWidth, height: itemWidth }], onPress: () => handleFileOpen(photo), activeOpacity: 0.8, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.photoContainer, children: _reactNative.Platform.OS === 'web' ? /*#__PURE__*/(0, _jsxRuntime.jsx)("img", { src: downloadUrl, alt: photo.filename, style: { width: '100%', height: '100%', objectFit: 'cover', borderRadius: 8, transition: 'transform 0.2s ease' }, loading: "lazy", onError: e => { console.error('Photo failed to load:', e); // Could replace with placeholder image }, onMouseEnter: e => { e.currentTarget.style.transform = 'scale(1.02)'; }, onMouseLeave: e => { e.currentTarget.style.transform = 'scale(1)'; } }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, { source: { uri: downloadUrl }, style: styles.photoImage, resizeMode: "cover", onError: e => { console.error('Photo failed to load:', e); } }) }) }, photo.id); }; const renderFileDetailsModal = () => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, { visible: showFileDetails, animationType: "slide", presentationStyle: "pageSheet", onRequestClose: () => setShowFileDetails(false), children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.modalContainer, { backgroundColor }], children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.modalHeader, { borderBottomColor: borderColor }], children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: styles.modalCloseButton, onPress: () => setShowFileDetails(false), children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "close", size: 24, color: textColor }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.modalTitle, { color: textColor }], children: "File Details" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.modalPlaceholder })] }), selectedFile && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, { style: styles.modalContent, children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.fileDetailCard, { backgroundColor: secondaryBackgroundColor, borderColor }], children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.fileDetailIcon, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: getFileIcon(selectedFile.contentType), size: 64, color: primaryColor }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.fileDetailName, { color: textColor }], children: selectedFile.filename }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.fileDetailInfo, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.detailRow, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }], children: "Size:" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.detailValue, { color: textColor }], children: formatFileSize(selectedFile.length) })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.detailRow, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }], children: "Type:" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.detailValue, { color: textColor }], children: selectedFile.contentType })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.detailRow, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }], children: "Uploaded:" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.detailValue, { color: textColor }], children: new Date(selectedFile.uploadDate).toLocaleString() })] }), selectedFile.metadata?.description && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.detailRow, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }], children: "Description:" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.detailValue, { color: textColor }], children: selectedFile.metadata.description })] })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.modalActions, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, { style: [styles.modalActionButton, { backgroundColor: primaryColor }], onPress: () => { handleFileDownload(selectedFile.id, selectedFile.filename); setShowFileDetails(false); }, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "download", size: 20, color: "#FFFFFF" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: styles.modalActionText, children: "Download" })] }), user?.id === targetUserId && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, { style: [styles.modalActionButton, { backgroundColor: dangerColor }], onPress: () => { setShowFileDetails(false); handleFileDelete(selectedFile.id, selectedFile.filename); }, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "trash", size: 20, color: "#FFFFFF" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: styles.modalActionText, children: "Delete" })] })] })] }) })] }) }); const renderFileViewer = () => { if (!openedFile) return null; const isImage = openedFile.contentType.startsWith('image/'); const isText = openedFile.contentType.startsWith('text/') || openedFile.contentType.includes('json') || openedFile.contentType.includes('xml') || openedFile.contentType.includes('javascript') || openedFile.contentType.includes('typescript'); const isPDF = openedFile.contentType.includes('pdf'); const isVideo = openedFile.contentType.startsWith('video/'); const isAudio = openedFile.contentType.startsWith('audio/'); return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.fileViewerContainer, { backgroundColor }], children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.fileViewerHeader, { borderBottomColor: borderColor }], children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: styles.backButton, onPress: handleCloseFile, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "arrow-back", size: 24, color: textColor }) }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.fileViewerTitleContainer, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.fileViewerTitle, { color: textColor }], numberOfLines: 1, children: openedFile.filename }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, { style: [styles.fileViewerSubtitle, { color: isDarkTheme ? '#BBBBBB' : '#666666' }], children: [formatFileSize(openedFile.length), " \u2022 ", openedFile.contentType] })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.fileViewerActions, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: [styles.actionButton, { backgroundColor: isDarkTheme ? '#333333' : '#F0F0F0' }], onPress: () => handleFileDownload(openedFile.id, openedFile.filename), children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "download", size: 20, color: primaryColor }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNat