UNPKG

@oxyhq/services

Version:

OxyHQ Expo/React Native SDK — UI components, screens, and native features

260 lines (244 loc) 8.88 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.confirmAction = confirmAction; exports.convertDocumentPickerAssetToFile = convertDocumentPickerAssetToFile; exports.createAvatarPickerHandler = createAvatarPickerHandler; exports.formatFileSize = formatFileSize; exports.getFileIcon = getFileIcon; exports.getSafeDownloadUrl = getSafeDownloadUrl; exports.uploadFileRaw = uploadFileRaw; var _reactNative = require("react-native"); var _sonner = require("../../lib/sonner"); var _avatarUtils = require("./avatarUtils.js"); /** * Format file size in bytes to human-readable string */ function 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 Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Get icon name for file based on content type */ function 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'; } /** * Unified confirmation dialog - uses Alert.alert for all platforms */ function confirmAction(message, title, confirmText = 'OK', cancelText = 'Cancel') { return new Promise(resolve => { _reactNative.Alert.alert(title || 'Confirm', message, [{ text: cancelText, style: 'cancel', onPress: () => resolve(false) }, { text: confirmText, onPress: () => resolve(true) }], { cancelable: true, onDismiss: () => resolve(false) }); }); } /** * Convert DocumentPicker asset to File object * Handles both web (native File API) and mobile (URI-based) file sources * Expo 54 compatible - works across all platforms */ async function convertDocumentPickerAssetToFile(doc, index) { try { let file = null; // Priority 1: Use doc.file if available (web native File API) // This is the most efficient path as it doesn't require fetching if (doc.file && doc.file instanceof globalThis.File) { file = doc.file; // Ensure file has required properties if (!file.name && doc.name) { // Create new File with proper name if missing file = new globalThis.File([file], doc.name, { type: file.type || doc.mimeType || 'application/octet-stream' }); } // Preserve URI for preview if available (useful for mobile previews) if (doc.uri) { file.uri = doc.uri; } return file; } // Priority 2: Use uri to create File using Expo 54 FileSystem API // This path handles mobile file URIs (file://, content://) and web blob URLs if (doc.uri) { try { // Check if it's a web blob URL - use fetch for those if (doc.uri.startsWith('blob:') || doc.uri.startsWith('http://') || doc.uri.startsWith('https://')) { const response = await fetch(doc.uri); if (!response.ok) { throw new Error(`Failed to fetch file: ${response.statusText}`); } const blob = await response.blob(); const fileName = doc.name || `file-${index + 1}`; const fileType = doc.mimeType || blob.type || 'application/octet-stream'; file = new globalThis.File([blob], fileName, { type: fileType }); // Preserve URI for preview file.uri = doc.uri; return file; } // For mobile file URIs (file://, content://), use fetch to get blob // React Native's Blob doesn't support Uint8Array directly, so we use fetch const fileName = doc.name || `file-${index + 1}`; const fileType = doc.mimeType || 'application/octet-stream'; // Use fetch to get the file as a blob (works with file:// and content:// URIs in React Native) const response = await fetch(doc.uri); if (!response.ok) { throw new Error(`Failed to fetch file: ${response.statusText}`); } const blob = await response.blob(); file = new globalThis.File([blob], fileName, { type: fileType }); // Preserve URI for preview (especially important for mobile) file.uri = doc.uri; return file; } catch (error) { console.error('Failed to read file from URI:', error); throw new Error(`Failed to load file: ${error.message || 'Unknown error'}`); } } // No file or URI available - this shouldn't happen with Expo 54 throw new Error('Missing file data (no file or uri property)'); } catch (error) { console.error('Error converting document to file:', error); throw error; } } /** * Helper to safely request a thumbnail variant only for image mime types. * Prevents backend warnings: "Variant thumb not supported for mime application/pdf". * * @param file - File metadata * @param variant - Variant type (default: 'thumb') * @param getFileDownloadUrl - Function to get download URL from oxyServices */ function getSafeDownloadUrl(file, variant = 'thumb', getFileDownloadUrl) { const isImage = file.contentType.startsWith('image/'); const isVideo = file.contentType.startsWith('video/'); // Prefer explicit variant key if variants metadata present if (file.variants && file.variants.length > 0) { // For videos, try 'poster' regardless of requested variant if (isVideo) { const poster = file.variants.find(v => v.type === 'poster'); if (poster) return getFileDownloadUrl(file.id, 'poster'); } if (isImage) { const desired = file.variants.find(v => v.type === variant); if (desired) return getFileDownloadUrl(file.id, variant); } } if (isImage) { return getFileDownloadUrl(file.id, variant); } if (isVideo) { // Fallback to poster if backend supports implicit generation try { return getFileDownloadUrl(file.id, 'poster'); } catch { return getFileDownloadUrl(file.id); } } // Other mime types: no variant return getFileDownloadUrl(file.id); } /** * Upload file raw - helper function for file uploads */ async function uploadFileRaw(file, userId, oxyServices, visibility) { return await oxyServices.uploadRawFile(file, visibility); } /** * Configuration for creating an avatar picker handler */ /** * Creates a reusable avatar picker handler function. * * This function navigates to the FileManagement screen and handles: * - Image file validation * - File visibility update to public * - Profile avatar update via mutation * - Success/error toast notifications * * @example * ```tsx * const openAvatarPicker = createAvatarPickerHandler({ * navigate, * oxyServices, * updateProfileMutation, * onAvatarSelected: setAvatarFileId, * t, * contextName: 'AccountSettings' * }); * * <TouchableOpacity onPress={openAvatarPicker}> * <Text>Change Avatar</Text> * </TouchableOpacity> * ``` */ function createAvatarPickerHandler(config) { const { navigate, oxyServices, updateProfileMutation, onAvatarSelected, t, contextName = 'AvatarPicker' } = config; return () => { if (!navigate) { console.warn(`[${contextName}] navigate function is not available`); return; } navigate('FileManagement', { selectMode: true, multiSelect: false, disabledMimeTypes: ['video/', 'audio/', 'application/pdf'], afterSelect: 'none', // Don't navigate away - stay on current screen onSelect: async file => { if (!file.contentType.startsWith('image/')) { _sonner.toast.error(t('editProfile.toasts.selectImage') || 'Please select an image file'); return; } try { // Update file visibility to public for avatar await (0, _avatarUtils.updateAvatarVisibility)(file.id, oxyServices, contextName); // Update local state if callback provided if (onAvatarSelected) { onAvatarSelected(file.id); } // Update user using TanStack Query mutation await updateProfileMutation.mutateAsync({ avatar: file.id }); _sonner.toast.success(t('editProfile.toasts.avatarUpdated') || 'Avatar updated'); } catch (e) { _sonner.toast.error(e.message || t('editProfile.toasts.updateAvatarFailed') || 'Failed to update avatar'); } } }); }; } //# sourceMappingURL=fileManagement.js.map