Add multi-account / profile switcher (roadmap #2)

- 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>
This commit is contained in:
Jure
2026-03-10 17:28:39 +01:00
parent 4ef824a26a
commit a8627b7305
3 changed files with 250 additions and 65 deletions

View File

@@ -1,10 +1,8 @@
import { useState } from "react";
import { useUIStore } from "../../stores/ui";
import { useFeedStore } from "../../stores/feed";
import { useUserStore } from "../../stores/user";
import { getNDK } from "../../lib/nostr";
import { LoginModal } from "../shared/LoginModal";
import { shortenPubkey } from "../../lib/utils";
import { AccountSwitcher } from "./AccountSwitcher";
const NAV_ITEMS = [
{ id: "feed" as const, label: "feed", icon: "◈" },
@@ -14,13 +12,9 @@ const NAV_ITEMS = [
] as const;
export function Sidebar() {
const { currentView, setView, sidebarCollapsed, toggleSidebar, openProfile } = useUIStore();
const { currentView, setView, sidebarCollapsed, toggleSidebar } = useUIStore();
const { connected, notes } = useFeedStore();
const { loggedIn, profile, npub, logout } = useUserStore();
const [showLogin, setShowLogin] = useState(false);
const userName = profile?.displayName || profile?.name || (npub ? shortenPubkey(npub) : null);
const userAvatar = profile?.picture;
const { loggedIn } = useUserStore();
return (
<>
@@ -74,52 +68,8 @@ export function Sidebar() {
))}
</nav>
{/* User / Login */}
{!sidebarCollapsed && (
<div className="border-t border-border shrink-0">
{loggedIn ? (
<div className="px-3 py-2">
<div
className="flex items-center gap-2 mb-1.5 cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => { const { pubkey } = useUserStore.getState(); if (pubkey) openProfile(pubkey); }}
>
{userAvatar ? (
<img
src={userAvatar}
alt=""
className="w-6 h-6 rounded-sm object-cover"
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
) : (
<div className="w-6 h-6 rounded-sm bg-accent/20 flex items-center justify-center text-accent text-[10px]">
{(userName || "?").charAt(0).toUpperCase()}
</div>
)}
<span className="text-text text-[11px] truncate flex-1">
{userName}
</span>
</div>
<button
onClick={logout}
className="text-text-dim hover:text-danger text-[10px] transition-colors"
>
logout
</button>
</div>
) : (
<div className="px-3 py-2">
<button
onClick={() => setShowLogin(true)}
className="w-full px-2 py-1.5 text-[11px] border border-border text-text-muted hover:text-accent hover:border-accent/40 transition-colors"
>
login
</button>
</div>
)}
</div>
)}
{/* Account switcher */}
{!sidebarCollapsed && <AccountSwitcher />}
{/* Status footer */}
{!sidebarCollapsed && (
@@ -133,7 +83,6 @@ export function Sidebar() {
)}
</aside>
{showLogin && <LoginModal onClose={() => setShowLogin(false)} />}
</>
);
}