- 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.
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.
- 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.
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.
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.
Keep Feed component mounted (hidden via CSS) instead of unmounting
when navigating to threads/profiles/etc. Back button now returns to
the exact scroll position in the feed.
Add inline video/audio players for direct media URLs, rich link cards
for YouTube (with thumbnails), Vimeo, Spotify, and Tidal. Fix video
clicks navigating to thread by splitting NoteContent into inline/media
modes. Fix published notes not appearing on Following tab.
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.
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).
- 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>
Show full "MIT License — Copyright (c) 2026 Jure Sršen" in both
places to match the LICENSE file exactly.
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>
Use justify-between on the QR section so Lightning address sits flush
left and Bitcoin address sits flush right, making them visually
impossible to confuse when scanning.
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>
- tauri-plugin-updater + tauri-plugin-process registered (Rust + npm)
- Updater endpoint: github.com/hoornet/wrystr/releases/latest/download/latest.json
- Ed25519 signing keypair generated; public key in tauri.conf.json;
private key added to TAURI_SIGNING_PRIVATE_KEY GitHub secret
- Release workflow passes TAURI_SIGNING_PRIVATE_KEY env var so
tauri-action signs artifacts and generates latest.json manifest
- useUpdater hook: checks for updates 5 s after startup (silent on
error); exposes available, version, install(), dismiss()
- UpdateBanner: dismissible top-of-app bar shown when an update is
available — "Update & restart" downloads, installs, and relaunches
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>
- 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>
Replaces the raw nostr+walletconnect:// textarea with a two-step wizard:
Step 1 — wallet chooser: grid of 4 cards (Alby Hub, Alby Extension,
Mutiny, Phoenix) each with an "open ↗" link to the wallet's NWC setup
page and a "connect →" button to advance; "I already have a connection
string" skip link for power users.
Step 2 — paste URI: per-wallet numbered instructions, textarea with
real-time format validation (detects missing params, wrong prefix),
green "✓ Valid — relay: hostname" on success, specific error messages
on failure; ← back link to wallet chooser.
Connected state: shows detected wallet name + relay hostname, disconnect
button. Raw URI textarea kept as the fallback path via "generic" wallet.
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>
- 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>