Browse Source

feat(upload): isolate photo and file uploads via requestId and connect to tmp-media service

master
mortezaei 2 days ago
parent
commit
83bc7cbf51
  1. 30
      src/components/Componentes/question-file.tsx
  2. 23
      src/components/Componentes/question-photo.tsx
  3. 1
      src/lib/webview-actions.ts
  4. 4
      src/types/window.d.ts

30
src/components/Componentes/question-file.tsx

@ -1,7 +1,7 @@
"use client";
import Image from "next/image";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import type { QuestionField } from "@/lib/schema-adapter";
import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media";
import { getApiRequestUrl } from "@/lib/http";
@ -83,6 +83,7 @@ export function QuestionFile({
const [filePreviewUrl, setFilePreviewUrl] = useState<string | null>(
initialFileUrl,
);
const isInitiatorRef = useRef(false);
const [isFlutterPicking, setIsFlutterPicking] = useState(false);
const acceptedFiles = (question.extras?.options ?? [])
@ -93,6 +94,8 @@ export function QuestionFile({
onSuccess: (response) => {
if (response.path) {
setAnswerValue(question, response.path);
setSelectedFileName(response.name ?? response.path.split("/").pop() ?? "uploaded");
setFilePreviewUrl(response.path);
}
},
onError: (error) => {
@ -108,6 +111,8 @@ export function QuestionFile({
const unsubscribe = window.addFlutterResponseListener?.((event) => {
if (event.action !== "upload_file") return;
if (event.requestId && String(event.requestId) !== String(question.id)) return;
if (!event.requestId && !isInitiatorRef.current) return;
switch (event.status) {
case "picking":
@ -117,20 +122,23 @@ export function QuestionFile({
if (event.data?.files?.[0]) {
const fileName = event.data.files[0].name ?? null;
setSelectedFileName(fileName);
if (fileName) {
setAnswerValue(question, fileName);
}
}
break;
case "progress":
break;
case "completed": {
setIsFlutterPicking(false);
isInitiatorRef.current = false;
const file = event.data?.files?.[0];
if (file?.url) {
setAnswerValue(question, file.url);
setSelectedFileName(file.name ?? "uploaded");
setFilePreviewUrl(file.url);
const remoteUrl =
file?.path ||
file?.url ||
(file?.data?.path as string | undefined) ||
(file?.data?.url as string | undefined);
if (remoteUrl) {
setAnswerValue(question, remoteUrl);
setSelectedFileName(file?.name ?? remoteUrl.split("/").pop() ?? "uploaded");
setFilePreviewUrl(remoteUrl);
} else if (file?.base64) {
const b64 = file.base64;
setSelectedFileName(file.name ?? "upload");
@ -148,9 +156,11 @@ export function QuestionFile({
}
case "cancelled":
setIsFlutterPicking(false);
isInitiatorRef.current = false;
break;
case "failed":
setIsFlutterPicking(false);
isInitiatorRef.current = false;
console.error("upload_file failed:", event.message);
break;
}
@ -168,7 +178,11 @@ export function QuestionFile({
);
const mediaType = resolveMediaType(extensions);
isInitiatorRef.current = true;
setIsFlutterPicking(true);
uploadFile({
requestId: String(question.id),
mediaType,
source: "gallery",
returnAs: "upload",

23
src/components/Componentes/question-photo.tsx

@ -1,7 +1,7 @@
"use client";
import Image from "next/image";
import { type ReactNode, useCallback, useEffect, useId, useState } from "react";
import { type ReactNode, useCallback, useEffect, useId, useRef, useState } from "react";
import type { QuestionField } from "@/lib/schema-adapter";
import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media";
import { getApiRequestUrl } from "@/lib/http";
@ -34,10 +34,12 @@ export function QuestionPhoto({
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const storedValue = getAnswerValue(question);
const isInitiatorRef = useRef(false);
const uploadTmpMediaMutation = useUploadTmpMediaMutation({
onSuccess: (response) => {
if (response.path) {
setAnswerValue(question, response.path);
setLocalPreviewUrl(response.path);
}
},
onError: (error) => {
@ -53,6 +55,8 @@ export function QuestionPhoto({
const unsubscribe = window.addFlutterResponseListener?.((event) => {
if (event.action !== "upload_file") return;
if (event.requestId && String(event.requestId) !== String(question.id)) return;
if (!event.requestId && !isInitiatorRef.current) return;
switch (event.status) {
case "picking":
@ -67,10 +71,16 @@ export function QuestionPhoto({
break;
case "completed": {
setIsFlutterPicking(false);
isInitiatorRef.current = false;
const file = event.data?.files?.[0];
if (file?.url) {
setAnswerValue(question, file.url);
setLocalPreviewUrl(file.url);
const remoteUrl =
file?.path ||
file?.url ||
(file?.data?.path as string | undefined) ||
(file?.data?.url as string | undefined);
if (remoteUrl) {
setAnswerValue(question, remoteUrl);
setLocalPreviewUrl(remoteUrl);
} else if (file?.base64) {
const b64 = file.base64;
setLocalPreviewUrl(b64);
@ -89,6 +99,7 @@ export function QuestionPhoto({
case "cancelled":
case "failed":
setIsFlutterPicking(false);
isInitiatorRef.current = false;
break;
}
});
@ -103,7 +114,11 @@ export function QuestionPhoto({
o.replace(/^\./, "").toLowerCase(),
);
isInitiatorRef.current = true;
setIsFlutterPicking(true);
uploadFile({
requestId: String(question.id),
mediaType: "image",
source: "gallery",
returnAs: "upload",

1
src/lib/webview-actions.ts

@ -115,6 +115,7 @@ export function openExternalUrl(options: OpenExternalUrlOptions): boolean {
// ─── upload_file ─────────────────────────────────────────
export interface UploadFileOptions {
requestId?: string | number;
mediaType: "image" | "video" | "image+video" | "audio" | "file";
source?: "gallery" | "camera" | "any";
multiple?: boolean;

4
src/types/window.d.ts

@ -9,6 +9,7 @@ declare global {
interface FlutterResponseEvent {
action: string;
success: boolean;
requestId?: string | number;
/** Compatibility payload used by the uppercase Flutter event protocol. */
payload?: FlutterResponseEvent["data"];
/** Top-level status for multi-step actions (download_file, upload_file, …) */
@ -67,10 +68,13 @@ declare global {
source?: string;
files?: Array<{
url?: string;
path?: string;
apath?: string;
base64?: string;
name?: string;
size?: number;
mimeType?: string;
data?: Record<string, unknown>;
}>;
// copy_to_clipboard
label?: string;

Loading…
Cancel
Save