- 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
Tauri 2 requires bundle.createUpdaterArtifacts: true to produce .sig
files and updater zip/tar.gz archives. Without it, TAURI_SIGNING_PRIVATE_KEY
is ignored and latest.json is never generated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Apple Silicon (macos-latest) is sufficient for current Mac user base.
Intel can be revisited if demand arises.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
README: add v0.1.7 features (per-account NWC, image paste, new account
tab, conditional zap, etc.), update "Up next" to Phase 2, add GitHub
Sponsors to Support table.
ROADMAP: mark Phase 1 complete, move all shipped items to changelog
(v0.1.3–v0.1.7), Phase 2 is now the active target.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
"updater" is not a valid Tauri 2.x bundle target — the auto-updater
is configured as a plugin, not a bundle format. This was causing
exit code 1 on all three platforms for the v0.1.6 build.
Also updates release notes in the workflow for v0.1.7.
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>
AppImage library bundling causes cascading compatibility failures on
rolling-release distros (Arch, mesa 26+): libwayland-egl conflicts
break EGL, removing it cascades into GLib version mismatches, and so
on. Native packages sidestep this entirely.
- tauri.conf.json: targets now ["deb","rpm","nsis","msi","dmg","updater"]
— no AppImage
- release.yml: drop LINUXDEPLOY_EXCLUDELIST workaround (no longer needed);
update install instructions in release body
- PKGBUILD: added for Arch/Manjaro users; builds from source via git tag;
desktop entry sets WEBKIT_DISABLE_DMABUF_RENDERER=1 (required on Wayland)
- README: install table updated with distro-specific commands
Arch users: build from source with PKGBUILD or `npm run tauri build`.
Ubuntu/Debian/Fedora/openSUSE: install the .deb or .rpm from Releases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The AppImage bundled libwayland-egl.so.1 from ubuntu-22.04 which is
incompatible with mesa 26's EGL implementation, causing:
"Could not create default EGL display: EGL_BAD_PARAMETER. Aborting..."
Fix: exclude all libwayland-* libs from the AppImage via LINUXDEPLOY_EXCLUDELIST
so the app always uses the system's Wayland libraries. The dev binary
already worked correctly since it uses system libs directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes broken "Zap the developer" link in GitHub release notes.
Includes Lightning, Bitcoin, Ko-fi, and in-app zap instructions.
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>
- tauri "tray-icon" feature enabled
- Tray icon created on startup using the app's default window icon
- Tray menu: "Open Wrystr" (show + focus) and "Quit" (app.exit)
- Left-click on tray icon: show + focus the main window
- Right-click: context menu (platform default)
- Window close button (X) now hides to tray instead of exiting —
"Quit" in the tray menu is the exit point
- Works on Windows (system tray), Linux (status bar tray), macOS
(menu bar); DE-specific tray support varies on Linux
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>