Override NDK default outbox relays (purplepag.es DNS failure was stalling
getZapInfo), add 45s zap timeout, disable outbox on NWC instance, fix
follower notification dedup in poller. Zap history rows now show a
clickable preview of the original zapped note.
Kind 3 events are replaceable — same person produces a new event
ID every time they update their contact list. Dedup followers by
pubkey instead of event ID to prevent re-notifying.
Bookmarks load instantly from DB cache, then fetch missing notes
from relays in background. Articles feed shows cached kind-30023
events immediately on the latest tab. Both persist to SQLite for
instant load on next visit.
Followers now load instantly from SQLite on startup, then merge
relay results in background. Own profile name/picture loads from
DB cache so sidebar badge never shows raw npub on slow relays.
Tag-based #p queries are slower on some relays. Increase timeout
from 8s to 12s for fetchMentions. Also retry once after 3s if the
initial notification fetch returns empty (helps on cold start when
relays need time to connect).
Threads now render the focused note immediately instead of showing a
loading skeleton. Root + ancestors fetch in parallel, timeouts cut
from 10s to 5s per round-trip, ancestor lookups from 5s to 2s.
Trending feed adds a base recency score so notes always appear even
when engagement data times out from relays.
WebKitGTK can't serialize FormData with Blob objects through Tauri's
HTTP plugin, throwing "InvalidStateError: Unable to read form data
blob". Build the multipart/form-data body manually as raw bytes
instead. Also add void.cat and nostrimg.com to the HTTP scope.
Notifications now load instantly from SQLite on startup instead of
waiting for relay responses. New events merge in as they arrive.
Read state persists in DB across restarts.
Also: filter profile owner from WoT followers list, make "+N more"
clickable to expand, fix reaction throttle queue jamming on errors.
Query Vertex Verify Reputation API (kind 5312) for each profile and
display "Followed by people you trust" with clickable top-follower
avatars. Includes in-memory cache, request dedup, and graceful
fallback when logged out or API unreachable.
- Emoji reactions now display as grouped pills (❤️5 🤙3 🔥2) instead
of a single aggregated count. Multi-reaction per note supported.
- Throttled reaction fetch queue (max 4 concurrent) prevents relay overload.
- Searching a bare npub/nprofile navigates directly to that profile.
- Notification poller waits for relay connection before first fetch,
fixing empty results on startup.
- Dev-only debug logger (src/lib/debug.ts) — silent in production builds.
Don't overwrite existing notifications with empty results when relay
times out or disconnects. Also fetch notification counts immediately
on startup instead of waiting for the first poll cycle.
Text search now queries relay.nostr.band and search.nos.today via a
persistent NDK instance, while also running hashtag lookups on user
relays. Results are merged and deduplicated. Hashtag searches now show
all three tabs (notes, articles, people) instead of notes-only. Added
loading spinner and clear-on-search for better UX.
Followers tab fetches kind 3 events referencing the user, following tab
shows the contact list. Each row has avatar, NIP-05 badge, follow/unfollow
button, and "follows you" indicator. New follower notifications from the
background poller increment a sidebar badge that clears on view open.
Hidden dev tool showing NDK uptime, live subscription status,
per-relay connection state, per-tab last-updated timestamps,
and recent feed diagnostics log. Polls every 2s while visible.
Closes with Ctrl+Shift+D, Escape, or X button.
- Relay status badge in feed header: shows connected/total relay count with
color coding (green >75%, yellow 25-75%, red <25%), hover tooltip with
per-relay status
- Toast notification system: transient messages for connection lost,
reconnecting, relay reset, and back-online events
- Per-tab "last updated" relative timestamp in feed header (global,
following, trending tracked independently)
- Consolidated all relay management into RelaysView (removed duplicate
relay section from Settings); per-relay remove button on health cards
- Show all supported NIP badges on relay cards (was filtering to 11 notable)
- Tooltips on relay status dots explaining green/yellow/red/gray meaning
- Fix relay removal with trailing-slash URL normalization
- Fix stale health results lingering after relay removal
- Add acknowledgements section to README
Global feed now uses a persistent live subscription (closeOnEose: false)
so new notes stream in real-time instead of requiring manual refresh.
Inspired by Wisp's streaming architecture.
Every fetchEvents call across the entire codebase now uses
fetchWithTimeout with groupable: false — prevents NDK from
batching/reusing stale subscriptions. This fixes Articles, DMs,
Notifications, Zaps, and Trending hanging indefinitely.
Also adds since filters on global (2h) and follow (24h) feeds
to ensure relay freshness, and fixes ArticleFeed re-fetch bug
where follows changes wiped latest tab results.
The liveness probe in ensureConnected was causing a death spiral —
it force-disconnected working relays when a 3s probe timed out,
then resetNDK killed new connections before they could establish.
Now ensureConnected trusts relay.connected and only reconnects when
zero relays are connected. All fetchEvents calls have timeouts
(5-10s) so nothing hangs. resetNDK kept as background-only recovery
in the connection monitor after 30s of continuous failure.
Also adds feed diagnostics (feedDiagnostics.ts) for tracking fetch
timing, event freshness, and relay states via console helpers.
Podcast feature:
- Podcast discovery via Podcast Index API (trending + search)
- Persistent player bar with play/pause, seek, speed (1x/1.5x/2x), volume
- Audio persists across view navigation, resumes from saved position
- Fountain.fm URL detection in feed with rich playable cards
- "Play in Wrystr" button on inline audio blocks
- V4V streaming sats via NWC (LNURL-pay, 5min accumulation, split payments)
- Share what you're listening to (publish note with confirm)
- Space key toggles play/pause globally
Notification fixes:
- Per-notification read tracking (click to mark read) instead of mark-all-on-open
- Read notifications persist at 50% opacity, unread get accent border
- Always fetches last 7 days, keeps 15 most recent
- Filter out own replies from notifications
- Sidebar badge shows only unread count
Syntax highlighting: shared markdown renderer with highlight.js
(atom-one-dark theme), 12 language grammars registered (JS, TS,
Python, Rust, Go, Bash, JSON, YAML, SQL, CSS, HTML, Markdown).
Applied to both article reader and editor preview.
OS notifications: Tauri notification plugin for mentions, DMs, and
zaps. Per-type toggles in Settings with custom toggle switches.
Fires on new unread mentions/DMs; requests OS permission on first
enable. Notification utility at src/lib/notifications.ts.
Zen mode: fullscreen distraction-free writing via F11 or "zen" button;
hides all chrome, centers content in serif font at 17px, shows only
title + content + word count. Esc/F11 to exit, exits on unmount.
Auto-save indicator: shows "saved Xs ago" in editor header, updates
every 10s. After publishing, shows "published to N relays" confirmation
in green, or warning if zero relays confirmed.
Article discovery feed with Latest/Following tabs, article search
(NIP-50 + hashtag for kind 30023), Notes/Articles tab on profiles,
reading time + bookmark + like buttons on article reader. Event
passed directly from card to reader to avoid relay re-fetch failures.
DMs now send via NIP-17 (kind 1059 gift-wrap) with self-copy for sent
messages. Receive supports both NIP-17 and legacy NIP-04 for backward
compat. Protocol indicator shown in conversation list and compose footer.
Note card context menu (⋯) now includes follow/unfollow option so users
can follow authors directly from the feed without visiting their profile.
Fix crash (black screen) when starting a new DM conversation — empty
event array caused undefined access on lastEvent.
Published notes now appear in the feed immediately. Thread replies
show up without waiting for the relay round-trip. Includes all
v0.2.9 fixes (image paste, sent zaps, reply-to clickable, feed
refresh on login).
Four features shipped in this release:
- Feed reply context: replies show "↩ replying to @name" above the
note content; clicking fetches and opens the parent thread
- NIP-65 outbox model: fetchUserRelayList + publishRelayList +
fetchUserNotesNIP65 in client.ts; profile notes fetched via the
author's write relays; "Publish relay list to Nostr" button in
Settings (kind 10002)
- Notifications: new store (notifications.ts) + NotificationsView;
🔔 sidebar nav item with unread badge; DM nav item also shows
unread conversation count; badges clear on open/select
- Keyboard shortcuts: useKeyboardShortcuts hook + HelpModal;
n=compose, /=search, j/k=feed nav with ring highlight,
Esc=back, ?=help overlay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: switchAccount fetched the nsec from the OS keychain on every
switch. Any keychain failure (timeout, Windows Credential Manager quirk,
no entry yet) silently fell through to loginWithPubkey → read-only mode.
Fix: cache each NDKPrivateKeySigner in-memory (_signerCache map) the
moment loginWithNsec succeeds. switchAccount checks the cache first;
the OS keychain is now only consulted at startup (restoreSession). Signers
are pure crypto objects with no session state — safe to reuse indefinitely.
Verified with a 9-switch stress test across 3 accounts (A1→A2→A3→A1→
A2→A1→A3→A2→A1): hasSigner=true and correct pubkey on every switch.
Also adds dev tooling:
- src/lib/tauri-dev-mock.ts: localStorage-backed keychain + SQLite stubs
so the frontend can run in a plain browser for Playwright testing
- src/main.tsx: import mock first in DEV mode (no-op in production)
- test/gen-accounts.mjs: generates 3 deterministic test keypairs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fetchNoteById(eventId): fetches a single event by ID
- NoteContent: note1 and nevent1 (kind 1) references now parsed as
"quote" segment type instead of plain mention text
- QuotePreview component: lazily fetches the referenced note, renders
a bordered card with author avatar + name + truncated content;
click navigates to the thread view
- Quote cards rendered as a block below the note text, consistent with
how images/videos are handled
- naddr1 (kind 30023) and other nevent kinds still open via the
existing article/njump.me handler
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fetchZapCount(eventId): fetches kind 9735 receipts for an event,
parses millisat amounts from embedded zap request description tags,
returns { count, totalSats }
- useZapCount hook: session-cached, same pattern as useReactionCount
- NoteCard: zap button shows "⚡ N sats" when total > 0, falls back
to "⚡ zap" when no zaps yet; stats row shown for logged-out users
displaying ♥ and ⚡ counts when non-zero
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
Nostr layer:
- fetchDMConversations: fetches all kind-4 events to/from the user
(both directions in parallel), deduplicates, newest-first
- fetchDMThread: fetches both directions for a specific conversation,
sorted oldest-first for display
- sendDM: NIP-04 encrypts content via NDK signer, publishes kind 4
- decryptDM: decrypts regardless of direction (ECDH shared secret is
symmetric — always pass the other party to signer.decrypt)
UI (DMView):
- Two-panel layout: conversation list (w-56) + active thread
- ConvRow: avatar, name, time; shows "🔒 encrypted" preview to avoid
decrypting the whole inbox on load
- MessageBubble: decrypts lazily on mount; mine right-aligned,
theirs left-aligned; shows "Could not decrypt" on failure
- ThreadPanel: loads full thread, auto-scrolls to bottom, re-fetches
after send; Ctrl+Enter to send
- NewConvInput: start a new conversation by pasting an npub1 or hex
pubkey; validates and resolves before opening thread
- Read-only (npub) accounts see a clear "nsec required" message
Navigation:
- ✉ messages added to sidebar nav
- openDM(pubkey) in UI store → navigates to dm view with pending pubkey
- ProfileView: "✉ message" button in action row opens DM thread
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- relayInfo.ts: checkNip50Support fetches NIP-11 relay info (Accept:
application/nostr+json) and checks supported_nips for 50; results
cached per session with 4s timeout; getNip50Relays checks all relays
in parallel
- SearchView: relay NIP-50 support checked on mount in the background
and shown as context in the idle state ("N of M relays support
full-text search") and in zero-results states
- Zero results for full-text search now shows:
- relay NIP-50 count (or "none of your relays support full-text")
- prominent "Search #<query>" one-click button to retry as hashtag
- Tabs (notes / people) now always rendered after any search, not only
when both result types are non-empty — makes the UI consistent
- Per-tab zero-results explanations: people tab explains NIP-50
requirement when no relay supports it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Image upload (ImageField):
- "upload" button next to profile picture and banner URL fields
- Opens native file picker (accept="image/*"), uploads to nostr.build
free hosting via their v2 API, auto-fills the URL on success
- Shows inline error if upload fails; URL field still editable manually
NIP-05 verification (Nip05Field):
- Replaces the plain nip05 text field
- Debounced live check (900ms): fetches /.well-known/nostr.json?name=...
and compares the returned pubkey against the logged-in user's pubkey
- Status badges: checking… / ✓ verified / ✗ pubkey mismatch / ✗ not found
- "How to get verified ↗" link to nostr.how guide
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fetchZapsReceived: kind 9735 filtered by #p tag (receipts)
- fetchZapsSent: kind 9734 filtered by authors (zap requests)
- ZapHistoryView: Received / Sent tabs with row count; header shows
total sats in/out; each row: avatar, amount, counterpart name
(clickable → profile), comment, time ago
- Receipt parsing: amount from embedded zap request in "description"
tag (millisats → sats); sender from uppercase "P" tag with fallback
to zap request pubkey
- Request parsing: amount from "amount" tag, recipient from "p" tag
- ⚡ zaps nav item added to sidebar between search and relays
- Logged-out fallback state with prompt to log in
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- publishRepost: kind 6 event with stringified original event as content
and ["e", id, "", "mention"] + ["p", pubkey] tags
- publishQuote: kind 1 note with user's text + appended nostr:nevent1...
reference and ["q", id] + ["p", pubkey] tags
- QuoteModal: compose modal with live quoted-note preview (avatar, name,
truncated content); Ctrl+Enter to post, Escape to close
- NoteCard: "repost" (one-click, shows "reposted ✓") and "quote" (opens
QuoteModal) added to the actions row alongside reply/like/zap
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>