From ac4ab3c7928ab1e9fbfd86148963712a3166ae3f Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 15 Nov 2025 14:05:40 +0100 Subject: [PATCH] bug-fixed mobiled --- src/components/PostEditor/Uploader.tsx | 161 +++++-------------------- src/services/media-upload.service.ts | 133 ++------------------ 2 files changed, 40 insertions(+), 254 deletions(-) diff --git a/src/components/PostEditor/Uploader.tsx b/src/components/PostEditor/Uploader.tsx index aba2dd8..5656d22 100644 --- a/src/components/PostEditor/Uploader.tsx +++ b/src/components/PostEditor/Uploader.tsx @@ -23,155 +23,54 @@ export default function Uploader({ const fileInputRef = useRef(null) const handleFileChange = async (event: React.ChangeEvent) => { - // Stop propagation to prevent event bubbling - event.stopPropagation() - - if (!event.target.files) { - if (fileInputRef.current) { - fileInputRef.current.value = '' - } - return - } + if (!event.target.files) return const abortControllerMap = new Map() - // Wrap in try-catch to handle any synchronous errors - try { - for (const file of event.target.files) { - const abortController = new AbortController() - abortControllerMap.set(file, abortController) - try { - onUploadStart?.(file, () => abortController.abort()) - } catch (error) { - logger.error('Error in onUploadStart callback', { error, fileName: file.name }) - } - } + for (const file of event.target.files) { + const abortController = new AbortController() + abortControllerMap.set(file, abortController) + onUploadStart?.(file, () => abortController.abort()) + } - for (const file of event.target.files) { - try { - logger.debug('Starting file upload', { fileName: file.name, fileType: file.type, fileSize: file.size }) - const abortController = abortControllerMap.get(file) - const result = await mediaUpload.upload(file, { - onProgress: (p) => { - try { - logger.debug('Upload progress', { fileName: file.name, progress: p }) - onProgress?.(file, p) - } catch (error) { - logger.error('Error in onProgress callback', { error, fileName: file.name }) - } - }, - signal: abortController?.signal - }) - logger.debug('File upload successful', { fileName: file.name, url: result.url }) - try { - onUploadSuccess(result) - } catch (error) { - logger.error('Error in onUploadSuccess callback', { error, fileName: file.name }) - toast.error('Failed to process uploaded file') - } - try { - onUploadEnd?.(file) - } catch (error) { - logger.error('Error in onUploadEnd callback', { error, fileName: file.name }) - } - } catch (error) { - logger.error('Error uploading file', { - error, - fileName: file.name, - fileType: file.type, - fileSize: file.size, - errorMessage: error instanceof Error ? error.message : String(error), - errorStack: error instanceof Error ? error.stack : undefined - }) - const message = (error as Error).message - if (message !== UPLOAD_ABORTED_ERROR_MSG) { - toast.error(`Failed to upload file: ${message}`) - } - try { - onUploadEnd?.(file) - } catch (endError) { - logger.error('Error in onUploadEnd callback during error handling', { error: endError }) - } + for (const file of event.target.files) { + try { + const abortController = abortControllerMap.get(file) + const result = await mediaUpload.upload(file, { + onProgress: (p) => onProgress?.(file, p), + signal: abortController?.signal + }) + onUploadSuccess(result) + onUploadEnd?.(file) + } catch (error) { + logger.error('Error uploading file', { error, file: file.name }) + const message = (error as Error).message + if (message !== UPLOAD_ABORTED_ERROR_MSG) { + toast.error(`Failed to upload file: ${message}`) } - } - } catch (error) { - // Catch any unexpected errors in the outer try-catch - logger.error('Unexpected error in handleFileChange', { - error, - errorMessage: error instanceof Error ? error.message : String(error), - errorStack: error instanceof Error ? error.stack : undefined - }) - toast.error('An unexpected error occurred during file upload') - } finally { - // Always reset the file input value - if (fileInputRef.current) { - fileInputRef.current.value = '' + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + onUploadEnd?.(file) } } } - const handleUploadClick = (e: React.MouseEvent | React.KeyboardEvent) => { - e.preventDefault() - e.stopPropagation() - if (e.nativeEvent) { - e.nativeEvent.stopImmediatePropagation() - } - - // Prevent any form submission - if ('currentTarget' in e && e.currentTarget instanceof HTMLElement) { - const form = e.currentTarget.closest('form') - if (form) { - e.preventDefault() - e.stopPropagation() - } - } - - try { - if (fileInputRef.current) { - fileInputRef.current.value = '' // clear the value so that the same file can be uploaded again - fileInputRef.current.click() - } - } catch (error) { - logger.error('Error triggering file input click', { error }) - toast.error('Failed to open file picker') + const handleUploadClick = () => { + if (fileInputRef.current) { + fileInputRef.current.value = '' // clear the value so that the same file can be uploaded again + fileInputRef.current.click() } } return ( -
{ - // Only stop propagation, don't prevent default to avoid interfering with file input - e.stopPropagation() - }} - > -
{ - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault() - e.stopPropagation() - handleUploadClick(e) - } - }} - onMouseDown={(e) => { - // Prevent any default behavior on mouse down - e.preventDefault() - }} - > - {children} -
+
+
{children}
{ - // Prevent event bubbling - e.stopPropagation() - }} accept={accept} multiple /> diff --git a/src/services/media-upload.service.ts b/src/services/media-upload.service.ts index f4c3575..e39442a 100644 --- a/src/services/media-upload.service.ts +++ b/src/services/media-upload.service.ts @@ -146,64 +146,14 @@ class MediaUploadService { const auth = await client.signHttpAuth(uploadUrl, 'POST', 'Uploading media file') - // Check if service worker might be interfering - const hasServiceWorker = 'serviceWorker' in navigator && navigator.serviceWorker.controller - const isFirefoxMobile = /Firefox/i.test(navigator.userAgent) && /Mobile/i.test(navigator.userAgent) - - if (hasServiceWorker) { - console.warn('⚠️ Service worker is active - this may interfere with uploads on mobile', { uploadUrl, isFirefoxMobile }) - } - - // For Firefox mobile, add a cache-busting parameter to help bypass service worker - // Also add a timestamp to ensure the request is unique - let finalUploadUrl = uploadUrl as string - if (isFirefoxMobile && hasServiceWorker) { - const separator = finalUploadUrl.includes('?') ? '&' : '?' - finalUploadUrl = `${finalUploadUrl}${separator}_nocache=${Date.now()}&_bypass_sw=1` - console.log('🔧 Firefox mobile: Added cache-busting parameters to upload URL', { finalUploadUrl }) - } - // Use XMLHttpRequest for upload progress support - // Note: XMLHttpRequest should bypass service workers, but on mobile Firefox this isn't always reliable - // We add cache-busting parameters for Firefox mobile to help bypass service worker const result = await new Promise<{ url: string; tags: string[][] }>((resolve, reject) => { const xhr = new XMLHttpRequest() - xhr.open('POST', finalUploadUrl, true) // async=true to ensure it's not cached + xhr.open('POST', uploadUrl as string) xhr.responseType = 'json' xhr.setRequestHeader('Authorization', auth) - // Add headers to prevent caching on Firefox mobile - if (isFirefoxMobile) { - xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, must-revalidate') - xhr.setRequestHeader('Pragma', 'no-cache') - xhr.setRequestHeader('Expires', '0') - } - - // Log upload start for debugging - console.log('📤 Starting upload', { - uploadUrl, - fileSize: file.size, - fileType: file.type, - fileName: file.name, - hasServiceWorker, - userAgent: navigator.userAgent - }) - - // Set a timeout (60 seconds for uploads) - xhr.timeout = 60000 - - // Track if we've already handled the response to avoid double handling - let isHandled = false - - const handleError = (error: Error | string) => { - if (isHandled) return - isHandled = true - const errorMessage = error instanceof Error ? error.message : error - reject(new Error(errorMessage)) - } - + const handleAbort = () => { - if (isHandled) return - isHandled = true try { xhr.abort() } catch { @@ -211,102 +161,39 @@ class MediaUploadService { } reject(new Error(UPLOAD_ABORTED_ERROR_MSG)) } - if (options?.signal) { if (options.signal.aborted) { return handleAbort() } options.signal.addEventListener('abort', handleAbort, { once: true }) } - - // Handle timeout - xhr.ontimeout = () => { - console.error('⏱️ Upload timeout', { uploadUrl, fileSize: file.size }) - handleError('Upload timeout - the connection took too long. Please check your network connection and try again.') - } - - // Handle abort - xhr.onabort = () => { - if (!isHandled) { - isHandled = true - reject(new Error(UPLOAD_ABORTED_ERROR_MSG)) - } - } - - // Handle network errors - xhr.onerror = () => { - // Try to get more details about the error - // Status 0 can mean: CORS failure, network error, service worker blocking, or connection refused - let errorMessage = 'Network error' - if (xhr.status === 0) { - // On mobile, status 0 often means CORS or service worker issue, not necessarily connection failure - if (isFirefoxMobile) { - errorMessage = 'Upload failed on Firefox mobile - this is often due to a service worker issue. Try: 1) Refreshing the page, 2) Clearing browser cache, or 3) Disabling service workers in Firefox settings.' - } else { - errorMessage = 'Upload failed - this may be due to a service worker or CORS issue. Please try refreshing the page or clearing your browser cache.' - } - } else if (xhr.status >= 400) { - errorMessage = `Upload failed with status ${xhr.status}: ${xhr.statusText || 'Unknown error'}` - } - console.error('❌ Upload network error', { - uploadUrl, - status: xhr.status, - statusText: xhr.statusText, - readyState: xhr.readyState, - fileSize: file.size, - errorMessage - }) - handleError(errorMessage) - } - + xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const percent = Math.round((event.loaded / event.total) * 100) - console.log('📊 Upload progress', { percent, loaded: event.loaded, total: event.total }) options?.onProgress?.(percent) } } - + xhr.onerror = () => reject(new Error('Network error')) xhr.onload = () => { - if (isHandled) return - if (xhr.status >= 200 && xhr.status < 300) { + const data = xhr.response try { - const data = xhr.response - // Handle case where response might be a string that needs parsing - let parsedData = data - if (typeof data === 'string') { - try { - parsedData = JSON.parse(data) - } catch { - handleError('Invalid response format from upload server') - return - } - } - - const tags = z.array(z.array(z.string())).parse(parsedData?.nip94_event?.tags ?? []) + const tags = z.array(z.array(z.string())).parse(data?.nip94_event?.tags ?? []) const url = tags.find(([tagName]: string[]) => tagName === 'url')?.[1] if (url) { - console.log('✅ Upload successful', { url, uploadUrl }) - isHandled = true resolve({ url, tags }) } else { - console.error('❌ No URL in upload response', { parsedData, tags }) - handleError('No url found in upload response') + reject(new Error('No url found')) } } catch (e) { - handleError(e instanceof Error ? e : new Error('Failed to parse upload response')) + reject(e as Error) } } else { - handleError(`Upload failed with status ${xhr.status}: ${xhr.statusText || 'Unknown error'}`) + reject(new Error(xhr.status.toString() + ' ' + xhr.statusText)) } } - - try { - xhr.send(formData) - } catch (error) { - handleError(error instanceof Error ? error : new Error('Failed to send upload request')) - } + xhr.send(formData) }) return result