mirror of
https://github.com/hoornet/vega.git
synced 2026-05-07 20:59:12 -07:00
Image upload in replies, multi-image articles, UX fixes
Add image support to InlineReplyBox (paste, file picker), paste-to-upload in article editor, multi-select for article image toolbar. Fix emoji picker opening off-screen (right-align), enlarge emoji/attach buttons, make note card names clickable to open profile.
This commit is contained in:
@@ -6,7 +6,7 @@ import { MarkdownToolbar, handleEditorKeyDown } from "./MarkdownToolbar";
|
||||
import { useDraftStore, type ArticleDraft } from "../../stores/drafts";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
import { uploadBytes } from "../../lib/upload";
|
||||
import { uploadBytes, uploadImage } from "../../lib/upload";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
export function ArticleEditor() {
|
||||
@@ -162,6 +162,56 @@ export function ArticleEditor() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleArticlePaste = async (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
const fileFromFiles = Array.from(e.clipboardData.files).find((f) => f.type.startsWith("image/"));
|
||||
if (fileFromFiles) {
|
||||
e.preventDefault();
|
||||
setUploading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const url = await uploadImage(fileFromFiles);
|
||||
const ta = textareaRef.current;
|
||||
if (ta) {
|
||||
const start = ta.selectionStart ?? content.length;
|
||||
const end = ta.selectionEnd ?? content.length;
|
||||
const md = ``;
|
||||
setContent(content.slice(0, start) + md + content.slice(end));
|
||||
setTimeout(() => { ta.selectionStart = ta.selectionEnd = start + md.length; ta.focus(); }, 0);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Image upload failed: ${err}`);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const items = Array.from(e.clipboardData.items ?? []);
|
||||
const imageItem = items.find((item) => item.type.startsWith("image/"));
|
||||
if (imageItem) {
|
||||
const file = imageItem.getAsFile();
|
||||
if (file) {
|
||||
e.preventDefault();
|
||||
setUploading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const url = await uploadImage(file);
|
||||
const ta = textareaRef.current;
|
||||
if (ta) {
|
||||
const start = ta.selectionStart ?? content.length;
|
||||
const end = ta.selectionEnd ?? content.length;
|
||||
const md = ``;
|
||||
setContent(content.slice(0, start) + md + content.slice(end));
|
||||
setTimeout(() => { ta.selectionStart = ta.selectionEnd = start + md.length; ta.focus(); }, 0);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Image upload failed: ${err}`);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// If no active draft, show the draft list
|
||||
if (!activeDraftId) {
|
||||
return <DraftListView onNewDraft={handleNewDraft} />;
|
||||
@@ -356,6 +406,7 @@ export function ArticleEditor() {
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onKeyDown={(e) => handleEditorKeyDown(e, textareaRef, content, setContent)}
|
||||
onPaste={handleArticlePaste}
|
||||
placeholder="Write your article in Markdown…"
|
||||
className="w-full h-full min-h-[400px] bg-transparent text-text text-[14px] leading-relaxed placeholder:text-text-dim resize-none focus:outline-none font-mono"
|
||||
/>
|
||||
|
||||
@@ -141,27 +141,29 @@ export function MarkdownToolbar({ textareaRef, content, setContent, setUploading
|
||||
const handleImageUpload = async () => {
|
||||
try {
|
||||
const selected = await open({
|
||||
multiple: false,
|
||||
multiple: true,
|
||||
filters: [
|
||||
{ name: "Images", extensions: ["jpg", "jpeg", "png", "gif", "webp", "svg"] },
|
||||
],
|
||||
});
|
||||
if (!selected) return;
|
||||
const filePath = typeof selected === "string" ? selected : selected;
|
||||
const paths = Array.isArray(selected) ? selected : [selected];
|
||||
setUploading?.(true);
|
||||
setError?.(null);
|
||||
try {
|
||||
const bytes = await readFile(filePath);
|
||||
const fileName = filePath.split(/[\\/]/).pop() || "image.png";
|
||||
const ext = fileName.split(".").pop()?.toLowerCase() || "png";
|
||||
const mimeMap: Record<string, string> = {
|
||||
jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif",
|
||||
webp: "image/webp", svg: "image/svg+xml",
|
||||
};
|
||||
const url = await uploadBytes(new Uint8Array(bytes), fileName, mimeMap[ext] || "image/png");
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
applyMarkdown(textarea, "image", content, setContent, ``);
|
||||
for (const filePath of paths) {
|
||||
const bytes = await readFile(filePath);
|
||||
const fileName = filePath.split(/[\\/]/).pop() || "image.png";
|
||||
const ext = fileName.split(".").pop()?.toLowerCase() || "png";
|
||||
const mimeMap: Record<string, string> = {
|
||||
jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif",
|
||||
webp: "image/webp", svg: "image/svg+xml",
|
||||
};
|
||||
const url = await uploadBytes(new Uint8Array(bytes), fileName, mimeMap[ext] || "image/png");
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
applyMarkdown(textarea, "image", content, setContent, ``);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setUploading?.(false);
|
||||
|
||||
Reference in New Issue
Block a user