Bump to v0.7.0 — writer tools, NIP-98 uploads, multi-draft, article bookmarks

- NIP-98 HTTP Auth for image uploads with fallback services (void.cat, nostrimg.com)
- Markdown toolbar (bold, italic, heading, link, image, quote, code, list) + Ctrl+B/I/K
- Multi-draft management with draft list, resume, delete, auto-migrate
- Cover image file picker in article meta panel
- Article bookmarks via NIP-51 'a' tags; Notes/Articles tabs in BookmarkView
- Removed Rust upload_file command; dropped reqwest/mime_guess deps
- Upload spinner, draft count badge, empty states
This commit is contained in:
Jure
2026-03-18 18:36:08 +01:00
parent c66885440a
commit 092553ab9b
19 changed files with 846 additions and 152 deletions

View File

@@ -2,15 +2,21 @@ import { useEffect, useState } from "react";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { useBookmarkStore } from "../../stores/bookmark";
import { useUserStore } from "../../stores/user";
import { fetchNoteById } from "../../lib/nostr";
import { fetchNoteById, fetchByAddr } from "../../lib/nostr";
import { NoteCard } from "../feed/NoteCard";
import { ArticleCard } from "../article/ArticleCard";
import { SkeletonNoteList } from "../shared/Skeleton";
type BookmarkTab = "notes" | "articles";
export function BookmarkView() {
const { bookmarkedIds, fetchBookmarks } = useBookmarkStore();
const { bookmarkedIds, bookmarkedArticleAddrs, fetchBookmarks } = useBookmarkStore();
const { pubkey } = useUserStore();
const [tab, setTab] = useState<BookmarkTab>("notes");
const [notes, setNotes] = useState<NDKEvent[]>([]);
const [loading, setLoading] = useState(false);
const [articles, setArticles] = useState<NDKEvent[]>([]);
const [loadingNotes, setLoadingNotes] = useState(false);
const [loadingArticles, setLoadingArticles] = useState(false);
useEffect(() => {
if (pubkey) fetchBookmarks(pubkey);
@@ -24,8 +30,16 @@ export function BookmarkView() {
loadNotes();
}, [bookmarkedIds]);
useEffect(() => {
if (bookmarkedArticleAddrs.length === 0) {
setArticles([]);
return;
}
loadArticles();
}, [bookmarkedArticleAddrs]);
const loadNotes = async () => {
setLoading(true);
setLoadingNotes(true);
try {
const results = await Promise.all(
bookmarkedIds.map((id) => fetchNoteById(id))
@@ -36,36 +50,81 @@ export function BookmarkView() {
.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))
);
} finally {
setLoading(false);
setLoadingNotes(false);
}
};
const loadArticles = async () => {
setLoadingArticles(true);
try {
const results = await Promise.all(
bookmarkedArticleAddrs.map((addr) => fetchByAddr(addr))
);
setArticles(
results
.filter((e): e is NDKEvent => e !== null)
.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))
);
} finally {
setLoadingArticles(false);
}
};
const totalCount = bookmarkedIds.length + bookmarkedArticleAddrs.length;
const loading = tab === "notes" ? loadingNotes : loadingArticles;
const items = tab === "notes" ? notes : articles;
return (
<div className="h-full flex flex-col">
<header className="border-b border-border px-4 py-2.5 shrink-0">
<div className="flex items-center justify-between">
<h2 className="text-text text-[13px] font-medium">Bookmarks</h2>
<span className="text-text-dim text-[11px]">{bookmarkedIds.length} saved</span>
<div className="flex items-center gap-3">
<h2 className="text-text text-[13px] font-medium">Bookmarks</h2>
<div className="flex border border-border text-[11px]">
<button
onClick={() => setTab("notes")}
className={`px-3 py-0.5 transition-colors ${tab === "notes" ? "bg-accent/10 text-accent" : "text-text-muted hover:text-text"}`}
>
Notes
</button>
<button
onClick={() => setTab("articles")}
className={`px-3 py-0.5 transition-colors ${tab === "articles" ? "bg-accent/10 text-accent" : "text-text-muted hover:text-text"}`}
>
Articles
</button>
</div>
</div>
<span className="text-text-dim text-[11px]">{totalCount} saved</span>
</div>
</header>
<div className="flex-1 overflow-y-auto">
{loading && notes.length === 0 && (
{loading && items.length === 0 && (
<SkeletonNoteList count={3} />
)}
{!loading && notes.length === 0 && (
{!loading && items.length === 0 && (
<div className="px-4 py-12 text-center space-y-2">
<p className="text-text-dim text-[13px]">No bookmarks yet.</p>
<p className="text-text-dim text-[13px]">
{tab === "notes" ? "No bookmarked notes." : "No bookmarked articles."}
</p>
<p className="text-text-dim text-[11px] opacity-60">
Use the <span className="text-accent">save</span> button on any note to bookmark it here.
{tab === "notes"
? <>Use the <span className="text-accent">save</span> button on any note to bookmark it here.</>
: <>Use the <span className="text-accent">save</span> button on any article to add it to your reading list.</>
}
</p>
</div>
)}
{notes.map((event) => (
{tab === "notes" && notes.map((event) => (
<NoteCard key={event.id} event={event} />
))}
{tab === "articles" && articles.map((event) => (
<ArticleCard key={event.id} event={event} />
))}
</div>
</div>
);