Browse Source

bug-fixed mobiled

imwald
Silberengel 4 months ago
parent
commit
ac4ab3c792
  1. 161
      src/components/PostEditor/Uploader.tsx
  2. 129
      src/services/media-upload.service.ts

161
src/components/PostEditor/Uploader.tsx

@ -23,155 +23,54 @@ export default function Uploader({ @@ -23,155 +23,54 @@ export default function Uploader({
const fileInputRef = useRef<HTMLInputElement>(null)
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
// 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<File, AbortController>()
// 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 (
<div
className={className}
onClick={(e) => {
// Only stop propagation, don't prevent default to avoid interfering with file input
e.stopPropagation()
}}
>
<div
onClick={handleUploadClick}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
e.stopPropagation()
handleUploadClick(e)
}
}}
onMouseDown={(e) => {
// Prevent any default behavior on mouse down
e.preventDefault()
}}
>
{children}
</div>
<div className={className}>
<div onClick={handleUploadClick}>{children}</div>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
onChange={handleFileChange}
onClick={(e) => {
// Prevent event bubbling
e.stopPropagation()
}}
accept={accept}
multiple
/>

129
src/services/media-upload.service.ts

@ -146,64 +146,14 @@ class MediaUploadService { @@ -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,7 +161,6 @@ class MediaUploadService { @@ -211,7 +161,6 @@ class MediaUploadService {
}
reject(new Error(UPLOAD_ABORTED_ERROR_MSG))
}
if (options?.signal) {
if (options.signal.aborted) {
return handleAbort()
@ -219,94 +168,32 @@ class MediaUploadService { @@ -219,94 +168,32 @@ class MediaUploadService {
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

Loading…
Cancel
Save