mirror of
https://github.com/hoornet/vega.git
synced 2026-05-07 20:59:12 -07:00
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.
This commit is contained in:
@@ -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 && (
|
||||
<div className="flex items-center gap-2 px-6 py-2 border-b border-border bg-bg-raised/50 overflow-x-auto shrink-0">
|
||||
<span className="text-text-dim text-[10px] shrink-0">{inlineImages.length} {inlineImages.length === 1 ? "image" : "images"}</span>
|
||||
{inlineImages.map((img, i) => (
|
||||
<div key={i} className="relative shrink-0 group">
|
||||
<img
|
||||
src={img.url}
|
||||
alt={img.alt}
|
||||
className="h-12 w-auto rounded-sm border border-border object-cover"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content area */}
|
||||
<div className="flex-1 overflow-y-auto px-6 pb-6">
|
||||
{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"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex items-center gap-0.5 border-b border-border px-2 py-1 bg-bg-raised shrink-0">
|
||||
{TOOLS.map(({ action, label, title }) => (
|
||||
{TOOLS.map(({ action, label, title, bold, italic }) => (
|
||||
<button
|
||||
key={action}
|
||||
onClick={() => handleClick(action)}
|
||||
title={title}
|
||||
className="px-2 py-0.5 text-[12px] text-text-muted hover:text-text hover:bg-bg-hover transition-colors rounded-sm"
|
||||
style={action === "bold" ? { fontWeight: "bold" } : action === "italic" ? { fontStyle: "italic" } : undefined}
|
||||
className="px-2 py-0.5 text-[11px] text-text-muted hover:text-text hover:bg-bg-hover transition-colors rounded-sm"
|
||||
style={{ fontWeight: bold ? "bold" : undefined, fontStyle: italic ? "italic" : undefined }}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user