import { AnonUserAvatar } from '@/components/AnonUserAvatar' import { SimpleUserAvatar } from '@/components/UserAvatar' import { Button } from '@/components/ui/button' import { accountPointerKey, createAnonAccountPointer, isAnonAccount, isRedundantAccountPick, listSwitchableAccounts } from '@/lib/account' import { accountPubkeyToHex, formatPubkey, hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey' import { cn } from '@/lib/utils' import { Nip07Signer } from '@/providers/NostrProvider/nip-07.signer' import { useNostr } from '@/providers/NostrProvider' import storage from '@/services/local-storage.service' import type { TAccountPointer } from '@/types' import { Loader2, X } from 'lucide-react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' type Props = { className?: string triggerClassName?: string /** Show the inline label on narrow viewports (e.g. full-screen post composer). */ showLabelAlways?: boolean /** Separator under the row. */ withBottomBorder?: boolean /** Separator above the row (e.g. post editor footer). */ withTopBorder?: boolean /** Align chips to the end (e.g. beside the publish button). */ alignEnd?: boolean /** Post composer: keep clicks from bubbling to the dialog/sheet dismiss layer. */ inComposer?: boolean } const EXTENSION_SYNC_HINT_DISMISSED_PREFIX = 'extensionSyncHintDismissed:' const anonAccount = createAnonAccountPointer() function readExtensionSyncHintDismissed(pubkey: string | null): boolean { if (!pubkey || typeof window === 'undefined') return false try { return sessionStorage.getItem(`${EXTENSION_SYNC_HINT_DISMISSED_PREFIX}${pubkey}`) === 'true' } catch { return false } } /** * Switch {@link useNostr} session among stored accounts (notifications spell, post editor). * Avatar chips instead of a native select; NIP-07 extension sync hint + retry when read-only. */ export default function StoredAccountSwitchSelect({ className, showLabelAlways = false, withBottomBorder = false, withTopBorder = false, alignEnd = false, inComposer = false }: Props) { const { t } = useTranslation() const { pubkey, account, accounts, isAnonSession, switchAccount, isAccountSessionHydrating, retryNip07SignerForPreferredAccount, adoptExtensionNip07Identity } = useNostr() const [switchingKey, setSwitchingKey] = useState(null) const [retryingExtension, setRetryingExtension] = useState(false) const [extensionPubkey, setExtensionPubkey] = useState(null) const [extensionSyncHintDismissed, setExtensionSyncHintDismissed] = useState(false) const sessionPubkey = useMemo(() => { if (isAnonSession) return null const cur = pubkey?.trim() return cur ? normalizeHexPubkey(cur) : null }, [pubkey, isAnonSession]) const storedAccounts = useMemo(() => listSwitchableAccounts(accounts), [accounts]) const needsExtensionSync = useMemo(() => { if (!sessionPubkey || !account || account.signerType !== 'npub') return false return accounts.some( (a) => a.signerType === 'nip-07' && hexPubkeysEqual(normalizeHexPubkey(a.pubkey), sessionPubkey) ) }, [accounts, sessionPubkey, account]) const extensionDiffersFromSession = useMemo(() => { if (!extensionPubkey || !sessionPubkey) return false return !hexPubkeysEqual(normalizeHexPubkey(extensionPubkey), sessionPubkey) }, [extensionPubkey, sessionPubkey]) useEffect(() => { setExtensionSyncHintDismissed(readExtensionSyncHintDismissed(sessionPubkey)) }, [sessionPubkey]) useEffect(() => { if (!needsExtensionSync) { setExtensionPubkey(null) return } let cancelled = false void (async () => { try { const nip07Signer = new Nip07Signer() await nip07Signer.init() const pk = await nip07Signer.getPublicKey() if (!cancelled && pk?.trim()) setExtensionPubkey(pk) } catch { if (!cancelled) setExtensionPubkey(null) } })() return () => { cancelled = true } }, [needsExtensionSync]) useEffect(() => { if (!needsExtensionSync || !extensionPubkey || !sessionPubkey) return if (!hexPubkeysEqual(normalizeHexPubkey(extensionPubkey), sessionPubkey)) return let cancelled = false void (async () => { const ok = await retryNip07SignerForPreferredAccount() if (!cancelled && ok) { toast.success(t('accountSwitch.extensionConnected')) } })() return () => { cancelled = true } }, [ needsExtensionSync, extensionPubkey, sessionPubkey, retryNip07SignerForPreferredAccount, t ]) const handlePick = useCallback( async (nextAccount: TAccountPointer) => { if (isAnonAccount(nextAccount)) { if (isAnonSession) return setSwitchingKey(accountPointerKey(nextAccount)) try { await switchAccount(nextAccount) } finally { setSwitchingKey(null) } return } const target = accountPubkeyToHex(nextAccount.pubkey) if (isRedundantAccountPick(nextAccount, account)) { if (account?.signerType === 'npub' && nextAccount.signerType === 'nip-07') { setSwitchingKey(accountPointerKey(nextAccount)) try { await switchAccount(nextAccount) const ok = await retryNip07SignerForPreferredAccount() if (ok) toast.success(t('accountSwitch.extensionConnected')) else toast.error(t('accountSwitch.extensionRetryFailed')) } finally { setSwitchingKey(null) } } return } setSwitchingKey(accountPointerKey(nextAccount)) try { const switched = await switchAccount(nextAccount) if (!switched) { toast.error(t('notificationsSwitchAccountFailed')) return } if (target && !hexPubkeysEqual(switched, target)) { toast.error(t('notificationsSwitchAccountFailed')) return } if (inComposer && nextAccount.signerType === 'nip-07') { const current = storage.getCurrentAccount() if (current?.signerType === 'npub' && hexPubkeysEqual(current.pubkey, switched)) { toast.error(t('accountSwitch.composerExtensionMismatch')) } } } finally { setSwitchingKey(null) } }, [account, switchAccount, retryNip07SignerForPreferredAccount, t, inComposer, isAnonSession] ) const handleRetryExtension = useCallback(async () => { setRetryingExtension(true) try { const ok = await retryNip07SignerForPreferredAccount() if (ok) { toast.success(t('accountSwitch.extensionConnected')) } else { toast.error(t('accountSwitch.extensionRetryFailed')) } } finally { setRetryingExtension(false) } }, [retryNip07SignerForPreferredAccount, t]) const dismissExtensionSyncHint = useCallback(() => { setExtensionSyncHintDismissed(true) if (!sessionPubkey) return try { sessionStorage.setItem(`${EXTENSION_SYNC_HINT_DISMISSED_PREFIX}${sessionPubkey}`, 'true') } catch { // ignore quota or private browsing } }, [sessionPubkey]) const showSwitcher = inComposer || isAnonSession || storedAccounts.length > 1 || storedAccounts.length > 0 if (!showSwitcher) return null if (!inComposer && storedAccounts.length <= 1 && !isAnonSession) return null const busy = isAccountSessionHydrating || switchingKey !== null const anonActive = isAnonSession const anonSwitching = switchingKey === accountPointerKey(anonAccount) return (
{t('notificationsViewAsAccount')}
{storedAccounts.map((act) => { const pk = normalizeHexPubkey(act.pubkey) const isActive = !isAnonSession && sessionPubkey != null && hexPubkeysEqual(pk, sessionPubkey) && (account?.signerType === act.signerType || (account?.signerType === 'npub' && act.signerType === 'nip-07' && needsExtensionSync)) const isSwitching = switchingKey === accountPointerKey(act) const readOnlyChip = isActive && needsExtensionSync return ( ) })}
{needsExtensionSync && !extensionSyncHintDismissed ? (

{t('accountSwitch.extensionSyncHint')}

{extensionDiffersFromSession ? ( ) : null}
) : null}
) }