Files
caravel/frontend/src/lib/nostr.ts
T
2026-03-26 14:46:22 -07:00

160 lines
4.8 KiB
TypeScript

import { createEffect, createSignal, onCleanup } from "solid-js"
import { EventStore } from "applesauce-core"
import { getProfilePicture } from "applesauce-core/helpers/profile"
import { createOutboxMap, selectOptimalRelays, setFallbackRelays } from "applesauce-core/helpers/relay-selection"
import { includeMailboxes } from "applesauce-core/observable"
import { RelayPool } from "applesauce-relay"
import { AccountManager } from "applesauce-accounts"
import type { IAccount, SerializedAccount } from "applesauce-accounts"
import { registerCommonAccountTypes } from "applesauce-accounts/accounts"
import { createEventLoaderForStore } from "applesauce-loaders/loaders"
import { NostrConnectSigner } from "applesauce-signers"
import { map, of } from "rxjs"
type NostrTag = string[]
export type UnsignedEvent = {
kind: number
content: string
created_at: number
tags: NostrTag[]
}
export type SignedEvent = UnsignedEvent & {
id: string
pubkey: string
sig: string
}
export type EventSigner = {
signEvent(event: UnsignedEvent): Promise<SignedEvent>
}
export const API_URL = import.meta.env.VITE_API_URL
export const PLATFORM_NAME = import.meta.env.VITE_PLATFORM_NAME || "Caravel"
export const eventStore = new EventStore()
export const pool = new RelayPool()
export const accounts = new AccountManager()
createEventLoaderForStore(eventStore, pool, {
lookupRelays: ["wss://purplepag.es/", "wss://relay.damus.io/", "wss://indexer.coracle.social/"],
extraRelays: ["wss://relay.damus.io/", "wss://nos.lol/", "wss://relay.primal.net/"],
})
const ACCOUNTS_STORAGE_KEY = "caravel.accounts"
const ACTIVE_ACCOUNT_STORAGE_KEY = "caravel.activeAccount"
registerCommonAccountTypes(accounts)
NostrConnectSigner.subscriptionMethod = pool.subscription.bind(pool)
NostrConnectSigner.publishMethod = pool.publish.bind(pool)
export function restoreAccounts() {
const raw = localStorage.getItem(ACCOUNTS_STORAGE_KEY)
if (raw) {
try {
const saved = JSON.parse(raw) as SerializedAccount[]
accounts.fromJSON(saved, true)
} catch {
// ignore corrupted local state
}
}
const activeId = localStorage.getItem(ACTIVE_ACCOUNT_STORAGE_KEY)
if (activeId && accounts.getAccount(activeId)) {
accounts.setActive(activeId)
}
}
export function activateAccount(account: IAccount) {
accounts.addAccount(account)
accounts.setActive(account)
persistAccounts()
}
export function persistAccounts() {
localStorage.setItem(ACCOUNTS_STORAGE_KEY, JSON.stringify(accounts.toJSON(true)))
const active = accounts.getActive()
if (active) {
localStorage.setItem(ACTIVE_ACCOUNT_STORAGE_KEY, active.id)
} else {
localStorage.removeItem(ACTIVE_ACCOUNT_STORAGE_KEY)
}
}
export function useActiveAccount() {
const [account, setAccount] = createSignal(accounts.active)
const sub = accounts.active$.subscribe(setAccount)
onCleanup(() => sub.unsubscribe())
return account
}
export function getActiveSigner(): EventSigner | undefined {
const account = accounts.getActive() as { signer?: EventSigner } | undefined
return account?.signer
}
export function getActivePubkey(): string | undefined {
const account = accounts.getActive() as { pubkey?: string } | undefined
return account?.pubkey
}
export function useProfilePicture(pubkey: () => string | undefined) {
const [picture, setPicture] = createSignal<string | undefined>()
createEffect(() => {
const pk = pubkey()
if (!pk) {
setPicture(undefined)
return
}
// Subscribe to profile changes in the event store
const profileSub = eventStore.profile(pk).subscribe(profile => {
setPicture(getProfilePicture(profile))
})
const reqSub = primeProfiles([pk])
onCleanup(() => {
profileSub.unsubscribe()
reqSub.unsubscribe()
})
})
return picture
}
export function primeProfiles(pubkeys: string[]) {
const uniquePubkeys = Array.from(new Set(pubkeys.filter(Boolean)))
if (uniquePubkeys.length === 0) {
return { unsubscribe() {} }
}
const seedRelays = Array.from(pool.relays.keys())
const mailboxSeedSub = seedRelays.length
? pool.request(seedRelays, { kinds: [10002], authors: uniquePubkeys }).subscribe((event) => {
eventStore.add(event)
})
: undefined
const outboxMap$ = of(uniquePubkeys.map((pubkey) => ({ pubkey }))).pipe(
includeMailboxes(eventStore),
map((pointers) => (seedRelays.length > 0 ? setFallbackRelays(pointers, seedRelays) : pointers)),
map((pointers) => selectOptimalRelays(pointers, { maxConnections: 8, maxRelaysPerUser: 3 })),
map(createOutboxMap),
)
const profileSub = pool.outboxSubscription(outboxMap$, { kinds: [0] }).subscribe((message) => {
if (message !== "EOSE") eventStore.add(message)
})
return {
unsubscribe() {
profileSub.unsubscribe()
mailboxSeedSub?.unsubscribe()
},
}
}