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.
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.
- 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.
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.
- Subscribe/unsubscribe button on podcast cards and episode list header
- "My Podcasts" tab shows subscribed podcasts, opens first if subscriptions exist
- Subscriptions persisted in localStorage
- Tab shows subscription count
- Empty state guides to search/trending
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
- Fix mute button having no effect in Media Feed (missing filter)
- Fix notification toggle switches overlapping labels (sizing + shrink-0)
- Fix external links not opening in system browser (use Tauri opener plugin)
- Fix trending refresh showing no visual feedback (clear list on force refresh)
- Fix emoji reactions inaccessible behind WebKitGTK context menu (visible + button)
- Add emoji picker to compose box, inline reply, and thread reply
- New shared EmojiPicker component with categorized emoji groups
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.
- ComposeBox: remove hard 280-char post limit (Nostr has none); show counter
only after 3000 chars with yellow/red warnings at 3500/4000
- Article reader: switch from monospace to serif font (Georgia stack) at 17px
for comfortable long-form reading; article preview gets serif at 15px
- ArticleView: add 2px accent-colored reading progress bar (sticky top,
scroll-driven, smooth transition)
- Connection indicator: data-aware checking (wraps fetchEvents), 30s recent-
fetch grace period, 25s offline grace (5 checks) before marking disconnected
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.
Add GStreamer codec dependencies to PKGBUILD and install instructions
to README for Linux video/audio playback. Add periodic relay pool
status check so connection indicator stays accurate after reconnects.
Feed tab (Global/Following) moved from local state to UI store so it
survives thread/profile navigation. Fixed hardcoded "feed" in openThread
calls to pass currentView instead.
- restoreSession pre-loads all nsec accounts from keychain into signer
cache at startup, not just the active one
- switchAccount updates state to target account on failure instead of
leaving UI stuck on previous account
- AccountSwitcher shows re-login prompt when account has no signer
- store_nsec now logs warnings instead of silently swallowing errors
- Fix: repost + quote buttons added to RootNote in ThreadView (Issue A)
- Fix: switchAccount no longer silently goes read-only when nsec keychain
entry is missing — nsec accounts stay logged out so the login button
handles recovery; loginType field added to SavedAccount (Issue B)
- Fix: macos-12 runner replaced with macos-13 in release.yml — macos-12
was deprecated by GitHub, causing macOS jobs to never run, which
prevented latest.json from being assembled and broke the auto-updater
for every user since v0.1.5
- Bump Cargo.toml version (was stuck at 0.1.10)
- Release notes updated with v0.2.1 + v0.2.0 Phase 2 changelog
- README: Windows unsigned installer note added
- ROADMAP: NIP-05 monetization added to brainstorm backlog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- Fix switchAccount: check signer was set after loginWithNsec before
returning; fall back to loginWithPubkey on silent failure; always
navigate to feed after switch to clear stale UI
- Fix profile edit button: hidden in read-only (npub) mode; read-only
badge shown in profile header to make state visible
- Sidebar: show version number (v0.1.8) below WRYSTR brand, auto-tracked
from package.json — no more hardcoding
- Support page: increase QR code gap (gap-8 → gap-16) to prevent
accidentally scanning the wrong address
- ROADMAP: add language/alphabet feed filter to Phase 3 backlog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Critical:
- NWC wallet now stored per-account (wrystr_nwc_<pubkey>); switching
accounts loads the correct wallet automatically
- Clear NDK signer before account switch to prevent race where old
account could sign outgoing events
- LoginModal: add "New account" tab to create a fresh keypair inline
(same flow as onboarding, with nsec copy + confirmation checkbox)
- ThreadView: add like + zap action row to the root note (was missing)
UX:
- Zap button now conditional on lud16/lud06 (NoteCard, ProfileView,
RootNote) — no zap button shown for profiles without Lightning
- Remove "200 notes" counter from sidebar footer
- AccountSwitcher: larger active account avatar (w-8), name more
prominent; sign-out/remove moved into dropdown only
Quick wins:
- AboutView: add GitHub Sponsors link
- ComposeBox: paste image from clipboard → uploads via nostr.build,
inserts URL at cursor with "uploading image…" status
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>
- sidebarCollapsed now persisted to localStorage — remembered across sessions
- Explicit ‹/› toggle button always visible in the header; no longer
requires knowing to click the WRYSTR wordmark
- Expanded: WRYSTR on left, ‹ collapse button on right
- Collapsed: centered › expand button fills the header
- Write article (✦) now visible in collapsed mode as icon-only with
tooltip, consistent with the rest of the nav
- Nav items have title tooltip in collapsed mode for discoverability
- Status footer: collapsed shows just the connection dot with tooltip;
expanded shows dot + online/offline text + note count as before
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>
- fetchMuteList / publishMuteList in nostr client (kind 10000)
- mute store: mutedPubkeys persisted to localStorage + synced to relay;
mute/unmute publish kind 10000 best-effort; fetchMuteList merges relay
list with local mutes on login
- fetchMuteList called after every login (nsec + pubkey)
- Feed: muted pubkeys filtered from both Global and Following tabs
- NoteCard: ⋯ context menu (appears on hover, hidden for own notes)
with mute / unmute action; backdrop click closes menu
- ProfileView: mute / unmute button in the action row (next to follow)
- SettingsView: MuteSection lists muted accounts with name + avatar;
hover to reveal unmute button; hidden when mute list is empty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New 'about' view wired into sidebar nav (♥ support) and App.tsx
- Sections: in-app zap button (reuses ZapModal + NWC infra), Lightning
address QR + copy, Bitcoin address QR + copy, Ko-fi link, GitHub link,
version/tech credits footer
- QR codes rendered as SVG via react-qr-code (no canvas dependency)
- lightning: URI and bitcoin: URI formats for wallet-scannable QR codes
- Tasteful and non-nagging — lives in the nav, never pops up unprompted
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rust: rusqlite (bundled) with WAL mode; wrystr.db in app data dir
- db_save_notes: upsert batch of raw event JSON, prune to 500 kind-1 notes
- db_load_feed: return N most-recent kind-1 raws for instant startup display
- db_save_profile / db_load_profile: cache NDKUserProfile JSON by pubkey
- Falls back to in-memory SQLite if the on-disk open fails
- src/lib/db.ts: typed invoke wrappers; all errors silenced (cache is best-effort)
- feed store: loadCachedFeed() populates notes before relay connects;
loadFeed() merges fresh+cached (so relay returning fewer notes doesn't
erase cached ones), then saves fresh notes to SQLite
- useProfile: reads SQLite cache to show avatar/name instantly while
relay request is in-flight; saves result to SQLite after relay responds
- Feed: calls loadCachedFeed() first → notes visible before relay connects
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SavedAccount list persisted in localStorage (wrystr_accounts)
- loginWithNsec / loginWithPubkey now upsert into the accounts list
- fetchOwnProfile caches name + picture into the account entry
- switchAccount: loads nsec from OS keychain, falls back to read-only
- removeAccount: deletes keychain entry + removes from list; logs out
if it was the active account
- logout: clears active session only — keychain entries kept for instant
switch-back
- AccountSwitcher component in sidebar footer: shows current account,
expand (▼/▲) to list all saved accounts, click to switch instantly,
× to remove, "+ add account" opens LoginModal, sign-out / remove
account actions inline
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rust: store_nsec / load_nsec / delete_nsec Tauri commands via keyring crate
(macOS Keychain, Windows Credential Manager, Linux Secret Service)
- On nsec login: key is stored in OS keychain keyed by hex pubkey
- On startup: restoreSession() auto-loads nsec from keychain and re-establishes
the NDK signer — no manual re-login required after restart
- On logout: keychain entry is deleted
- Graceful degradation: if keychain is unavailable (e.g. Linux without a Secret
Service daemon), the app starts logged-out — same UX as before, no crash
Also updates ROADMAP.md with 4 new items from the Windows playtest (multi-account
switcher, NWC wizard, system tray, zap history view) and reorders the list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Mention clicks (nostr:npub1, nostr:nprofile) open internal ProfileView
- njump.me links intercepted: npub/nprofile decoded and opened internally,
note/nevent/naddr fall through to browser (no reader yet)
- Hashtag clicks navigate to SearchView and auto-run the search
- openSearch(query) action added to UIStore; pendingSearch consumed on mount
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ArticleEditor: annotate map callback parameter as string
- ui.ts: prefix unused get parameter with _ to silence noUnusedLocals
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- searchNotes: NIP-50 full-text for text queries, #t tag filter for #hashtags
- searchUsers: NIP-50 kind 0 search for people
- SearchView with tabbed notes/people results, follow/unfollow inline
- Hashtag queries skip people search and use universal #t filter
- Graceful empty state explains NIP-50 relay support caveat
- Search added to sidebar nav (⌕)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- publishContactList (kind 3) in nostr lib — replaces full follow list on each change
- follow() and unfollow() actions in user store with optimistic UI update
- Follow/Unfollow button in ProfileView header (visible when logged in, not own profile)
- Button shows "unfollow" in muted style with danger hover when already following
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>