diff --git a/src/components/Componentes/question-file.tsx b/src/components/Componentes/question-file.tsx
index b662781..a089c80 100644
--- a/src/components/Componentes/question-file.tsx
+++ b/src/components/Componentes/question-file.tsx
@@ -164,6 +164,20 @@ function isImageFile(
return /\.(jpg|jpeg|png|webp|gif|svg|bmp|avif)$/i.test(nameToCheck);
}
+function base64ToFile(dataUrl: string, filename = "document"): File {
+ const parts = dataUrl.split(",");
+ const mimeMatch = parts[0]?.match(/:(.*?);/);
+ const mime = mimeMatch ? mimeMatch[1] : "application/octet-stream";
+ 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 type UploadedDoc = {
url: string;
name: string;
@@ -335,15 +349,12 @@ export function QuestionFile({
previewUrl: resolved,
});
} else if (f?.base64) {
- const b64 = f.base64;
- fetch(b64)
- .then((res) => res.blob())
- .then((blob) => {
- const fileObj = new File([blob], f.name ?? "document", {
- type: blob.type,
- });
- uploadTmpMediaMutation.mutate(fileObj);
- });
+ try {
+ const fileObj = base64ToFile(f.base64, f.name ?? "document");
+ uploadTmpMediaMutation.mutate(fileObj);
+ } catch (e) {
+ console.error("Failed to decode base64 file:", e);
+ }
}
}
@@ -618,7 +629,7 @@ export function QuestionFile({
{isImg && doc.previewUrl && !hasThumbError ? (
{
setImgErrorSet((prev) => new Set(prev).add(index));
}}
diff --git a/src/components/Componentes/question-photo.tsx b/src/components/Componentes/question-photo.tsx
index 1d84377..8292006 100644
--- a/src/components/Componentes/question-photo.tsx
+++ b/src/components/Componentes/question-photo.tsx
@@ -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]);
+ setToastMessage(
+ t["Failed to load image. Please try uploading again."] ??
+ "Failed to load image. Please try uploading again.",
+ );
+ }, [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);
- })
- .then((res) => {
- if (res?.path) {
- setAnswerValue(question, res.path);
- setLocalPreviewUrl((prev) => prev || res.path);
- setHasImageError(false);
- pendingDeferredRef.current?.resolve(res.path);
- } else {
+ 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);
+ 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 to server error:", err);
handleUploadFailure();
- pendingDeferredRef.current?.resolve();
- }
- pendingDeferredRef.current = null;
- })
- .catch((err) => {
- console.error("Flutter photo upload error:", err);
- handleUploadFailure();
- pendingDeferredRef.current?.reject(err);
- pendingDeferredRef.current = null;
- });
- registerPendingUpload?.(question.id, uploadPromise);
+ 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"
/>