Bump version to 0.1.7 — Windows playtest fixes

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>
This commit is contained in:
Jure
2026-03-11 12:19:48 +01:00
parent 7d68b1fa6f
commit e8ad01117b
15 changed files with 322 additions and 107 deletions

View File

@@ -8,6 +8,7 @@ const LIGHTNING_ADDRESS = "harpos@getalby.com";
const BITCOIN_ADDRESS = "bc1qcgaupf80j28ca537xjlcs9dm9s03khezjs7crp";
const KOFI_URL = "https://ko-fi.com/jure";
const GITHUB_URL = "https://github.com/hoornet/wrystr";
const GITHUB_SPONSORS_URL = "https://github.com/sponsors/hoornet";
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
@@ -111,6 +112,17 @@ export function AboutView() {
{GITHUB_URL}
</a>
</div>
<div className="flex items-center gap-3">
<span className="text-text-muted text-[12px] w-16 shrink-0">Sponsors</span>
<a
href={GITHUB_SPONSORS_URL}
target="_blank"
rel="noopener noreferrer"
className="text-accent text-[12px] hover:text-accent-hover transition-colors"
>
{GITHUB_SPONSORS_URL}
</a>
</div>
</div>
</section>

View File

@@ -1,12 +1,84 @@
import { useState } from "react";
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { useUserStore } from "../../stores/user";
interface LoginModalProps {
onClose: () => void;
}
function NewAccountTab({ onClose }: { onClose: () => void }) {
const { loginWithNsec, loginError } = useUserStore();
const [signer] = useState(() => NDKPrivateKeySigner.generate());
const [confirmed, setConfirmed] = useState(false);
const [copied, setCopied] = useState(false);
const [logging, setLogging] = useState(false);
const nsec = signer.nsec;
const handleCopy = () => {
navigator.clipboard.writeText(nsec);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
const handleConfirm = async () => {
if (!confirmed) return;
setLogging(true);
await loginWithNsec(nsec);
if (!useUserStore.getState().loginError) {
onClose();
}
setLogging(false);
};
return (
<div>
<p className="text-text-muted text-[12px] mb-3">
A new private key has been generated for you. Save it somewhere safe it cannot be recovered.
</p>
<div className="bg-bg border border-border px-3 py-2 font-mono text-[11px] text-text break-all mb-2"
style={{ WebkitUserSelect: "text", userSelect: "text" } as React.CSSProperties}
>
{nsec}
</div>
<button
onClick={handleCopy}
className="text-[11px] px-3 py-1 border border-border text-text-muted hover:text-accent hover:border-accent/40 transition-colors mb-4"
>
{copied ? "copied ✓" : "copy key"}
</button>
<label className="flex items-start gap-2 cursor-pointer mb-4">
<input
type="checkbox"
checked={confirmed}
onChange={(e) => setConfirmed(e.target.checked)}
className="mt-0.5 shrink-0"
/>
<span className="text-text-muted text-[12px]">
I've saved my private key in a safe place
</span>
</label>
{loginError && (
<p className="text-danger text-[11px] mb-2">{loginError}</p>
)}
<button
onClick={handleConfirm}
disabled={!confirmed || logging}
className="w-full px-4 py-2 text-[12px] bg-accent hover:bg-accent-hover text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
{logging ? "logging in…" : "create account"}
</button>
</div>
);
}
export function LoginModal({ onClose }: LoginModalProps) {
const [tab, setTab] = useState<"nsec" | "pubkey">("nsec");
const [tab, setTab] = useState<"nsec" | "pubkey" | "new">("nsec");
const [input, setInput] = useState("");
const { loginWithNsec, loginWithPubkey, loginError } = useUserStore();
@@ -15,7 +87,7 @@ export function LoginModal({ onClose }: LoginModalProps) {
if (tab === "nsec") {
await loginWithNsec(input.trim());
} else {
} else if (tab === "pubkey") {
await loginWithPubkey(input.trim());
}
@@ -60,7 +132,7 @@ export function LoginModal({ onClose }: LoginModalProps) {
: "text-text-muted hover:text-text"
}`}
>
Private key (nsec)
Private key
</button>
<button
onClick={() => setTab("pubkey")}
@@ -70,50 +142,66 @@ export function LoginModal({ onClose }: LoginModalProps) {
: "text-text-muted hover:text-text"
}`}
>
Public key (read-only)
Read-only
</button>
<button
onClick={() => setTab("new")}
className={`flex-1 px-4 py-2 text-[12px] transition-colors ${
tab === "new"
? "text-accent border-b-2 border-accent"
: "text-text-muted hover:text-text"
}`}
>
New account
</button>
</div>
{/* Input */}
{/* Content */}
<div className="p-4">
<label className="block text-text-muted text-[11px] mb-1.5">
{tab === "nsec"
? "Paste your nsec or hex private key"
: "Paste your npub or hex public key"}
</label>
<input
type={tab === "nsec" ? "password" : "text"}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={tab === "nsec" ? "nsec1…" : "npub1…"}
autoFocus
className="w-full bg-bg border border-border px-3 py-2 text-text text-[13px] font-mono placeholder:text-text-dim focus:outline-none focus:border-accent/50"
/>
{tab === "new" ? (
<NewAccountTab onClose={onClose} />
) : (
<>
<label className="block text-text-muted text-[11px] mb-1.5">
{tab === "nsec"
? "Paste your nsec or hex private key"
: "Paste your npub or hex public key"}
</label>
<input
type={tab === "nsec" ? "password" : "text"}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={tab === "nsec" ? "nsec1…" : "npub1…"}
autoFocus
className="w-full bg-bg border border-border px-3 py-2 text-text text-[13px] font-mono placeholder:text-text-dim focus:outline-none focus:border-accent/50"
/>
{tab === "nsec" && (
<p className="text-text-dim text-[10px] mt-1.5">
Your key stays local. Never sent to any server.
</p>
{tab === "nsec" && (
<p className="text-text-dim text-[10px] mt-1.5">
Your key stays local. Never sent to any server.
</p>
)}
{tab === "pubkey" && (
<p className="text-text-dim text-[10px] mt-1.5">
Read-only mode you can browse but not post or zap.
</p>
)}
{loginError && (
<p className="text-danger text-[11px] mt-2">{loginError}</p>
)}
<button
onClick={handleLogin}
disabled={!input.trim()}
className="w-full mt-3 px-4 py-2 text-[12px] bg-accent hover:bg-accent-hover text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
{tab === "nsec" ? "Login" : "View as read-only"}
</button>
</>
)}
{tab === "pubkey" && (
<p className="text-text-dim text-[10px] mt-1.5">
Read-only mode you can browse but not post or zap.
</p>
)}
{loginError && (
<p className="text-danger text-[11px] mt-2">{loginError}</p>
)}
<button
onClick={handleLogin}
disabled={!input.trim()}
className="w-full mt-3 px-4 py-2 text-[12px] bg-accent hover:bg-accent-hover text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
{tab === "nsec" ? "Login" : "View as read-only"}
</button>
</div>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import { useState } from "react";
import { useLightningStore } from "../../stores/lightning";
import { useUserStore } from "../../stores/user";
import { isValidNwcUri, parseNwcUri } from "../../lib/lightning/nwc";
// ── Wallet catalogue ─────────────────────────────────────────────────────────
@@ -166,6 +167,7 @@ function PasteStep({
onConnected: () => void;
}) {
const { setNwcUri } = useLightningStore();
const { pubkey } = useUserStore();
const [input, setInput] = useState("");
const [error, setError] = useState<string | null>(null);
@@ -191,7 +193,7 @@ function PasteStep({
return;
}
try {
setNwcUri(uri);
setNwcUri(uri, pubkey ?? undefined);
onConnected();
} catch (err) {
setError(String(err));
@@ -254,11 +256,12 @@ function PasteStep({
export function NWCWizard() {
const { nwcUri, clearNwcUri } = useLightningStore();
const { pubkey } = useUserStore();
const [step, setStep] = useState<"choose" | "paste">("choose");
const [selectedWallet, setSelectedWallet] = useState<WalletDef>(GENERIC);
if (nwcUri) {
return <ConnectedState nwcUri={nwcUri} onDisconnect={clearNwcUri} />;
return <ConnectedState nwcUri={nwcUri} onDisconnect={() => clearNwcUri(pubkey ?? undefined)} />;
}
if (step === "paste") {