Files
caravel/frontend/src/lib/hooks.ts
T
Jon Staab 2be7f4406a Whatever
2026-03-27 12:57:11 -07:00

198 lines
5.8 KiB
TypeScript

import { createEffect, createResource, createSignal, onCleanup } from "solid-js"
import { AccountManager } from "applesauce-accounts"
import type { IAccount } from "applesauce-accounts"
import { registerCommonAccountTypes } from "applesauce-accounts/accounts"
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 { createEventLoaderForStore } from "applesauce-loaders/loaders"
import { Relay as NostrRelay, RelayManagement } from "applesauce-relay"
import { RelayPool } from "applesauce-relay"
import { NostrConnectSigner } from "applesauce-signers"
import { map, of } from "rxjs"
import {
createRelay,
deactivateRelay,
getIdentity,
getRelay,
getTenant,
listRelays,
listTenantInvoices,
listTenantRelays,
listTenants,
updateRelay,
updateTenantBilling,
type CreateRelayInput,
type Relay,
type Tenant,
type UpdateRelayInput,
} from "./api"
export type UnsignedEvent = {
kind: number
content: string
created_at: number
tags: string[][]
}
export type SignedEvent = UnsignedEvent & {
id: string
pubkey: string
sig: string
}
export type EventSigner = {
signEvent(event: UnsignedEvent): Promise<SignedEvent>
}
export const PLATFORM_NAME = import.meta.env.VITE_PLATFORM_NAME || "Caravel"
export const eventStore = new EventStore()
export const pool = new RelayPool()
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/"],
})
NostrConnectSigner.subscriptionMethod = pool.subscription.bind(pool)
NostrConnectSigner.publishMethod = pool.publish.bind(pool)
export const accountManager = new AccountManager()
registerCommonAccountTypes(accountManager)
export const [account, setAccount] = createSignal<IAccount | undefined>()
export const [identity, { refetch: refetchIdentity, mutate: setIdentity }] = createResource(
() => account()?.pubkey,
pubkey => {
if (pubkey) return getIdentity()
}
)
;(() => {
try {
accountManager.fromJSON(JSON.parse(localStorage.getItem("caravel.accounts")!))
} catch {
// pass
}
const active = localStorage.getItem("caravel.accounts.active")
if (active) {
accountManager.setActive(active)
}
accountManager.active$.subscribe(account => {
setAccount(account)
localStorage.setItem("caravel.accounts", JSON.stringify(accountManager.toJSON(true)))
if (account) {
localStorage.setItem("caravel.accounts.active", account.id)
} else {
localStorage.removeItem("caravel.accounts.active")
}
refetchIdentity()
})
})()
export function useProfilePicture(pubkey: () => string | undefined) {
const [picture, setPicture] = createSignal<string | undefined>()
createEffect(() => {
const pk = pubkey()
if (!pk) {
setPicture(undefined)
return
}
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()
},
}
}
export const useTenant = () => createResource(() => getTenant(account()!.pubkey))
export const useTenantRelays = () => createResource(() => listTenantRelays(account()!.pubkey))
export const useTenantInvoices = () => createResource(() => listTenantInvoices(account()!.pubkey))
export const useRelay = (relayId: () => string) => createResource(relayId, getRelay)
export const useAdminTenants = () => createResource(listTenants)
export const useAdminRelays = () => createResource(listRelays)
export const useAdminTenant = (pubkey: () => string) => createResource(pubkey, getTenant)
export const useAdminTenantRelays = (pubkey: () => string) => createResource(pubkey, listTenantRelays)
export const createRelayForActiveTenant = (input: CreateRelayInput) => createRelay({ ...input, tenant: account()!.pubkey })
export const updateActiveTenantBilling = (nwc_url: string) => updateTenantBilling(account()!.pubkey, { nwc_url })
export const updateRelayById = (id: string, input: UpdateRelayInput) => updateRelay(id, input)
export const updateRelayPlanById = (id: string, plan: string) => updateRelay(id, { plan })
export const deactivateRelayById = (id: string) => deactivateRelay(id)
export async function getRelayMembers(url: string) {
const management = new RelayManagement(new NostrRelay(url), account()!.signer)
try {
return await management.listAllowedPubkeys()
} catch {
return []
}
}
export type { Relay, Tenant }