From ef21667d9f8983d14ab1e3a49c074c8506728488 Mon Sep 17 00:00:00 2001
From: Jure <44338+hoornet@users.noreply.github.com>
Date: Wed, 25 Mar 2026 12:01:47 +0100
Subject: [PATCH] Fix thread disappearing bug, improve article editor UX
Move selectedNote guard after hooks in ThreadView to fix React rules
of hooks violation that caused thread content to blank out. Article
editor gets inline image previews, readable toolbar labels, and
drag-and-drop image support. Thread reply textareas now auto-expand.
---
src/components/article/ArticleEditor.tsx | 58 +++++++++++++++++++++-
src/components/article/MarkdownToolbar.tsx | 22 ++++----
src/components/thread/ThreadNode.tsx | 4 +-
src/components/thread/ThreadView.tsx | 7 +--
4 files changed, 74 insertions(+), 17 deletions(-)
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 }: {