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

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

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

1
src/lib/webview-actions.ts

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

4
src/types/window.d.ts

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

Loading…
Cancel
Save