From cf331007a7ed2907f75f63a4672d1a96087d76ec Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 3 Jun 2026 09:23:00 +0200 Subject: [PATCH] fix refresh --- src/components/AccountList/index.tsx | 66 ++++- src/components/AccountManager/index.tsx | 31 ++- .../AccountQuickSwitchMenuItems.tsx | 93 +++++++ .../BottomNavigationBar/WriteButton.tsx | 7 +- src/components/HelpAndAccountMenu.tsx | 24 +- src/components/LoginDialog/index.tsx | 19 +- src/components/ReadOnlySessionIndicator.tsx | 12 +- src/components/Sidebar/PostButton.tsx | 7 +- src/components/StoredAccountSwitchSelect.tsx | 116 ++++---- src/i18n/locales/de.ts | 2 +- src/i18n/locales/en.ts | 2 +- src/lib/account.test.ts | 58 ++++ src/lib/account.ts | 72 ++++- src/lib/pubkey-nip07.test.ts | 23 ++ src/lib/pubkey.ts | 11 + src/providers/NostrProvider/index.tsx | 247 ++++++++++++++---- src/providers/nostr-context.tsx | 8 +- 17 files changed, 655 insertions(+), 143 deletions(-) create mode 100644 src/components/AccountQuickSwitchMenuItems.tsx create mode 100644 src/lib/account.test.ts create mode 100644 src/lib/pubkey-nip07.test.ts diff --git a/src/components/AccountList/index.tsx b/src/components/AccountList/index.tsx index 7433e5cd..83dbcc74 100644 --- a/src/components/AccountList/index.tsx +++ b/src/components/AccountList/index.tsx @@ -1,13 +1,15 @@ import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' -import { isSameAccount } from '@/lib/account' -import { formatPubkey } from '@/lib/pubkey' +import { isRedundantAccountPick, isSameAccount } from '@/lib/account' +import { formatPubkey, hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey' import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' import { TAccountPointer, TSignerType } from '@/types' import { Trash2 } from 'lucide-react' import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' import { SimpleUserAvatar } from '../UserAvatar' import { SimpleUsername } from '../Username' @@ -23,7 +25,15 @@ export default function AccountList({ * dialogs fighting over focus trapping). */ closeDialog?: () => void }) { - const { accounts, account, switchAccount, removeAccount } = useNostr() + const { t } = useTranslation() + const { + accounts, + account, + switchAccount, + viewAccountAsReadOnly, + removeAccount, + retryNip07SignerForPreferredAccount + } = useNostr() const [switchingAccount, setSwitchingAccount] = useState(null) return ( @@ -33,19 +43,49 @@ export default function AccountList({ key={`${act.pubkey}-${act.signerType}`} className={cn( 'relative rounded-lg', - act.pubkey === account?.pubkey ? 'border border-primary' : 'clickable' + account && + hexPubkeysEqual( + normalizeHexPubkey(act.pubkey), + normalizeHexPubkey(account.pubkey) + ) && + (act.signerType === account.signerType || + (account.signerType === 'npub' && act.signerType === 'nip-07')) + ? 'border border-primary' + : 'clickable' )} onClick={() => { - if (isSameAccount(act, account)) return - setSwitchingAccount(act) - if (act.signerType === 'ncryptsec') { - closeDialog?.() - } - switchAccount(act) - .then(() => { + void (async () => { + if (isRedundantAccountPick(act, account)) { + if (account?.signerType === 'npub' && act.signerType === 'nip-07') { + setSwitchingAccount(act) + const ok = await retryNip07SignerForPreferredAccount() + if (ok) toast.success(t('accountSwitch.extensionConnected')) + else toast.error(t('accountSwitch.extensionRetryFailed')) + setSwitchingAccount(null) + } + return + } + setSwitchingAccount(act) + if (act.signerType === 'ncryptsec') { + closeDialog?.() + } + try { + const needsWriteSigner = + act.signerType === 'nsec' || + act.signerType === 'ncryptsec' || + act.signerType === 'bunker' + const switched = needsWriteSigner + ? await switchAccount(act) + : await viewAccountAsReadOnly(act) + if (!switched) { + toast.error(t('notificationsSwitchAccountFailed')) + return + } if (act.signerType !== 'ncryptsec') afterSwitch() - }) - .finally(() => setSwitchingAccount(null)) + } finally { + setSwitchingAccount(null) + } + })() }} >
diff --git a/src/components/AccountManager/index.tsx b/src/components/AccountManager/index.tsx index 97217e92..e7e5e763 100644 --- a/src/components/AccountManager/index.tsx +++ b/src/components/AccountManager/index.tsx @@ -5,7 +5,8 @@ import { Separator } from '@/components/ui/separator' import { useNostr } from '@/providers/NostrProvider' import { generateSecretKey } from 'nostr-tools' import { nsecEncode } from 'nostr-tools/nip19' -import { useState } from 'react' +import { useState, useCallback } from 'react' +import { Loader2 } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import AccountList from '../AccountList' @@ -41,9 +42,26 @@ function AccountManagerNav({ close?: () => void }) { const { t } = useTranslation() - const { nip07Login, nsecLogin, accounts } = useNostr() + const { nip07Login, nsecLogin, accounts, isNip07LoginInFlight, requestAccountNetworkHydrate } = + useNostr() const [password, setPassword] = useState('') const [signingUp, setSigningUp] = useState(false) + const [extensionLoginPending, setExtensionLoginPending] = useState(false) + + const handleExtensionLogin = useCallback(async () => { + setExtensionLoginPending(true) + try { + const pubkey = await nip07Login() + if (pubkey) { + await requestAccountNetworkHydrate() + close?.() + } + } catch { + // nip07Login toasts and rethrows + } finally { + setExtensionLoginPending(false) + } + }, [nip07Login, close]) const handleSignUp = async () => { setSigningUp(true) @@ -67,7 +85,14 @@ function AccountManagerNav({
{!!window.nostr && ( - )} diff --git a/src/components/AccountQuickSwitchMenuItems.tsx b/src/components/AccountQuickSwitchMenuItems.tsx new file mode 100644 index 00000000..41b1a18d --- /dev/null +++ b/src/components/AccountQuickSwitchMenuItems.tsx @@ -0,0 +1,93 @@ +import { SimpleUserAvatar } from '@/components/UserAvatar' +import { SimpleUsername } from '@/components/Username' +import { + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator +} from '@/components/ui/dropdown-menu' +import { + accountPointerKey, + isRedundantAccountPick, + isSameAccountPubkey, + listSwitchableAccounts +} from '@/lib/account' +import { formatPubkey } from '@/lib/pubkey' +import { cn } from '@/lib/utils' +import { useNostr } from '@/providers/NostrProvider' +import type { TAccountPointer } from '@/types' +import { Check } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' + +export function AccountQuickSwitchMenuItems({ onAfterSwitch }: { onAfterSwitch?: () => void }) { + const { t } = useTranslation() + const { + accounts, + account, + switchAccount, + viewAccountAsReadOnly, + retryNip07SignerForPreferredAccount + } = useNostr() + const rows = listSwitchableAccounts(accounts) + + if (rows.length <= 1) return null + + const handleSwitch = async (act: TAccountPointer) => { + if (isRedundantAccountPick(act, account)) { + if (account?.signerType === 'npub' && act.signerType === 'nip-07') { + const ok = await retryNip07SignerForPreferredAccount() + if (ok) { + toast.success(t('accountSwitch.extensionConnected')) + onAfterSwitch?.() + } else { + toast.error(t('accountSwitch.extensionRetryFailed')) + } + } + return + } + const needsWriteSigner = + act.signerType === 'nsec' || + act.signerType === 'ncryptsec' || + act.signerType === 'bunker' + const switched = needsWriteSigner + ? await switchAccount(act) + : await viewAccountAsReadOnly(act) + if (!switched) { + toast.error(t('notificationsSwitchAccountFailed')) + return + } + onAfterSwitch?.() + } + + return ( + <> + + {t('notificationsViewAsAccount')} + + {rows.map((act) => { + const active = + account != null && + isSameAccountPubkey(act, account) && + (account.signerType === act.signerType || + (account.signerType === 'npub' && act.signerType === 'nip-07')) + return ( + void handleSwitch(act)} + > + + + + + {formatPubkey(act.pubkey)} + + + + + ) + })} + + + ) +} diff --git a/src/components/BottomNavigationBar/WriteButton.tsx b/src/components/BottomNavigationBar/WriteButton.tsx index 03ca092c..404265e1 100644 --- a/src/components/BottomNavigationBar/WriteButton.tsx +++ b/src/components/BottomNavigationBar/WriteButton.tsx @@ -6,16 +6,19 @@ import { useEffect, useState } from 'react' import BottomNavigationBarItem from './BottomNavigationBarItem' export default function WriteButton() { - const { checkLogin } = useNostr() + const { checkLogin, canSignEvents } = useNostr() const [open, setOpen] = useState(false) useEffect(() => { + if (!canSignEvents) return const onRequest = () => { checkLogin(() => setOpen(true)) } postEditorService.addEventListener('requestOpenNewPost', onRequest) return () => postEditorService.removeEventListener('requestOpenNewPost', onRequest) - }, [checkLogin]) + }, [canSignEvents, checkLogin]) + + if (!canSignEvents) return null return ( <> diff --git a/src/components/HelpAndAccountMenu.tsx b/src/components/HelpAndAccountMenu.tsx index 0488e4c8..3295fa28 100644 --- a/src/components/HelpAndAccountMenu.tsx +++ b/src/components/HelpAndAccountMenu.tsx @@ -20,6 +20,8 @@ import { usePrimaryPage } from '@/contexts/primary-page-context' import { useSmartSettingsNavigation } from '@/PageManager' import { useFetchProfile } from '@/hooks/useFetchProfile' import { useNostr } from '@/providers/NostrProvider' +import { AccountQuickSwitchMenuItems } from '@/components/AccountQuickSwitchMenuItems' +import { ReadOnlySessionIndicator } from '@/components/ReadOnlySessionIndicator' import { ActiveRelaysDropdownSection } from '@/components/ConnectedRelays/ActiveRelaysDropdownSection' import { useRelayConnectionRows } from '@/hooks/useRelayConnectionRows' import { ArrowDownUp, Database, LogIn, LogOut, Settings, User, UserRound } from 'lucide-react' @@ -34,17 +36,21 @@ export type HelpAndAccountMenuVariant = 'sidebar' | 'titlebar' function AccountDropdownItems({ onSwitchAccount, onLogoutClick, - onBrowseCache + onBrowseCache, + onCloseMenu }: { onSwitchAccount: () => void onLogoutClick: () => void onBrowseCache: () => void + onCloseMenu?: () => void }) { const { t } = useTranslation() const { navigate } = usePrimaryPage() return ( <> + + navigate('profile')}> {t('Profile')} @@ -83,6 +89,7 @@ function SidebarAccountMenu({ const { t } = useTranslation() const { account, profile } = useNostr() const { current, display } = usePrimaryPage() + const [menuOpen, setMenuOpen] = useState(false) const pubkey = account?.pubkey const { profile: fetchedProfile } = useFetchProfile(pubkey) const active = useMemo(() => current === 'profile' && display, [display, current]) @@ -96,7 +103,7 @@ function SidebarAccountMenu({ const { username, avatar } = resolvedProfile || { username: fallbackUsername, avatar: defaultAvatar } return ( - +