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  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 && (
+
{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 = ``;
+ 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 }: {