|
|
|
@ -17,6 +17,20 @@ type QuestionPhotoProps = { |
|
|
|
disabled?: boolean; |
|
|
|
}; |
|
|
|
|
|
|
|
function base64ToFile(dataUrl: string, filename = "photo.jpg"): File { |
|
|
|
const parts = dataUrl.split(","); |
|
|
|
const mimeMatch = parts[0]?.match(/:(.*?);/); |
|
|
|
const mime = mimeMatch ? mimeMatch[1] : "image/jpeg"; |
|
|
|
const base64Data = parts[1] || parts[0]; |
|
|
|
const binaryString = atob(base64Data); |
|
|
|
const len = binaryString.length; |
|
|
|
const bytes = new Uint8Array(len); |
|
|
|
for (let i = 0; i < len; i++) { |
|
|
|
bytes[i] = binaryString.charCodeAt(i); |
|
|
|
} |
|
|
|
return new File([bytes], filename, { type: mime }); |
|
|
|
} |
|
|
|
|
|
|
|
export function QuestionPhoto({ |
|
|
|
question, |
|
|
|
description, |
|
|
|
@ -37,7 +51,6 @@ export function QuestionPhoto({ |
|
|
|
? question.extras.options.join(",") |
|
|
|
: "image/*"; |
|
|
|
|
|
|
|
|
|
|
|
const { getAnswerValue, setAnswerValue, registerPendingUpload } = |
|
|
|
useQuestionAnswers(); |
|
|
|
const storedValue = getAnswerValue(question); |
|
|
|
@ -61,6 +74,7 @@ export function QuestionPhoto({ |
|
|
|
const handleUploadFailure = useCallback( |
|
|
|
(errorMessage?: string) => { |
|
|
|
setIsUploading(false); |
|
|
|
isInitiatorRef.current = false; |
|
|
|
if (activeBlobUrlRef.current) { |
|
|
|
URL.revokeObjectURL(activeBlobUrlRef.current); |
|
|
|
activeBlobUrlRef.current = null; |
|
|
|
@ -92,15 +106,11 @@ export function QuestionPhoto({ |
|
|
|
if (fileInputRef.current) { |
|
|
|
fileInputRef.current.value = ""; |
|
|
|
} |
|
|
|
// Only show the toast if the user was actively uploading;
|
|
|
|
// stale stored URLs from previous sessions fail silently.
|
|
|
|
if (isUploading || isInitiatorRef.current) { |
|
|
|
setToastMessage( |
|
|
|
t["Failed to load image. Please try uploading again."] ?? |
|
|
|
"Failed to load image. Please try uploading again.", |
|
|
|
); |
|
|
|
} |
|
|
|
}, [question, setAnswerValue, t, isUploading]); |
|
|
|
}, [question, setAnswerValue, t]); |
|
|
|
|
|
|
|
const uploadTmpMediaMutation = useUploadTmpMediaMutation({ |
|
|
|
onSuccess: (response) => { |
|
|
|
@ -114,6 +124,7 @@ export function QuestionPhoto({ |
|
|
|
handleUploadFailure(); |
|
|
|
} |
|
|
|
setIsUploading(false); |
|
|
|
isInitiatorRef.current = false; |
|
|
|
}, |
|
|
|
onError: (error) => { |
|
|
|
console.error("Photo upload error:", error); |
|
|
|
@ -140,57 +151,70 @@ export function QuestionPhoto({ |
|
|
|
setIsUploading(true); |
|
|
|
setHasImageError(false); |
|
|
|
setToastMessage(null); |
|
|
|
if (event.data?.files?.[0]?.base64) { |
|
|
|
const b64 = event.data.files[0].base64; |
|
|
|
setLocalPreviewUrl(b64); |
|
|
|
} |
|
|
|
break; |
|
|
|
case "completed": { |
|
|
|
setIsUploading(false); |
|
|
|
isInitiatorRef.current = false; |
|
|
|
const file = event.data?.files?.[0]; |
|
|
|
const remoteUrl = |
|
|
|
file?.path || |
|
|
|
file?.url || |
|
|
|
(file?.data?.path as string | undefined) || |
|
|
|
(file?.data?.url as string | undefined); |
|
|
|
|
|
|
|
if (remoteUrl) { |
|
|
|
setIsUploading(false); |
|
|
|
isInitiatorRef.current = false; |
|
|
|
setAnswerValue(question, remoteUrl); |
|
|
|
setLocalPreviewUrl((prev) => prev || remoteUrl); |
|
|
|
setHasImageError(false); |
|
|
|
pendingDeferredRef.current?.resolve(remoteUrl); |
|
|
|
pendingDeferredRef.current = null; |
|
|
|
} else if (file?.base64) { |
|
|
|
const b64 = file.base64; |
|
|
|
setLocalPreviewUrl(b64); |
|
|
|
const uploadPromise = fetch(b64) |
|
|
|
.then((res) => res.blob()) |
|
|
|
.then((blob) => { |
|
|
|
const f = new File([blob], file.name ?? "photo.jpg", { |
|
|
|
type: blob.type || "image/jpeg", |
|
|
|
}); |
|
|
|
return uploadTmpMediaMutation.mutateAsync(f); |
|
|
|
}) |
|
|
|
setIsUploading(true); |
|
|
|
setHasImageError(false); |
|
|
|
setToastMessage(null); |
|
|
|
try { |
|
|
|
const f = base64ToFile(file.base64, file.name ?? "photo.jpg"); |
|
|
|
if (activeBlobUrlRef.current) { |
|
|
|
URL.revokeObjectURL(activeBlobUrlRef.current); |
|
|
|
} |
|
|
|
const objectUrl = URL.createObjectURL(f); |
|
|
|
activeBlobUrlRef.current = objectUrl; |
|
|
|
setLocalPreviewUrl(objectUrl); |
|
|
|
|
|
|
|
const uploadPromise = uploadTmpMediaMutation |
|
|
|
.mutateAsync(f) |
|
|
|
.then((res) => { |
|
|
|
if (res?.path) { |
|
|
|
setAnswerValue(question, res.path); |
|
|
|
setLocalPreviewUrl((prev) => prev || res.path); |
|
|
|
setHasImageError(false); |
|
|
|
if (!activeBlobUrlRef.current) { |
|
|
|
setLocalPreviewUrl(res.path); |
|
|
|
} |
|
|
|
pendingDeferredRef.current?.resolve(res.path); |
|
|
|
} else { |
|
|
|
handleUploadFailure(); |
|
|
|
pendingDeferredRef.current?.resolve(); |
|
|
|
} |
|
|
|
setIsUploading(false); |
|
|
|
isInitiatorRef.current = false; |
|
|
|
pendingDeferredRef.current = null; |
|
|
|
}) |
|
|
|
.catch((err) => { |
|
|
|
console.error("Flutter photo upload error:", err); |
|
|
|
console.error("Flutter photo upload to server error:", err); |
|
|
|
handleUploadFailure(); |
|
|
|
pendingDeferredRef.current?.reject(err); |
|
|
|
pendingDeferredRef.current = null; |
|
|
|
}); |
|
|
|
registerPendingUpload?.(question.id, uploadPromise); |
|
|
|
} catch (convErr) { |
|
|
|
console.error("Failed to decode base64 photo:", convErr); |
|
|
|
handleUploadFailure(); |
|
|
|
pendingDeferredRef.current?.reject(convErr); |
|
|
|
pendingDeferredRef.current = null; |
|
|
|
} |
|
|
|
} else { |
|
|
|
setIsUploading(false); |
|
|
|
isInitiatorRef.current = false; |
|
|
|
pendingDeferredRef.current?.resolve(); |
|
|
|
pendingDeferredRef.current = null; |
|
|
|
} |
|
|
|
@ -208,7 +232,7 @@ export function QuestionPhoto({ |
|
|
|
case "failed": |
|
|
|
setIsUploading(false); |
|
|
|
isInitiatorRef.current = false; |
|
|
|
handleUploadFailure(); |
|
|
|
handleUploadFailure(event.message || event.data?.message); |
|
|
|
pendingDeferredRef.current?.reject(new Error("Upload failed")); |
|
|
|
pendingDeferredRef.current = null; |
|
|
|
break; |
|
|
|
@ -265,6 +289,9 @@ export function QuestionPhoto({ |
|
|
|
allowedExtensions: |
|
|
|
extensions.length > 0 ? extensions : ["jpg", "jpeg", "png", "webp"], |
|
|
|
maxBytes: 10_485_760, // 10 MB
|
|
|
|
maxWidth: 1024, |
|
|
|
maxHeight: 1024, |
|
|
|
imageQuality: 85, |
|
|
|
title: |
|
|
|
typeof question.title === "string" ? question.title : "Upload Photo", |
|
|
|
}); |
|
|
|
@ -374,7 +401,7 @@ export function QuestionPhoto({ |
|
|
|
className="sr-only" |
|
|
|
/> |
|
|
|
<label |
|
|
|
htmlFor={inputId} |
|
|
|
htmlFor={isInFlutterWebView() ? undefined : inputId} |
|
|
|
onClick={handleLabelClick} |
|
|
|
className="flex w-full cursor-pointer flex-col items-center" |
|
|
|
> |
|
|
|
|