Add long-form article reader (Phase 1 #1, NIP-23)

- fetchArticle(naddr): fetches kind 30023 by d-tag + author pubkey
- fetchAuthorArticles(pubkey): for future profile articles tab
- ArticleView: cover image, title, italic summary, author row (avatar +
  name + date), tag pills, full markdown body (DOMPurify sanitized),
  zap author button in header and footer, copy nostr: link, njump.me
  fallback on fetch error
- prose-article CSS: reader-optimised typography (15px base, 1.8 line
  height, h2 border, styled blockquote/code/pre/links/images)
- NoteContent: naddr1 clicks for kind 30023 now open ArticleView
  instead of falling through to njump.me
- openArticle(naddr) added to UI store with previousView tracking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jure
2026-03-10 20:42:13 +01:00
parent f0cb9a97bb
commit ab7af96c45
9 changed files with 318 additions and 6 deletions

View File

@@ -2,7 +2,7 @@ import { create } from "zustand";
import { NDKEvent } from "@nostr-dev-kit/ndk";
type View = "feed" | "search" | "relays" | "settings" | "profile" | "thread" | "article-editor" | "about" | "zaps" | "dm";
type View = "feed" | "search" | "relays" | "settings" | "profile" | "thread" | "article-editor" | "article" | "about" | "zaps" | "dm";
interface UIState {
currentView: View;
@@ -12,11 +12,13 @@ interface UIState {
previousView: View;
pendingSearch: string | null;
pendingDMPubkey: string | null;
pendingArticleNaddr: string | null;
setView: (view: View) => void;
openProfile: (pubkey: string) => void;
openThread: (note: NDKEvent, from: View) => void;
openSearch: (query: string) => void;
openDM: (pubkey: string) => void;
openArticle: (naddr: string) => void;
goBack: () => void;
toggleSidebar: () => void;
}
@@ -31,11 +33,13 @@ export const useUIStore = create<UIState>((set, _get) => ({
previousView: "feed",
pendingSearch: null,
pendingDMPubkey: null,
pendingArticleNaddr: null,
setView: (currentView) => set({ currentView }),
openProfile: (pubkey) => set((s) => ({ currentView: "profile", selectedPubkey: pubkey, previousView: s.currentView as View })),
openThread: (note, from) => set({ currentView: "thread", selectedNote: note, previousView: from }),
openSearch: (query) => set({ currentView: "search", pendingSearch: query }),
openDM: (pubkey) => set({ currentView: "dm", pendingDMPubkey: pubkey }),
openArticle: (naddr) => set((s) => ({ currentView: "article", pendingArticleNaddr: naddr, previousView: s.currentView as View })),
goBack: () => set((s) => ({
currentView: s.previousView !== s.currentView ? s.previousView : "feed",
selectedNote: null,