diff --git a/src/components/sidebar/AccountSwitcher.tsx b/src/components/sidebar/AccountSwitcher.tsx
new file mode 100644
index 0000000..bae9c34
--- /dev/null
+++ b/src/components/sidebar/AccountSwitcher.tsx
@@ -0,0 +1,163 @@
+import { useState } from "react";
+import { useUserStore, SavedAccount } from "../../stores/user";
+import { useUIStore } from "../../stores/ui";
+import { LoginModal } from "../shared/LoginModal";
+import { shortenPubkey } from "../../lib/utils";
+
+function Avatar({ account, size = 6 }: { account: SavedAccount; size?: number }) {
+ const initial = (account.name || account.npub || "?").charAt(0).toUpperCase();
+ const cls = `w-${size} h-${size} rounded-sm object-cover shrink-0`;
+ if (account.picture) {
+ return (
+
{
+ (e.target as HTMLImageElement).style.display = "none";
+ }}
+ />
+ );
+ }
+ return (
+
+ {initial}
+
+ );
+}
+
+export function AccountSwitcher() {
+ const { accounts, pubkey, switchAccount, removeAccount, logout } = useUserStore();
+ const { openProfile } = useUIStore();
+ const [open, setOpen] = useState(false);
+ const [showAddLogin, setShowAddLogin] = useState(false);
+
+ const current = accounts.find((a) => a.pubkey === pubkey) ?? null;
+ const others = accounts.filter((a) => a.pubkey !== pubkey);
+
+ const displayName = (a: SavedAccount) =>
+ a.name || shortenPubkey(a.npub);
+
+ const handleSwitch = async (targetPubkey: string) => {
+ setOpen(false);
+ await switchAccount(targetPubkey);
+ };
+
+ const handleRemove = (e: React.MouseEvent, targetPubkey: string) => {
+ e.stopPropagation();
+ removeAccount(targetPubkey);
+ };
+
+ const handleAddAccount = () => {
+ setOpen(false);
+ setShowAddLogin(true);
+ };
+
+ // Not logged in
+ if (!pubkey || !current) {
+ return (
+ <>
+
+ {accounts.length > 0 && (
+
+ {accounts.map((a) => (
+
+ ))}
+
+
+ )}
+
+
+ {showAddLogin && setShowAddLogin(false)} />}
+ >
+ );
+ }
+
+ return (
+ <>
+
+ {/* Expanded account list */}
+ {open && (
+
+ {others.map((a) => (
+
handleSwitch(a.pubkey)}
+ >
+
+
{displayName(a)}
+
+
+ ))}
+
+
+
+ )}
+
+ {/* Current account row */}
+
+
+
openProfile(pubkey)}
+ >
+
+
{displayName(current)}
+
+
+
+
+ {open && (
+
+
+
+
+ )}
+
+
+
+ {showAddLogin && setShowAddLogin(false)} />}
+ >
+ );
+}
diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx
index 1ff3f2e..e4470dd 100644
--- a/src/components/sidebar/Sidebar.tsx
+++ b/src/components/sidebar/Sidebar.tsx
@@ -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() {
))}
- {/* User / Login */}
- {!sidebarCollapsed && (
-
- {loggedIn ? (
-
-
{ const { pubkey } = useUserStore.getState(); if (pubkey) openProfile(pubkey); }}
- >
- {userAvatar ? (
-

{
- (e.target as HTMLImageElement).style.display = "none";
- }}
- />
- ) : (
-
- {(userName || "?").charAt(0).toUpperCase()}
-
- )}
-
- {userName}
-
-
-
-
- ) : (
-
-
-
- )}
-
- )}
+ {/* Account switcher */}
+ {!sidebarCollapsed && }
{/* Status footer */}
{!sidebarCollapsed && (
@@ -133,7 +83,6 @@ export function Sidebar() {
)}
- {showLogin && setShowLogin(false)} />}
>
);
}
diff --git a/src/stores/user.ts b/src/stores/user.ts
index 5e83b06..9c47ff3 100644
--- a/src/stores/user.ts
+++ b/src/stores/user.ts
@@ -4,6 +4,31 @@ import { getNDK, publishContactList } from "../lib/nostr";
import { nip19 } from "@nostr-dev-kit/ndk";
import { invoke } from "@tauri-apps/api/core";
+export interface SavedAccount {
+ pubkey: string;
+ npub: string;
+ name?: string;
+ picture?: string;
+}
+
+function loadSavedAccounts(): SavedAccount[] {
+ try {
+ return JSON.parse(localStorage.getItem("wrystr_accounts") ?? "[]");
+ } catch {
+ return [];
+ }
+}
+
+function persistAccounts(accounts: SavedAccount[]) {
+ localStorage.setItem("wrystr_accounts", JSON.stringify(accounts));
+}
+
+function upsertAccount(accounts: SavedAccount[], entry: SavedAccount): SavedAccount[] {
+ const idx = accounts.findIndex((a) => a.pubkey === entry.pubkey);
+ if (idx === -1) return [...accounts, entry];
+ return accounts.map((a, i) => (i === idx ? { ...a, ...entry } : a));
+}
+
interface UserState {
pubkey: string | null;
npub: string | null;
@@ -11,11 +36,14 @@ interface UserState {
follows: string[];
loggedIn: boolean;
loginError: string | null;
+ accounts: SavedAccount[];
loginWithNsec: (nsec: string) => Promise;
loginWithPubkey: (pubkey: string) => Promise;
logout: () => void;
restoreSession: () => Promise;
+ switchAccount: (pubkey: string) => Promise;
+ removeAccount: (pubkey: string) => void;
fetchOwnProfile: () => Promise;
fetchFollows: () => Promise;
follow: (pubkey: string) => Promise;
@@ -29,6 +57,7 @@ export const useUserStore = create((set, get) => ({
follows: [],
loggedIn: false,
loginError: null,
+ accounts: loadSavedAccounts(),
loginWithNsec: async (nsecInput: string) => {
try {
@@ -55,9 +84,13 @@ export const useUserStore = create((set, get) => ({
const pubkey = user.pubkey;
const npub = nip19.npubEncode(pubkey);
- set({ pubkey, npub, loggedIn: true, loginError: null });
+ // Update accounts list
+ const accounts = upsertAccount(get().accounts, { pubkey, npub });
+ persistAccounts(accounts);
- // Persist pubkey for session restoration
+ set({ pubkey, npub, loggedIn: true, loginError: null, accounts });
+
+ // Persist active session
localStorage.setItem("wrystr_pubkey", pubkey);
localStorage.setItem("wrystr_login_type", "nsec");
@@ -89,7 +122,12 @@ export const useUserStore = create((set, get) => ({
}
const npub = nip19.npubEncode(pubkey);
- set({ pubkey, npub, loggedIn: true, loginError: null });
+
+ // Update accounts list
+ const accounts = upsertAccount(get().accounts, { pubkey, npub });
+ persistAccounts(accounts);
+
+ set({ pubkey, npub, loggedIn: true, loginError: null, accounts });
localStorage.setItem("wrystr_pubkey", pubkey);
localStorage.setItem("wrystr_login_type", "pubkey");
@@ -104,10 +142,7 @@ export const useUserStore = create((set, get) => ({
logout: () => {
const ndk = getNDK();
ndk.signer = undefined;
- const { pubkey } = get();
- if (pubkey) {
- invoke("delete_nsec", { pubkey }).catch(() => {});
- }
+ // Don't delete the keychain entry — keep the account available for instant switch-back.
localStorage.removeItem("wrystr_pubkey");
localStorage.removeItem("wrystr_login_type");
set({ pubkey: null, npub: null, profile: null, follows: [], loggedIn: false, loginError: null });
@@ -129,14 +164,45 @@ export const useUserStore = create((set, get) => ({
if (nsec) {
await get().loginWithNsec(nsec);
}
- // If no keychain entry (first run after feature lands, or keychain unavailable),
- // the user will be prompted to log in again — same as before.
+ // No keychain entry yet → stay logged out, user re-enters nsec once.
} catch {
// Keychain unavailable (e.g. no secret service on this Linux session) — stay logged out.
}
}
},
+ switchAccount: async (pubkey: string) => {
+ // Try nsec from keychain first; fall back to read-only
+ try {
+ const nsec = await invoke("load_nsec", { pubkey });
+ if (nsec) {
+ await get().loginWithNsec(nsec);
+ return;
+ }
+ } catch {
+ // Keychain unavailable
+ }
+ await get().loginWithPubkey(pubkey);
+ },
+
+ removeAccount: (pubkey: string) => {
+ // Delete keychain entry (best-effort)
+ invoke("delete_nsec", { pubkey }).catch(() => {});
+
+ const accounts = get().accounts.filter((a) => a.pubkey !== pubkey);
+ persistAccounts(accounts);
+ set({ accounts });
+
+ // If removing the active account, clear the session
+ if (get().pubkey === pubkey) {
+ const ndk = getNDK();
+ ndk.signer = undefined;
+ localStorage.removeItem("wrystr_pubkey");
+ localStorage.removeItem("wrystr_login_type");
+ set({ pubkey: null, npub: null, profile: null, follows: [], loggedIn: false, loginError: null });
+ }
+ },
+
fetchOwnProfile: async () => {
const { pubkey } = get();
if (!pubkey) return;
@@ -146,6 +212,13 @@ export const useUserStore = create((set, get) => ({
const user = ndk.getUser({ pubkey });
await user.fetchProfile();
set({ profile: user.profile });
+
+ // Update cached name/picture in accounts list
+ const name = user.profile?.displayName || user.profile?.name;
+ const picture = user.profile?.picture;
+ const accounts = upsertAccount(get().accounts, { pubkey, npub: get().npub!, name, picture });
+ persistAccounts(accounts);
+ set({ accounts });
} catch {
// Profile fetch is non-critical
}