diff --git a/src/components/AccountManager.tsx b/src/components/AccountManager.tsx new file mode 100644 index 0000000..e47fe1a --- /dev/null +++ b/src/components/AccountManager.tsx @@ -0,0 +1,158 @@ +import { useObservableMemo } from "applesauce-react/hooks"; +import { Check, User, UserX, UserPlus } from "lucide-react"; +import { toast } from "sonner"; +import accountManager from "@/services/accounts"; +import { useProfile } from "@/hooks/useProfile"; +import { getDisplayName } from "@/lib/nostr-utils"; +import { useAppShell } from "@/components/layouts/AppShellContext"; +import { Button } from "@/components/ui/button"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import Nip05 from "@/components/nostr/nip05"; + +function AccountCard({ + account, + isActive, +}: { + account: any; + isActive: boolean; +}) { + const profile = useProfile(account.pubkey); + const displayName = getDisplayName(account.pubkey, profile); + + const handleSwitch = () => { + accountManager.setActive(account.id); + toast.success("Switched account", { + description: `Now using ${displayName}`, + }); + }; + + const handleRemove = () => { + const confirmRemove = window.confirm( + `Remove account ${displayName}? This cannot be undone.`, + ); + if (confirmRemove) { + accountManager.removeAccount(account); + toast.success("Account removed", { + description: `Removed ${displayName}`, + }); + } + }; + + return ( + + +
+
+ {isActive && ( + + )} + + + + + + +
+
{displayName}
+ {profile && ( +
+ +
+ )} +
+ {account.pubkey.slice(0, 8)}...{account.pubkey.slice(-8)} +
+
+
+
+ {!isActive && ( + + )} + +
+
+
+
+ ); +} + +/** + * AccountManager - Shows all accounts with management actions + */ +export default function AccountManager() { + const activeAccount = useObservableMemo(() => accountManager.active$, []); + const allAccounts = useObservableMemo(() => accountManager.accounts$, []); + const { openCommandLauncher } = useAppShell(); + + const handleAddAccount = () => { + openCommandLauncher(); + }; + + return ( +
+
+ + + + Accounts + + + + + {allAccounts.length === 0 ? ( +
+ +

No accounts yet

+

+ Use the "Add Account" button or type{" "} + + login + {" "} + in the command launcher +

+
+ ) : ( + allAccounts.map((account) => ( + + )) + )} +
+
+ +
+

+ Tip: You can also switch accounts from the user menu in the + top-right corner +

+
+
+
+ ); +} diff --git a/src/components/LogoutHandler.tsx b/src/components/LogoutHandler.tsx new file mode 100644 index 0000000..35f9e23 --- /dev/null +++ b/src/components/LogoutHandler.tsx @@ -0,0 +1,86 @@ +import { useEffect } from "react"; +import { toast } from "sonner"; +import { LogOut } from "lucide-react"; +import accountManager from "@/services/accounts"; +import { useObservableMemo } from "applesauce-react/hooks"; + +interface LogoutHandlerProps { + action: "logout" | "logout-all"; + all: boolean; +} + +/** + * LogoutHandler - Executes logout command actions + * + * This component handles the result of the /logout command: + * - logout: Removes the active account + * - logout-all: Removes all accounts + */ +export default function LogoutHandler({ action, all }: LogoutHandlerProps) { + const activeAccount = useObservableMemo(() => accountManager.active$, []); + const allAccounts = useObservableMemo(() => accountManager.accounts$, []); + + useEffect(() => { + const handleLogout = () => { + if (action === "logout-all" || all) { + // Remove all accounts + if (allAccounts.length === 0) { + toast.info("No accounts to remove", { + icon: , + }); + return; + } + + const confirmLogoutAll = window.confirm( + `Remove all ${allAccounts.length} account(s)? This cannot be undone.`, + ); + + if (!confirmLogoutAll) { + toast.info("Logout cancelled", { + icon: , + }); + return; + } + + // Remove all accounts + allAccounts.forEach((account) => { + accountManager.removeAccount(account); + }); + + toast.success("All accounts removed", { + description: `Removed ${allAccounts.length} account(s)`, + icon: , + }); + } else { + // Remove only active account + if (!activeAccount) { + toast.info("No active account to remove", { + description: "You are not logged in", + icon: , + }); + return; + } + + accountManager.removeAccount(activeAccount); + + toast.success("Logged out", { + description: `Removed ${activeAccount.pubkey.slice(0, 8)}...${activeAccount.pubkey.slice(-8)}`, + icon: , + }); + } + }; + + handleLogout(); + }, [action, all, activeAccount, allAccounts]); + + // This component doesn't render anything visible - it just executes the action + return ( +
+
+ +

Processing logout...

+

This window can be closed.

+
+
+ ); +} diff --git a/src/components/WindowRenderer.tsx b/src/components/WindowRenderer.tsx index 90debd4..b7b08d7 100644 --- a/src/components/WindowRenderer.tsx +++ b/src/components/WindowRenderer.tsx @@ -34,6 +34,8 @@ const SpellbooksViewer = lazy(() => import("./SpellbooksViewer").then((m) => ({ default: m.SpellbooksViewer })), ); const LoginHandler = lazy(() => import("./LoginHandler")); +const AccountManager = lazy(() => import("./AccountManager")); +const LogoutHandler = lazy(() => import("./LogoutHandler")); // Loading fallback component function ViewerLoading() { @@ -185,6 +187,14 @@ export function WindowRenderer({ window, onClose }: WindowRendererProps) { /> ); break; + case "account-manager": + content = ; + break; + case "logout-handler": + content = ( + + ); + break; default: content = (
diff --git a/src/types/app.ts b/src/types/app.ts index 006975a..22a406d 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -19,7 +19,9 @@ export type AppId = | "spells" | "spellbooks" | "win" - | "login-handler"; + | "login-handler" + | "account-manager" + | "logout-handler"; export interface WindowInstance { id: string; diff --git a/src/types/man.ts b/src/types/man.ts index c20e258..399d62c 100644 --- a/src/types/man.ts +++ b/src/types/man.ts @@ -140,6 +140,43 @@ export const manPages: Record = { }, defaultProps: { action: "open-dialog" }, }, + accounts: { + name: "accounts", + section: "1", + synopsis: "accounts", + description: + "View and manage all Nostr accounts in Grimoire. Shows all logged-in accounts with their connection types, allows switching between accounts, and provides account management options.", + examples: ["accounts View all accounts"], + seeAlso: ["login", "logout", "profile"], + appId: "account-manager", + category: "System", + defaultProps: {}, + }, + logout: { + name: "logout", + section: "1", + synopsis: "logout [--all]", + description: + "Remove the active Nostr account from Grimoire. Use the --all flag to remove all accounts at once.", + options: [ + { + flag: "--all", + description: "Remove all accounts instead of just the active one", + }, + ], + examples: [ + "logout Remove the active account", + "logout --all Remove all accounts", + ], + seeAlso: ["login", "accounts"], + appId: "logout-handler", + category: "System", + argParser: (args: string[]) => { + const all = args.includes("--all"); + return { action: all ? "logout-all" : "logout", all }; + }, + defaultProps: { action: "logout", all: false }, + }, kinds: { name: "kinds", section: "1",