@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
JavaScript
"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