diff --git a/src/components/article/ArticleEditor.tsx b/src/components/article/ArticleEditor.tsx index 227ae15..e0680cd 100644 --- a/src/components/article/ArticleEditor.tsx +++ b/src/components/article/ArticleEditor.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useCallback } from "react"; +import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { renderMarkdown } from "../../lib/markdown"; import { publishArticle } from "../../lib/nostr"; import { useUIStore } from "../../stores/ui"; @@ -9,6 +9,17 @@ import { readFile } from "@tauri-apps/plugin-fs"; import { uploadBytes, uploadImage } from "../../lib/upload"; import { getCurrentWindow } from "@tauri-apps/api/window"; +/** Extract image URLs from markdown ![alt](url) patterns */ +function extractImages(md: string): { alt: string; url: string }[] { + const re = /!\[([^\]]*)\]\(([^)]+)\)/g; + const images: { alt: string; url: string }[] = []; + let m; + while ((m = re.exec(md))) { + images.push({ alt: m[1], url: m[2] }); + } + return images; +} + export function ArticleEditor() { const { goBack } = useUIStore(); const { activeDraftId, drafts, updateDraft, deleteDraft, setActiveDraft, createDraft } = useDraftStore(); @@ -103,6 +114,7 @@ export function ArticleEditor() { const renderedHtml = renderMarkdown(content || "*Nothing to preview yet.*"); const wordCount = content.trim() ? content.trim().split(/\s+/).length : 0; const canPublish = title.trim().length > 0 && content.trim().length > 0; + const inlineImages = useMemo(() => extractImages(content), [content]); const handlePublish = async () => { if (!canPublish || publishing) return; @@ -398,6 +410,23 @@ export function ArticleEditor() { /> )} + {/* Inline image previews (write mode only) */} + {mode === "write" && inlineImages.length > 0 && ( +
+ {inlineImages.length} {inlineImages.length === 1 ? "image" : "images"} + {inlineImages.map((img, i) => ( +
+ {img.alt} { (e.target as HTMLImageElement).style.display = "none"; }} + /> +
+ ))} +
+ )} + {/* Content area */}
{mode === "write" ? ( @@ -407,7 +436,32 @@ export function ArticleEditor() { onChange={(e) => setContent(e.target.value)} onKeyDown={(e) => handleEditorKeyDown(e, textareaRef, content, setContent)} onPaste={handleArticlePaste} - placeholder="Write your article in Markdown…" + onDrop={async (e) => { + const file = Array.from(e.dataTransfer.files).find((f) => f.type.startsWith("image/")); + if (!file) return; + e.preventDefault(); + e.stopPropagation(); + 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 = `![image](${url})`; + setContent(content.slice(0, start) + md + content.slice(end)); + } + } catch (err) { + setError(`Image upload failed: ${err}`); + } finally { + setUploading(false); + } + }} + onDragOver={(e) => { + if (Array.from(e.dataTransfer.types).includes("Files")) e.preventDefault(); + }} + placeholder="Write your article in Markdown… (paste or drop images)" 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" /> ) : ( diff --git a/src/components/article/MarkdownToolbar.tsx b/src/components/article/MarkdownToolbar.tsx index 25057ec..af0722f 100644 --- a/src/components/article/MarkdownToolbar.tsx +++ b/src/components/article/MarkdownToolbar.tsx @@ -116,15 +116,15 @@ function applyMarkdown( }); } -const TOOLS: { action: MarkdownAction; label: string; title: string }[] = [ - { action: "bold", label: "B", title: "Bold (Ctrl+B)" }, - { action: "italic", label: "I", title: "Italic (Ctrl+I)" }, +const TOOLS: { action: MarkdownAction; label: string; title: string; bold?: boolean; italic?: boolean }[] = [ + { action: "bold", label: "B", title: "Bold (Ctrl+B)", bold: true }, + { action: "italic", label: "I", title: "Italic (Ctrl+I)", italic: true }, { action: "heading", label: "H", title: "Heading" }, - { action: "link", label: "🔗", title: "Link (Ctrl+K)" }, - { action: "image", label: "🖼", title: "Image" }, - { action: "quote", label: "❝", title: "Quote" }, - { action: "code", label: "", title: "Code" }, - { action: "list", label: "☰", title: "List" }, + { action: "link", label: "Link", title: "Insert link (Ctrl+K)" }, + { action: "image", label: "Image", title: "Upload image" }, + { action: "quote", label: "Quote", title: "Block quote" }, + { action: "code", label: "Code", title: "Code block" }, + { action: "list", label: "List", title: "Bullet list" }, ]; export function MarkdownToolbar({ textareaRef, content, setContent, setUploading, setError }: ToolbarProps) { @@ -175,13 +175,13 @@ export function MarkdownToolbar({ textareaRef, content, setContent, setUploading return (
- {TOOLS.map(({ action, label, title }) => ( + {TOOLS.map(({ action, label, title, bold, italic }) => ( diff --git a/src/components/thread/ThreadNode.tsx b/src/components/thread/ThreadNode.tsx index 1a3f814..778cc2c 100644 --- a/src/components/thread/ThreadNode.tsx +++ b/src/components/thread/ThreadNode.tsx @@ -6,6 +6,7 @@ import { publishReply } from "../../lib/nostr"; import { useProfile } from "../../hooks/useProfile"; import { shortenPubkey } from "../../lib/utils"; import { EmojiPicker } from "../shared/EmojiPicker"; +import { useAutoResize } from "../../hooks/useAutoResize"; interface ThreadNodeProps { node: ThreadNodeType; @@ -31,6 +32,7 @@ function InlineThreadReply({ replyTo, rootEvent, onPublished }: { const [sent, setSent] = useState(false); const [showEmoji, setShowEmoji] = useState(false); const ref = useRef(null); + const autoResize = useAutoResize(2, 8); const handleSubmit = async () => { if (!text.trim() || replying) return; @@ -60,7 +62,7 @@ function InlineThreadReply({ replyTo, rootEvent, onPublished }: {