forked from coracle/caravel
160 lines
4.8 KiB
TypeScript
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()
|
|
},
|
|
}
|
|
}
|