pull/5075/merge
Ector 2 days ago committed by GitHub
commit 14ada7855b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,13 +1,10 @@
import { t } from "i18next";
import { LoaderIcon, PaperclipIcon } from "lucide-react";
import mime from "mime";
import { observer } from "mobx-react-lite";
import { useContext, useRef, useState } from "react";
import toast from "react-hot-toast";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { attachmentStore } from "@/store";
import { Attachment } from "@/types/proto/api/v1/attachment_service";
import { MemoEditorContext } from "../types";
interface Props {
@ -39,32 +36,15 @@ const UploadAttachmentButton = observer((props: Props) => {
uploadingFlag: true,
};
});
const createdAttachmentList: Attachment[] = [];
try {
if (!fileInputRef.current || !fileInputRef.current.files) {
return;
}
for (const file of fileInputRef.current.files) {
const { name: filename, size, type } = file;
const buffer = new Uint8Array(await file.arrayBuffer());
const attachment = await attachmentStore.createAttachment({
attachment: Attachment.fromPartial({
filename,
size,
type: type || mime.getType(filename) || "text/plain",
content: buffer,
}),
attachmentId: "",
});
createdAttachmentList.push(attachment);
// Delegate to editor's upload handler so progress UI is consistent
if (context.uploadFiles) {
await context.uploadFiles(fileInputRef.current.files);
}
} catch (error: any) {
console.error(error);
toast.error(error.details);
}
context.setAttachmentList([...context.attachmentList, ...createdAttachmentList]);
setState((state) => {
return {
...state,

@ -52,6 +52,8 @@ interface State {
relationList: MemoRelation[];
location: Location | undefined;
isUploadingAttachment: boolean;
// Track in-flight uploads for UI progress bars
uploadTasks: { id: string; name: string; progress: number }[];
isRequesting: boolean;
isComposing: boolean;
isDraggingFile: boolean;
@ -68,6 +70,7 @@ const MemoEditor = observer((props: Props) => {
relationList: [],
location: undefined,
isUploadingAttachment: false,
uploadTasks: [],
isRequesting: false,
isComposing: false,
isDraggingFile: false,
@ -199,16 +202,52 @@ const MemoEditor = observer((props: Props) => {
};
const handleUploadResource = async (file: File) => {
setState((state) => {
return {
...state,
isUploadingAttachment: true,
};
const taskId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
// Add task entry
setState((prev) => ({ ...prev, uploadTasks: [...prev.uploadTasks, { id: taskId, name: file.name, progress: 0 }] }));
const setTaskProgress = (progress: number) => {
setState((prev) => ({
...prev,
uploadTasks: prev.uploadTasks.map((t) => (t.id === taskId ? { ...t, progress } : t)),
}));
};
const removeTask = () => {
setState((prev) => ({
...prev,
uploadTasks: prev.uploadTasks.filter((t) => t.id !== taskId),
}));
};
// Read file with progress (up to 30%)
const buffer = await new Promise<Uint8Array>((resolve, reject) => {
try {
const reader = new FileReader();
reader.onprogress = (e) => {
if (e.lengthComputable) {
const frac = e.loaded / e.total;
setTaskProgress(Math.min(0.3, frac * 0.3));
}
};
reader.onerror = () => reject(reader.error ?? new Error("Failed to read file"));
reader.onload = () => {
setTaskProgress(0.3);
resolve(new Uint8Array(reader.result as ArrayBuffer));
};
reader.readAsArrayBuffer(file);
} catch (err) {
reject(err);
}
});
const { name: filename, size, type } = file;
const buffer = new Uint8Array(await file.arrayBuffer());
// Simulate upload progress while the request is in-flight (30% -> 95%)
let current = 0.3;
const interval = window.setInterval(() => {
current = Math.min(0.95, current + 0.02);
setTaskProgress(current);
}, 200);
const { name: filename, size, type } = file;
try {
const attachment = await attachmentStore.createAttachment({
attachment: Attachment.fromPartial({
@ -219,26 +258,22 @@ const MemoEditor = observer((props: Props) => {
}),
attachmentId: "",
});
setState((state) => {
return {
...state,
isUploadingAttachment: false,
};
});
window.clearInterval(interval);
setTaskProgress(1);
// Remove task shortly after completion
window.setTimeout(removeTask, 500);
return attachment;
} catch (error: any) {
window.clearInterval(interval);
console.error(error);
toast.error(error.details);
setState((state) => {
return {
...state,
isUploadingAttachment: false,
};
});
toast.error(error.details ?? "Upload failed");
// Remove task on error as well
removeTask();
}
};
const uploadMultiFiles = async (files: FileList) => {
setState((prev) => ({ ...prev, isUploadingAttachment: true }));
const uploadedAttachmentList: Attachment[] = [];
for (const file of files) {
const attachment = await handleUploadResource(file);
@ -261,6 +296,8 @@ const MemoEditor = observer((props: Props) => {
attachmentList: [...prevState.attachmentList, ...uploadedAttachmentList],
}));
}
// If no more tasks in-flight, clear uploading flag
setState((prev) => ({ ...prev, isUploadingAttachment: false }));
};
const handleDropEvent = async (event: React.DragEvent) => {
@ -478,6 +515,9 @@ const MemoEditor = observer((props: Props) => {
relationList,
}));
},
uploadFiles: async (files: FileList) => {
await uploadMultiFiles(files);
},
memoName,
}}
>
@ -497,6 +537,24 @@ const MemoEditor = observer((props: Props) => {
onCompositionEnd={handleCompositionEnd}
>
<Editor ref={editorRef} {...editorConfig} />
{state.uploadTasks.length > 0 && (
<div className="w-full mt-2 space-y-2">
{state.uploadTasks.map((task) => (
<div key={task.id} className="w-full">
<div className="flex items-center justify-between text-xs opacity-70 mb-1">
<span className="truncate max-w-[70%]">{task.name}</span>
<span className="tabular-nums">{Math.round(task.progress * 100)}%</span>
</div>
<div className="w-full h-1.5 rounded bg-muted">
<div
className="h-1.5 rounded bg-primary transition-[width] duration-200"
style={{ width: `${Math.max(2, Math.round(task.progress * 100))}%` }}
/>
</div>
</div>
))}
</div>
)}
<AttachmentListView attachmentList={state.attachmentList} setAttachmentList={handleSetAttachmentList} />
<RelationListView relationList={referenceRelations} setRelationList={handleSetRelationList} />
<div className="relative w-full flex flex-row justify-between items-center py-1 gap-2" onFocus={(e) => e.stopPropagation()}>

@ -8,6 +8,8 @@ interface Context {
setAttachmentList: (attachmentList: Attachment[]) => void;
setRelationList: (relationList: MemoRelation[]) => void;
memoName?: string;
// Optional: central upload handler so UI can show progress consistently
uploadFiles?: (files: FileList) => Promise<void>;
}
export const MemoEditorContext = createContext<Context>({

Loading…
Cancel
Save