index.tsx raw

   1  import LoginDialog from '@/components/LoginDialog'
   2  import { ApplicationDataKey, ExtendedKind } from '@/constants'
   3  import {
   4    createDeletionRequestDraftEvent,
   5    createFollowListDraftEvent,
   6    createMuteListDraftEvent,
   7    createRelayListDraftEvent,
   8    createSeenNotificationsAtDraftEvent
   9  } from '@/lib/draft-event'
  10  import {
  11    getLatestEvent,
  12    getReplaceableEventIdentifier,
  13    isProtectedEvent,
  14    minePow
  15  } from '@/lib/event'
  16  import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
  17  import { Pubkey } from '@/domain'
  18  import client from '@/services/client.service'
  19  import customEmojiService from '@/services/custom-emoji.service'
  20  import indexedDb from '@/services/indexed-db.service'
  21  import storage from '@/services/local-storage.service'
  22  import stuffStatsService from '@/services/stuff-stats.service'
  23  import {
  24    ISigner,
  25    TAccount,
  26    TAccountPointer,
  27    TDraftEvent,
  28    TProfile,
  29    TPublishOptions,
  30    TRelayList
  31  } from '@/types'
  32  import * as nobleUtils from '@noble/curves/abstract/utils'
  33  import { bech32 } from '@scure/base'
  34  import dayjs from 'dayjs'
  35  import { Event, kinds, VerifiedEvent } from 'nostr-tools'
  36  import * as nip49 from 'nostr-tools/nip49'
  37  import { createContext, useContext, useEffect, useState } from 'react'
  38  import { useTranslation } from 'react-i18next'
  39  import { toast } from 'sonner'
  40  import { useDeletedEvent } from '../DeletedEventProvider'
  41  import { usePasswordPrompt } from '../PasswordPromptProvider'
  42  import { BunkerSigner, parseBunkerUrl } from './bunker.signer'
  43  import { Nip07Signer } from './nip-07.signer'
  44  import { NpubSigner } from './npub.signer'
  45  import { NsecSigner } from './nsec.signer'
  46  
  47  type TNostrContext = {
  48    isInitialized: boolean
  49    pubkey: string | null
  50    profile: TProfile | null
  51    profileEvent: Event | null
  52    relayList: TRelayList | null
  53    bookmarkListEvent: Event | null
  54    favoriteRelaysEvent: Event | null
  55    userEmojiListEvent: Event | null
  56    pinListEvent: Event | null
  57    notificationsSeenAt: number
  58    account: TAccountPointer | null
  59    accounts: TAccountPointer[]
  60    nsec: string | null
  61    ncryptsec: string | null
  62    switchAccount: (account: TAccountPointer | null) => Promise<void>
  63    nsecLogin: (nsec: string, password?: string, needSetup?: boolean) => Promise<string>
  64    ncryptsecLogin: (ncryptsec: string) => Promise<string>
  65    nip07Login: () => Promise<string>
  66    npubLogin(npub: string): Promise<string>
  67    bunkerLogin: (bunkerUrl: string) => Promise<string>
  68    bunkerLoginWithSigner: (signer: BunkerSigner, pubkey: string) => Promise<string>
  69    removeAccount: (account: TAccountPointer) => void
  70    /**
  71     * Default publish the event to current relays, user's write relays and additional relays
  72     */
  73    publish: (draftEvent: TDraftEvent, options?: TPublishOptions) => Promise<Event>
  74    attemptDelete: (targetEvent: Event) => Promise<void>
  75    signHttpAuth: (url: string, method: string) => Promise<string>
  76    signEvent: (draftEvent: TDraftEvent) => Promise<VerifiedEvent>
  77    nip04Encrypt: (pubkey: string, plainText: string) => Promise<string>
  78    nip04Decrypt: (pubkey: string, cipherText: string) => Promise<string>
  79    nip44Encrypt: (pubkey: string, plainText: string) => Promise<string>
  80    nip44Decrypt: (pubkey: string, cipherText: string) => Promise<string>
  81    hasNip44Support: boolean
  82    startLogin: () => void
  83    checkLogin: <T>(cb?: () => T) => Promise<T | void>
  84    updateRelayListEvent: (relayListEvent: Event) => Promise<void>
  85    updateProfileEvent: (profileEvent: Event) => Promise<void>
  86    updateBookmarkListEvent: (bookmarkListEvent: Event) => Promise<void>
  87    updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise<void>
  88    updateUserEmojiListEvent: (userEmojiListEvent: Event) => Promise<void>
  89    updatePinListEvent: (pinListEvent: Event) => Promise<void>
  90    updateNotificationsSeenAt: (skipPublish?: boolean) => Promise<void>
  91  }
  92  
  93  const NostrContext = createContext<TNostrContext | undefined>(undefined)
  94  
  95  const lastPublishedSeenNotificationsAtEventAtMap = new Map<string, number>()
  96  
  97  export const useNostr = () => {
  98    const context = useContext(NostrContext)
  99    if (!context) {
 100      throw new Error('useNostr must be used within a NostrProvider')
 101    }
 102    return context
 103  }
 104  
 105  export function NostrProvider({ children }: { children: React.ReactNode }) {
 106    const { t } = useTranslation()
 107    const { addDeletedEvent } = useDeletedEvent()
 108    const { promptPassword } = usePasswordPrompt()
 109    const [accounts, setAccounts] = useState<TAccountPointer[]>(
 110      storage.getAccounts().map((act) => ({ pubkey: act.pubkey, signerType: act.signerType }))
 111    )
 112    const [account, setAccount] = useState<TAccountPointer | null>(null)
 113    const [nsec, setNsec] = useState<string | null>(null)
 114    const [ncryptsec, setNcryptsec] = useState<string | null>(null)
 115    const [signer, setSigner] = useState<ISigner | null>(null)
 116    const [openLoginDialog, setOpenLoginDialog] = useState(false)
 117    const [profile, setProfile] = useState<TProfile | null>(null)
 118    const [profileEvent, setProfileEvent] = useState<Event | null>(null)
 119    const [relayList, setRelayList] = useState<TRelayList | null>(null)
 120    const [bookmarkListEvent, setBookmarkListEvent] = useState<Event | null>(null)
 121    const [favoriteRelaysEvent, setFavoriteRelaysEvent] = useState<Event | null>(null)
 122    const [userEmojiListEvent, setUserEmojiListEvent] = useState<Event | null>(null)
 123    const [pinListEvent, setPinListEvent] = useState<Event | null>(null)
 124    const [notificationsSeenAt, setNotificationsSeenAt] = useState(-1)
 125    const [isInitialized, setIsInitialized] = useState(false)
 126  
 127    useEffect(() => {
 128      const init = async () => {
 129        if (hasNostrLoginHash()) {
 130          await loginByNostrLoginHash()
 131          setIsInitialized(true)
 132          return
 133        }
 134  
 135        const accounts = storage.getAccounts()
 136        const act = storage.getCurrentAccount() ?? accounts[0] // auto login the first account
 137        if (!act) {
 138          setIsInitialized(true)
 139          return
 140        }
 141  
 142        // Set account immediately so feed can load based on pubkey
 143        // while signer initializes in the background
 144        setAccount({ pubkey: act.pubkey, signerType: act.signerType })
 145        setIsInitialized(true)
 146  
 147        // Initialize signer in background - feed doesn't need it to load
 148        await loginWithAccountPointer(act)
 149      }
 150      init()
 151  
 152      const handleHashChange = () => {
 153        if (hasNostrLoginHash()) {
 154          loginByNostrLoginHash()
 155        }
 156      }
 157  
 158      window.addEventListener('hashchange', handleHashChange)
 159  
 160      return () => {
 161        window.removeEventListener('hashchange', handleHashChange)
 162      }
 163    }, [])
 164  
 165    useEffect(() => {
 166      const init = async () => {
 167        setRelayList(null)
 168        setProfile(null)
 169        setProfileEvent(null)
 170        setNsec(null)
 171        setFavoriteRelaysEvent(null)
 172        setBookmarkListEvent(null)
 173        setPinListEvent(null)
 174        setNotificationsSeenAt(-1)
 175        if (!account) {
 176          return
 177        }
 178  
 179        const controller = new AbortController()
 180        const storedNsec = storage.getAccountNsec(account.pubkey)
 181        if (storedNsec) {
 182          setNsec(storedNsec)
 183        } else {
 184          setNsec(null)
 185        }
 186        const storedNcryptsec = storage.getAccountNcryptsec(account.pubkey)
 187        if (storedNcryptsec) {
 188          setNcryptsec(storedNcryptsec)
 189        } else {
 190          setNcryptsec(null)
 191        }
 192  
 193        const storedNotificationsSeenAt = storage.getLastReadNotificationTime(account.pubkey)
 194  
 195        const [
 196          storedRelayListEvent,
 197          storedProfileEvent,
 198          storedBookmarkListEvent,
 199          storedFavoriteRelaysEvent,
 200          storedUserEmojiListEvent,
 201          storedPinListEvent
 202        ] = await Promise.all([
 203          indexedDb.getReplaceableEvent(account.pubkey, kinds.RelayList),
 204          indexedDb.getReplaceableEvent(account.pubkey, kinds.Metadata),
 205          indexedDb.getReplaceableEvent(account.pubkey, kinds.BookmarkList),
 206          indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.FAVORITE_RELAYS),
 207          indexedDb.getReplaceableEvent(account.pubkey, kinds.UserEmojiList),
 208          indexedDb.getReplaceableEvent(account.pubkey, kinds.Pinlist)
 209        ])
 210        if (storedRelayListEvent) {
 211          setRelayList(getRelayListFromEvent(storedRelayListEvent, storage.getFilterOutOnionRelays()))
 212        }
 213        if (storedProfileEvent) {
 214          setProfileEvent(storedProfileEvent)
 215          setProfile(getProfileFromEvent(storedProfileEvent))
 216        }
 217        if (storedBookmarkListEvent) {
 218          setBookmarkListEvent(storedBookmarkListEvent)
 219        }
 220        if (storedFavoriteRelaysEvent) {
 221          setFavoriteRelaysEvent(storedFavoriteRelaysEvent)
 222        }
 223        if (storedUserEmojiListEvent) {
 224          setUserEmojiListEvent(storedUserEmojiListEvent)
 225        }
 226        if (storedPinListEvent) {
 227          setPinListEvent(storedPinListEvent)
 228        }
 229  
 230        const relayListEvents = await client.fetchEvents(client.currentRelays, {
 231          kinds: [kinds.RelayList],
 232          authors: [account.pubkey]
 233        })
 234        const relayListEvent = getLatestEvent(relayListEvents) ?? storedRelayListEvent
 235        const relayList = getRelayListFromEvent(relayListEvent, storage.getFilterOutOnionRelays())
 236        if (relayListEvent) {
 237          client.updateRelayListCache(relayListEvent)
 238          await indexedDb.putReplaceableEvent(relayListEvent)
 239        }
 240        setRelayList(relayList)
 241  
 242        const events = await client.fetchEvents(relayList.write.concat(client.currentRelays).slice(0, 4), [
 243          {
 244            kinds: [
 245              kinds.Metadata,
 246              kinds.BookmarkList,
 247              ExtendedKind.FAVORITE_RELAYS,
 248              ExtendedKind.BLOSSOM_SERVER_LIST,
 249              kinds.UserEmojiList,
 250              kinds.Pinlist
 251            ],
 252            authors: [account.pubkey]
 253          },
 254          {
 255            kinds: [kinds.Application],
 256            authors: [account.pubkey],
 257            '#d': [ApplicationDataKey.NOTIFICATIONS_SEEN_AT]
 258          }
 259        ])
 260        const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
 261        const profileEvent = sortedEvents.find((e) => e.kind === kinds.Metadata)
 262        const bookmarkListEvent = sortedEvents.find((e) => e.kind === kinds.BookmarkList)
 263        const favoriteRelaysEvent = sortedEvents.find((e) => e.kind === ExtendedKind.FAVORITE_RELAYS)
 264        const blossomServerListEvent = sortedEvents.find(
 265          (e) => e.kind === ExtendedKind.BLOSSOM_SERVER_LIST
 266        )
 267        const userEmojiListEvent = sortedEvents.find((e) => e.kind === kinds.UserEmojiList)
 268        const notificationsSeenAtEvent = sortedEvents.find(
 269          (e) =>
 270            e.kind === kinds.Application &&
 271            getReplaceableEventIdentifier(e) === ApplicationDataKey.NOTIFICATIONS_SEEN_AT
 272        )
 273        const pinnedNotesEvent = sortedEvents.find((e) => e.kind === kinds.Pinlist)
 274  
 275        if (profileEvent) {
 276          const updatedProfileEvent = await indexedDb.putReplaceableEvent(profileEvent)
 277          if (updatedProfileEvent.id === profileEvent.id) {
 278            setProfileEvent(updatedProfileEvent)
 279            setProfile(getProfileFromEvent(updatedProfileEvent))
 280          }
 281        } else if (!storedProfileEvent) {
 282          const pk = Pubkey.tryFromString(account.pubkey)
 283          setProfile({
 284            pubkey: account.pubkey,
 285            npub: pk?.npub ?? '',
 286            username: pk?.formatNpub(12) ?? account.pubkey.slice(0, 8)
 287          })
 288        }
 289        if (bookmarkListEvent) {
 290          const updateBookmarkListEvent = await indexedDb.putReplaceableEvent(bookmarkListEvent)
 291          if (updateBookmarkListEvent.id === bookmarkListEvent.id) {
 292            setBookmarkListEvent(bookmarkListEvent)
 293          }
 294        }
 295        if (favoriteRelaysEvent) {
 296          const updatedFavoriteRelaysEvent = await indexedDb.putReplaceableEvent(favoriteRelaysEvent)
 297          if (updatedFavoriteRelaysEvent.id === favoriteRelaysEvent.id) {
 298            setFavoriteRelaysEvent(updatedFavoriteRelaysEvent)
 299          }
 300        }
 301        if (blossomServerListEvent) {
 302          await client.updateBlossomServerListEventCache(blossomServerListEvent)
 303        }
 304        if (userEmojiListEvent) {
 305          const updatedUserEmojiListEvent = await indexedDb.putReplaceableEvent(userEmojiListEvent)
 306          if (updatedUserEmojiListEvent.id === userEmojiListEvent.id) {
 307            setUserEmojiListEvent(updatedUserEmojiListEvent)
 308          }
 309        }
 310        if (pinnedNotesEvent) {
 311          const updatedPinnedNotesEvent = await indexedDb.putReplaceableEvent(pinnedNotesEvent)
 312          if (updatedPinnedNotesEvent.id === pinnedNotesEvent.id) {
 313            setPinListEvent(updatedPinnedNotesEvent)
 314          }
 315        }
 316  
 317        const notificationsSeenAt = Math.max(
 318          notificationsSeenAtEvent?.created_at ?? 0,
 319          storedNotificationsSeenAt
 320        )
 321        setNotificationsSeenAt(notificationsSeenAt)
 322        storage.setLastReadNotificationTime(account.pubkey, notificationsSeenAt)
 323  
 324        client.initUserIndexFromFollowings(account.pubkey, controller.signal)
 325        return controller
 326      }
 327      const promise = init()
 328      return () => {
 329        promise.then((controller) => {
 330          controller?.abort()
 331        })
 332      }
 333    }, [account])
 334  
 335    useEffect(() => {
 336      if (!account) return
 337  
 338      const initInteractions = async () => {
 339        const pubkey = account.pubkey
 340        const relayList = await client.fetchRelayList(pubkey)
 341        const events = await client.fetchEvents(relayList.write.slice(0, 4), [
 342          {
 343            authors: [pubkey],
 344            kinds: [kinds.Reaction, kinds.Repost],
 345            limit: 100
 346          },
 347          {
 348            '#P': [pubkey],
 349            kinds: [kinds.Zap],
 350            limit: 100
 351          }
 352        ])
 353        stuffStatsService.updateStuffStatsByEvents(events)
 354      }
 355      initInteractions()
 356    }, [account])
 357  
 358    useEffect(() => {
 359      if (signer) {
 360        client.signer = signer
 361      } else {
 362        client.signer = undefined
 363      }
 364    }, [signer])
 365  
 366    useEffect(() => {
 367      if (account) {
 368        client.pubkey = account.pubkey
 369      } else {
 370        client.pubkey = undefined
 371      }
 372    }, [account])
 373  
 374    useEffect(() => {
 375      customEmojiService.init(userEmojiListEvent)
 376    }, [userEmojiListEvent])
 377  
 378    const hasNostrLoginHash = () => {
 379      return window.location.hash && window.location.hash.startsWith('#nostr-login')
 380    }
 381  
 382    const loginByNostrLoginHash = async () => {
 383      const credential = window.location.hash.replace('#nostr-login=', '')
 384      const urlWithoutHash = window.location.href.split('#')[0]
 385      history.replaceState(null, '', urlWithoutHash)
 386  
 387      if (credential.startsWith('ncryptsec')) {
 388        return await ncryptsecLogin(credential)
 389      } else if (credential.startsWith('nsec')) {
 390        return await nsecLogin(credential)
 391      }
 392    }
 393  
 394    const login = (signer: ISigner, act: TAccount) => {
 395      const newAccounts = storage.addAccount(act)
 396      setAccounts(newAccounts)
 397      storage.switchAccount(act)
 398      setAccount({ pubkey: act.pubkey, signerType: act.signerType })
 399      setSigner(signer)
 400      return act.pubkey
 401    }
 402  
 403    const removeAccount = (act: TAccountPointer) => {
 404      const newAccounts = storage.removeAccount(act)
 405      setAccounts(newAccounts)
 406      if (account?.pubkey === act.pubkey) {
 407        setAccount(null)
 408        setSigner(null)
 409      }
 410    }
 411  
 412    const switchAccount = async (act: TAccountPointer | null) => {
 413      if (!act) {
 414        storage.switchAccount(null)
 415        setAccount(null)
 416        setSigner(null)
 417        return
 418      }
 419      await loginWithAccountPointer(act)
 420    }
 421  
 422    const nsecLogin = async (nsecOrHex: string, password?: string, needSetup?: boolean) => {
 423      const nsecSigner = new NsecSigner()
 424      let privkey: Uint8Array
 425      const input = nsecOrHex.trim()
 426  
 427      if (input.startsWith('nsec')) {
 428        // Use @scure/base bech32 for robust decoding (same as plebeian-signer)
 429        try {
 430          const { prefix, words } = bech32.decode(input as `${string}1${string}`, 5000)
 431          if (prefix !== 'nsec') {
 432            throw new Error('invalid nsec prefix')
 433          }
 434          privkey = new Uint8Array(bech32.fromWords(words))
 435        } catch (err) {
 436          throw new Error(`invalid nsec: ${err instanceof Error ? err.message : 'decode failed'}`)
 437        }
 438      } else if (/^[0-9a-fA-F]{64}$/.test(input)) {
 439        privkey = nobleUtils.hexToBytes(input)
 440      } else {
 441        throw new Error('invalid nsec or hex')
 442      }
 443  
 444      const pubkey = nsecSigner.login(privkey)
 445      if (password) {
 446        const ncryptsec = nip49.encrypt(privkey, password)
 447        login(nsecSigner, { pubkey, signerType: 'ncryptsec', ncryptsec })
 448      } else {
 449        // Use bech32 encode for consistency
 450        const words = bech32.toWords(privkey)
 451        const nsec = bech32.encode('nsec', words, 5000)
 452        login(nsecSigner, { pubkey, signerType: 'nsec', nsec })
 453      }
 454      if (needSetup) {
 455        setupNewUser(nsecSigner)
 456      }
 457      return pubkey
 458    }
 459  
 460    const ncryptsecLogin = async (ncryptsec: string) => {
 461      const password = await promptPassword(t('Enter the password to decrypt your ncryptsec'))
 462      if (!password) {
 463        throw new Error('Password is required')
 464      }
 465      const privkey = nip49.decrypt(ncryptsec, password)
 466      const browserNsecSigner = new NsecSigner()
 467      const pubkey = browserNsecSigner.login(privkey)
 468      return login(browserNsecSigner, { pubkey, signerType: 'ncryptsec', ncryptsec })
 469    }
 470  
 471    const npubLogin = async (npub: string) => {
 472      const npubSigner = new NpubSigner()
 473      const pubkey = npubSigner.login(npub)
 474      return login(npubSigner, { pubkey, signerType: 'npub', npub })
 475    }
 476  
 477    const nip07Login = async () => {
 478      try {
 479        const nip07Signer = new Nip07Signer()
 480        await nip07Signer.init()
 481        const pubkey = await nip07Signer.getPublicKey()
 482        if (!pubkey) {
 483          throw new Error('You did not allow to access your pubkey')
 484        }
 485        return login(nip07Signer, { pubkey, signerType: 'nip-07' })
 486      } catch (err) {
 487        toast.error(t('Login failed') + ': ' + (err as Error).message)
 488        throw err
 489      }
 490    }
 491  
 492    const bunkerLogin = async (bunkerUrl: string) => {
 493      try {
 494        const { pubkey: bunkerPubkey, relays, secret } = parseBunkerUrl(bunkerUrl)
 495        const bunkerSigner = new BunkerSigner(bunkerPubkey, relays, secret)
 496        await bunkerSigner.init()
 497        const pubkey = await bunkerSigner.getPublicKey()
 498        return login(bunkerSigner, {
 499          pubkey,
 500          signerType: 'bunker',
 501          bunkerPubkey,
 502          bunkerRelays: relays,
 503          bunkerSecret: secret
 504        })
 505      } catch (err) {
 506        toast.error(t('Bunker login failed') + ': ' + (err as Error).message)
 507        throw err
 508      }
 509    }
 510  
 511    /**
 512     * Login with an already-connected BunkerSigner instance.
 513     * Used for the nostr+connect flow where we wait for signer to connect.
 514     */
 515    const bunkerLoginWithSigner = async (signer: BunkerSigner, pubkey: string) => {
 516      try {
 517        return login(signer, {
 518          pubkey,
 519          signerType: 'bunker',
 520          bunkerPubkey: signer.getBunkerPubkey(),
 521          bunkerRelays: signer.getRelayUrls(),
 522          bunkerSecret: undefined
 523        })
 524      } catch (err) {
 525        toast.error(t('Bunker login failed') + ': ' + (err as Error).message)
 526        throw err
 527      }
 528    }
 529  
 530    const loginWithAccountPointer = async (act: TAccountPointer): Promise<string | null> => {
 531      let account = storage.findAccount(act)
 532      if (!account) {
 533        return null
 534      }
 535      if (account.signerType === 'nsec' || account.signerType === 'browser-nsec') {
 536        if (account.nsec) {
 537          const browserNsecSigner = new NsecSigner()
 538          browserNsecSigner.login(account.nsec)
 539          // Migrate to nsec
 540          if (account.signerType === 'browser-nsec') {
 541            storage.removeAccount(account)
 542            account = { ...account, signerType: 'nsec' }
 543            storage.addAccount(account)
 544          }
 545          return login(browserNsecSigner, account)
 546        }
 547      } else if (account.signerType === 'ncryptsec') {
 548        if (account.ncryptsec) {
 549          const password = await promptPassword(t('Enter the password to decrypt your ncryptsec'))
 550          if (!password) {
 551            return null
 552          }
 553          const privkey = nip49.decrypt(account.ncryptsec, password)
 554          const browserNsecSigner = new NsecSigner()
 555          browserNsecSigner.login(privkey)
 556          return login(browserNsecSigner, account)
 557        }
 558      } else if (account.signerType === 'nip-07') {
 559        const nip07Signer = new Nip07Signer()
 560        await nip07Signer.init()
 561        return login(nip07Signer, account)
 562      } else if (account.signerType === 'npub' && account.npub) {
 563        const npubSigner = new NpubSigner()
 564        const pubkey = npubSigner.login(account.npub)
 565        if (!pubkey) {
 566          storage.removeAccount(account)
 567          return null
 568        }
 569        if (pubkey !== account.pubkey) {
 570          storage.removeAccount(account)
 571          account = { ...account, pubkey }
 572          storage.addAccount(account)
 573        }
 574        return login(npubSigner, account)
 575      } else if (account.signerType === 'bunker' && account.bunkerPubkey && account.bunkerRelays) {
 576        try {
 577          const bunkerSigner = new BunkerSigner(
 578            account.bunkerPubkey,
 579            account.bunkerRelays,
 580            account.bunkerSecret
 581          )
 582          await bunkerSigner.init()
 583          return login(bunkerSigner, account)
 584        } catch (err) {
 585          console.error('Failed to reconnect to bunker:', err)
 586          toast.error(t('Failed to reconnect to bunker'))
 587          return null
 588        }
 589      }
 590      storage.removeAccount(account)
 591      return null
 592    }
 593  
 594    const setupNewUser = async (signer: ISigner) => {
 595      // Use currently connected relays as the bootstrap relays for new users
 596      const bootstrapRelays = client.currentRelays.length > 0 ? client.currentRelays : []
 597      if (bootstrapRelays.length === 0) return
 598  
 599      await Promise.allSettled([
 600        client.publishEvent(bootstrapRelays, await signer.signEvent(createFollowListDraftEvent([]))),
 601        client.publishEvent(bootstrapRelays, await signer.signEvent(createMuteListDraftEvent([]))),
 602        client.publishEvent(
 603          bootstrapRelays,
 604          await signer.signEvent(
 605            createRelayListDraftEvent(bootstrapRelays.map((url) => ({ url, scope: 'both' })))
 606          )
 607        )
 608      ])
 609    }
 610  
 611    const signEvent = async (draftEvent: TDraftEvent) => {
 612      const event = await signer?.signEvent(draftEvent)
 613      if (!event) {
 614        throw new Error('sign event failed')
 615      }
 616      return event as VerifiedEvent
 617    }
 618  
 619    const publish = async (
 620      draftEvent: TDraftEvent,
 621      { minPow = 0, ...options }: TPublishOptions = {}
 622    ) => {
 623      if (!account || !signer || account.signerType === 'npub') {
 624        throw new Error('You need to login first')
 625      }
 626  
 627      const draft = JSON.parse(JSON.stringify(draftEvent)) as TDraftEvent
 628      let event: VerifiedEvent
 629      if (minPow > 0) {
 630        const unsignedEvent = await minePow({ ...draft, pubkey: account.pubkey }, minPow)
 631        event = await signEvent(unsignedEvent)
 632      } else {
 633        event = await signEvent(draft)
 634      }
 635  
 636      if (event.kind !== kinds.Application && event.pubkey !== account.pubkey) {
 637        const eventAuthor = await client.fetchProfile(event.pubkey)
 638        const result = confirm(
 639          t(
 640            'You are about to publish an event signed by [{{eventAuthorName}}]. You are currently logged in as [{{currentUsername}}]. Are you sure?',
 641            { eventAuthorName: eventAuthor?.username, currentUsername: profile?.username }
 642          )
 643        )
 644        if (!result) {
 645          throw new Error(t('Cancelled'))
 646        }
 647      }
 648  
 649      const relays = await client.determineTargetRelays(event, options)
 650  
 651      await client.publishEvent(relays, event)
 652      return event
 653    }
 654  
 655    const attemptDelete = async (targetEvent: Event) => {
 656      if (!signer) {
 657        throw new Error(t('You need to login first'))
 658      }
 659      if (account?.pubkey !== targetEvent.pubkey) {
 660        throw new Error(t('You can only delete your own notes'))
 661      }
 662  
 663      const deletionRequest = await signEvent(createDeletionRequestDraftEvent(targetEvent))
 664  
 665      const seenOn = client.getSeenEventRelayUrls(targetEvent.id)
 666      const relays = await client.determineTargetRelays(targetEvent, {
 667        specifiedRelayUrls: isProtectedEvent(targetEvent) ? seenOn : undefined,
 668        additionalRelayUrls: seenOn
 669      })
 670  
 671      await client.publishEvent(relays, deletionRequest)
 672  
 673      addDeletedEvent(targetEvent)
 674      toast.success(t('Deletion request sent to {{count}} relays', { count: relays.length }))
 675    }
 676  
 677    const signHttpAuth = async (url: string, method: string, content = '') => {
 678      const event = await signEvent({
 679        content,
 680        kind: kinds.HTTPAuth,
 681        created_at: dayjs().unix(),
 682        tags: [
 683          ['u', url],
 684          ['method', method]
 685        ]
 686      })
 687      return 'Nostr ' + btoa(JSON.stringify(event))
 688    }
 689  
 690    const nip04Encrypt = async (pubkey: string, plainText: string) => {
 691      if (!signer) {
 692        throw new Error('No signer available for NIP-04 encryption')
 693      }
 694      try {
 695        const result = await signer.nip04Encrypt(pubkey, plainText)
 696        if (!result) {
 697          throw new Error('NIP-04 encryption returned empty result')
 698        }
 699        return result
 700      } catch (err) {
 701        console.error('NIP-04 encryption failed:', err)
 702        throw err
 703      }
 704    }
 705  
 706    const nip04Decrypt = async (pubkey: string, cipherText: string) => {
 707      return signer?.nip04Decrypt(pubkey, cipherText) ?? ''
 708    }
 709  
 710    const nip44Encrypt = async (pubkey: string, plainText: string) => {
 711      if (!signer?.nip44Encrypt) {
 712        throw new Error('NIP-44 encryption not supported by this signer')
 713      }
 714      return signer.nip44Encrypt(pubkey, plainText)
 715    }
 716  
 717    const nip44Decrypt = async (pubkey: string, cipherText: string) => {
 718      if (!signer?.nip44Decrypt) {
 719        throw new Error('NIP-44 decryption not supported by this signer')
 720      }
 721      return signer.nip44Decrypt(pubkey, cipherText)
 722    }
 723  
 724    const hasNip44Support = !!signer?.nip44Encrypt && !!signer?.nip44Decrypt
 725  
 726    const checkLogin = async <T,>(cb?: () => T): Promise<T | void> => {
 727      if (signer) {
 728        return cb && cb()
 729      }
 730      return setOpenLoginDialog(true)
 731    }
 732  
 733    const updateRelayListEvent = async (relayListEvent: Event) => {
 734      const newRelayList = await client.updateRelayListCache(relayListEvent)
 735      setRelayList(getRelayListFromEvent(newRelayList, storage.getFilterOutOnionRelays()))
 736    }
 737  
 738    const updateProfileEvent = async (profileEvent: Event) => {
 739      const newProfileEvent = await indexedDb.putReplaceableEvent(profileEvent)
 740      setProfileEvent(newProfileEvent)
 741      setProfile(getProfileFromEvent(newProfileEvent))
 742    }
 743  
 744    const updateBookmarkListEvent = async (bookmarkListEvent: Event) => {
 745      const newBookmarkListEvent = await indexedDb.putReplaceableEvent(bookmarkListEvent)
 746      if (newBookmarkListEvent.id !== bookmarkListEvent.id) return
 747  
 748      setBookmarkListEvent(newBookmarkListEvent)
 749    }
 750  
 751    const updateFavoriteRelaysEvent = async (favoriteRelaysEvent: Event) => {
 752      const newFavoriteRelaysEvent = await indexedDb.putReplaceableEvent(favoriteRelaysEvent)
 753      if (newFavoriteRelaysEvent.id !== favoriteRelaysEvent.id) return
 754  
 755      setFavoriteRelaysEvent(newFavoriteRelaysEvent)
 756    }
 757  
 758    const updateUserEmojiListEvent = async (userEmojiListEvent: Event) => {
 759      const newUserEmojiListEvent = await indexedDb.putReplaceableEvent(userEmojiListEvent)
 760      if (newUserEmojiListEvent.id !== userEmojiListEvent.id) return
 761  
 762      setUserEmojiListEvent(newUserEmojiListEvent)
 763    }
 764  
 765    const updatePinListEvent = async (pinListEvent: Event) => {
 766      const newPinListEvent = await indexedDb.putReplaceableEvent(pinListEvent)
 767      if (newPinListEvent.id !== pinListEvent.id) return
 768  
 769      setPinListEvent(newPinListEvent)
 770    }
 771  
 772    const updateNotificationsSeenAt = async (skipPublish = false) => {
 773      if (!account) return
 774  
 775      const now = dayjs().unix()
 776      storage.setLastReadNotificationTime(account.pubkey, now)
 777      setTimeout(() => {
 778        setNotificationsSeenAt(now)
 779      }, 5_000)
 780  
 781      // Prevent too frequent requests for signing seen notifications events
 782      const lastPublishedSeenNotificationsAtEventAt =
 783        lastPublishedSeenNotificationsAtEventAtMap.get(account.pubkey) ?? -1
 784      if (
 785        !skipPublish &&
 786        (lastPublishedSeenNotificationsAtEventAt < 0 ||
 787          now - lastPublishedSeenNotificationsAtEventAt > 10 * 60) // 10 minutes
 788      ) {
 789        await publish(createSeenNotificationsAtDraftEvent())
 790        lastPublishedSeenNotificationsAtEventAtMap.set(account.pubkey, now)
 791      }
 792    }
 793  
 794    return (
 795      <NostrContext.Provider
 796        value={{
 797          isInitialized,
 798          pubkey: account?.pubkey ?? null,
 799          profile,
 800          profileEvent,
 801          relayList,
 802          bookmarkListEvent,
 803          favoriteRelaysEvent,
 804          userEmojiListEvent,
 805          pinListEvent,
 806          notificationsSeenAt,
 807          account,
 808          accounts,
 809          nsec,
 810          ncryptsec,
 811          switchAccount,
 812          nsecLogin,
 813          ncryptsecLogin,
 814          nip07Login,
 815          npubLogin,
 816          bunkerLogin,
 817          bunkerLoginWithSigner,
 818          removeAccount,
 819          publish,
 820          attemptDelete,
 821          signHttpAuth,
 822          nip04Encrypt,
 823          nip04Decrypt,
 824          nip44Encrypt,
 825          nip44Decrypt,
 826          hasNip44Support,
 827          startLogin: () => setOpenLoginDialog(true),
 828          checkLogin,
 829          signEvent,
 830          updateRelayListEvent,
 831          updateProfileEvent,
 832          updateBookmarkListEvent,
 833          updateFavoriteRelaysEvent,
 834          updateUserEmojiListEvent,
 835          updatePinListEvent,
 836          updateNotificationsSeenAt
 837        }}
 838      >
 839        {children}
 840        <LoginDialog open={openLoginDialog} setOpen={setOpenLoginDialog} />
 841      </NostrContext.Provider>
 842    )
 843  }
 844