diff --git a/src/components/AccountManager.tsx b/src/components/AccountManager.tsx index 5ad2bcb..ba44c04 100644 --- a/src/components/AccountManager.tsx +++ b/src/components/AccountManager.tsx @@ -1,15 +1,15 @@ +import { useState } from "react"; import { useObservableMemo } from "applesauce-react/hooks"; import { Check, User, UserX, UserPlus, Eye, Puzzle } 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 { Badge } from "@/components/ui/badge"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import Nip05 from "@/components/nostr/nip05"; +import LoginDialog from "@/components/LoginDialog"; import type { IAccount } from "applesauce-accounts"; import type { ISigner } from "applesauce-signers"; @@ -18,7 +18,7 @@ function getAccountTypeBadge(account: IAccount) { if (accountType === "grimoire-readonly" || accountType === "readonly") { return ( - + Read-only @@ -27,7 +27,7 @@ function getAccountTypeBadge(account: IAccount) { if (accountType === "extension") { return ( - + Extension @@ -70,29 +70,23 @@ function AccountCard({
-
- {isActive && ( - - )} - - - - - - -
-
+
+
+ {isActive && ( + + )} +
{displayName}
{getAccountTypeBadge(account)}
- {profile && ( -
- -
- )} -
- {account.pubkey.slice(0, 8)}...{account.pubkey.slice(-8)} +
+ {profile && ( +
+
+ )} +
+ {account.pubkey.slice(0, 8)}...{account.pubkey.slice(-8)}
@@ -128,14 +122,15 @@ function AccountCard({ export default function AccountManager() { const activeAccount = useObservableMemo(() => accountManager.active$, []); const allAccounts = useObservableMemo(() => accountManager.accounts$, []); - const { openCommandLauncher } = useAppShell(); + const [showLoginDialog, setShowLoginDialog] = useState(false); const handleAddAccount = () => { - openCommandLauncher(); + setShowLoginDialog(true); }; return (
+
@@ -157,11 +152,7 @@ export default function AccountManager() {

No accounts yet

- Use the "Add Account" button or type{" "} - - login - {" "} - in the command launcher + Click "Add Account" to get started

) : ( diff --git a/src/components/LoginDialog.tsx b/src/components/LoginDialog.tsx new file mode 100644 index 0000000..045ac69 --- /dev/null +++ b/src/components/LoginDialog.tsx @@ -0,0 +1,142 @@ +import { useState } from "react"; +import { toast } from "sonner"; +import { Eye, Puzzle } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import accountManager from "@/services/accounts"; +import { ExtensionSigner } from "applesauce-signers"; +import { ExtensionAccount } from "applesauce-accounts/accounts"; +import { createAccountFromInput } from "@/lib/login-parser"; + +interface LoginDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export default function LoginDialog({ open, onOpenChange }: LoginDialogProps) { + const [readonlyInput, setReadonlyInput] = useState(""); + const [loading, setLoading] = useState(false); + + const handleReadonlyLogin = async () => { + if (!readonlyInput.trim()) { + toast.error("Please enter an identifier"); + return; + } + + setLoading(true); + try { + const account = await createAccountFromInput(readonlyInput); + accountManager.addAccount(account); + accountManager.setActive(account.id); + toast.success("Account added successfully"); + onOpenChange(false); + setReadonlyInput(""); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to add account", + ); + } finally { + setLoading(false); + } + }; + + const handleExtensionLogin = async () => { + setLoading(true); + try { + const signer = new ExtensionSigner(); + const pubkey = await signer.getPublicKey(); + const account = new ExtensionAccount(pubkey, signer); + accountManager.addAccount(account); + accountManager.setActive(account.id); + toast.success("Connected to extension"); + onOpenChange(false); + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Failed to connect to extension", + ); + } finally { + setLoading(false); + } + }; + + return ( + + + + Add Account + + + + + + + Read-only + + + + Extension + + + + +
+ + setReadonlyInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleReadonlyLogin(); + }} + disabled={loading} + /> +

+ Supports npub, NIP-05, hex pubkey, or nprofile +

+
+ + +
+ + +
+

+ Connect to your browser extension to sign events and encrypt + messages. +

+

+ Supports Alby, nos2x, and other NIP-07 compatible extensions. +

+
+ + +
+
+
+
+ ); +} diff --git a/src/components/WindowRenderer.tsx b/src/components/WindowRenderer.tsx index b7b08d7..dbca3ae 100644 --- a/src/components/WindowRenderer.tsx +++ b/src/components/WindowRenderer.tsx @@ -33,9 +33,7 @@ const SpellsViewer = lazy(() => 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() { @@ -178,23 +176,9 @@ export function WindowRenderer({ window, onClose }: WindowRendererProps) { case "spellbooks": content = ; break; - case "login-handler": - content = ( - - ); - break; case "account-manager": content = ; break; - case "logout-handler": - content = ( - - ); - break; default: content = (
diff --git a/src/components/nostr/user-menu.tsx b/src/components/nostr/user-menu.tsx index 5e6122b..6f045be 100644 --- a/src/components/nostr/user-menu.tsx +++ b/src/components/nostr/user-menu.tsx @@ -1,12 +1,9 @@ import { User, Check, UserPlus, Eye, Puzzle } from "lucide-react"; import accounts from "@/services/accounts"; -import { ExtensionSigner } from "applesauce-signers"; -import { ExtensionAccount } from "applesauce-accounts/accounts"; import { useProfile } from "@/hooks/useProfile"; import { useObservableMemo } from "applesauce-react/hooks"; import { getDisplayName } from "@/lib/nostr-utils"; import { useGrimoire } from "@/core/state"; -import { useAppShell } from "@/components/layouts/AppShellContext"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { @@ -18,10 +15,10 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import Nip05 from "./nip05"; import { RelayLink } from "./RelayLink"; import SettingsDialog from "@/components/SettingsDialog"; +import LoginDialog from "@/components/LoginDialog"; import { useState } from "react"; import type { IAccount } from "applesauce-accounts"; import type { ISigner } from "applesauce-signers"; @@ -31,7 +28,7 @@ function getAccountTypeBadge(account: IAccount) { if (accountType === "grimoire-readonly" || accountType === "readonly") { return ( - + Read-only @@ -40,7 +37,7 @@ function getAccountTypeBadge(account: IAccount) { if (accountType === "extension") { return ( - + Extension @@ -50,21 +47,6 @@ function getAccountTypeBadge(account: IAccount) { return null; } -function UserAvatar({ pubkey }: { pubkey: string }) { - const profile = useProfile(pubkey); - return ( - - - - {getDisplayName(pubkey, profile).slice(2)} - - - ); -} - function UserLabel({ account, }: { @@ -73,8 +55,8 @@ function UserLabel({ const profile = useProfile(account.pubkey); return (
-
- {getDisplayName(account.pubkey, profile)} +
+ {getDisplayName(account.pubkey, profile)} {getAccountTypeBadge(account)}
{profile ? ( @@ -90,9 +72,9 @@ export default function UserMenu() { const account = useObservableMemo(() => accounts.active$, []); const allAccounts = useObservableMemo(() => accounts.accounts$, []); const { state, addWindow } = useGrimoire(); - const { openCommandLauncher } = useAppShell(); const relays = state.activeAccount?.relays; const [showSettings, setShowSettings] = useState(false); + const [showLoginDialog, setShowLoginDialog] = useState(false); // Get other accounts (not the active one) const otherAccounts = allAccounts.filter((acc) => acc.id !== account?.id); @@ -106,16 +88,8 @@ export default function UserMenu() { ); } - async function login() { - try { - const signer = new ExtensionSigner(); - const pubkey = await signer.getPublicKey(); - const account = new ExtensionAccount(pubkey, signer); - accounts.addAccount(account); - accounts.setActive(account); - } catch (err) { - console.error(err); - } + function login() { + setShowLoginDialog(true); } function switchAccount(targetAccount: IAccount) { @@ -123,8 +97,7 @@ export default function UserMenu() { } function addAccount() { - // Open the command launcher (user will type "login" command) - openCommandLauncher(); + setShowLoginDialog(true); } async function logout() { @@ -135,6 +108,7 @@ export default function UserMenu() { return ( <> + @@ -179,10 +149,7 @@ export default function UserMenu() { onClick={() => switchAccount(acc)} className="cursor-crosshair" > -
- - -
+ ))} diff --git a/src/types/app.ts b/src/types/app.ts index 22a406d..ef5401a 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -19,9 +19,7 @@ export type AppId = | "spells" | "spellbooks" | "win" - | "login-handler" - | "account-manager" - | "logout-handler"; + | "account-manager"; export interface WindowInstance { id: string; diff --git a/src/types/man.ts b/src/types/man.ts index 399d62c..d6f36e3 100644 --- a/src/types/man.ts +++ b/src/types/man.ts @@ -5,7 +5,6 @@ import { parseOpenCommand } from "@/lib/open-parser"; import { parseProfileCommand } from "@/lib/profile-parser"; import { parseRelayCommand } from "@/lib/relay-parser"; import { resolveNip05Batch } from "@/lib/nip05"; -import { createAccountFromInput } from "@/lib/login-parser"; export interface ManPageEntry { name: string; @@ -93,53 +92,6 @@ export const manPages: Record = { category: "System", defaultProps: { cmd: "help" }, }, - login: { - name: "login", - section: "1", - synopsis: "login [identifier]", - description: - "Add a new Nostr account to Grimoire. Supports read-only accounts (npub, nip-05, hex pubkey, nprofile) and signing accounts (browser extension via NIP-07). When called without arguments, opens the login dialog with method selection.", - options: [ - { - flag: "[identifier]", - description: - "Account identifier: npub1..., user@domain.com, hex pubkey, nprofile1..., or leave empty to open dialog", - }, - ], - examples: [ - "login Open login dialog", - "login npub1abc... Add read-only account from npub", - "login alice@nostr.com Add read-only account from NIP-05", - "login nprofile1... Add read-only account with relay hints", - "login 3bf0c63f... Add read-only account from hex pubkey", - ], - seeAlso: ["profile", "req"], - appId: "login-handler", - category: "System", - argParser: async (args: string[]) => { - const input = args.join(" ").trim(); - - // No input - open dialog - if (!input) { - return { action: "open-dialog" }; - } - - // Try to create account from input - try { - const account = await createAccountFromInput(input); - return { - action: "add-account", - account, - }; - } catch (error) { - return { - action: "error", - message: error instanceof Error ? error.message : "Unknown error", - }; - } - }, - defaultProps: { action: "open-dialog" }, - }, accounts: { name: "accounts", section: "1", @@ -147,36 +99,11 @@ export const manPages: Record = { 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"], + seeAlso: ["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",