From 6415bcd7b7646c1a652db1c58fd5a6964df1a295 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Thu, 26 Mar 2026 17:07:44 -0700 Subject: [PATCH] Remove a lot of ceremony from frontend state management --- frontend/spec/lib/nostr.md | 85 ------- frontend/src/App.tsx | 46 ++-- frontend/src/components/AppShell.tsx | 17 +- frontend/src/components/Navbar.tsx | 3 +- frontend/src/components/RelayDetailCard.tsx | 2 +- frontend/src/index.tsx | 3 +- frontend/src/lib/api.ts | 21 +- frontend/src/lib/hooks.ts | 217 +++++++++++------- frontend/src/lib/nostr.ts | 159 ------------- frontend/src/pages/Account.tsx | 8 +- frontend/src/pages/Home.tsx | 4 +- frontend/src/pages/Login.tsx | 14 +- .../src/pages/admin/AdminTenantDetail.tsx | 15 +- frontend/src/pages/admin/AdminTenantList.tsx | 3 +- frontend/src/pages/relays/RelayDetail.tsx | 6 +- 15 files changed, 188 insertions(+), 415 deletions(-) delete mode 100644 frontend/spec/lib/nostr.md delete mode 100644 frontend/src/lib/nostr.ts diff --git a/frontend/spec/lib/nostr.md b/frontend/spec/lib/nostr.md deleted file mode 100644 index c90416e..0000000 --- a/frontend/spec/lib/nostr.md +++ /dev/null @@ -1,85 +0,0 @@ -Nostr integration layer for the frontend. It initializes shared Nostr primitives (event store, relay pool, account manager), wires signer transport, persists local account state, and exposes reactive helpers for auth/account/profile data. - -## Constants - -### `API_URL` - -- Reads `VITE_API_URL` from environment. - -### `PLATFORM_NAME` - -- Reads `VITE_PLATFORM_NAME` from environment. -- Falls back to `"Caravel"`. - -### Storage keys - -- `caravel.accounts`: serialized account list. -- `caravel.activeAccount`: currently selected account id. - -## Shared singletons - -### `eventStore` - -- Global `EventStore` used to cache and observe Nostr events (profiles, relay lists, etc). - -### `pool` - -- Global `RelayPool` used for relay connections, subscriptions, and publishing. - -### `accounts` - -- Global `AccountManager` that holds all known local accounts and active account state. - -## Startup wiring - -- Registers common account types with `registerCommonAccountTypes(accounts)`. -- Configures event loading via `createEventLoaderForStore(eventStore, pool, ...)` with lookup and extra relay seeds. -- Binds `NostrConnectSigner` network methods to the app relay pool: - - `subscriptionMethod = pool.subscription.bind(pool)` - - `publishMethod = pool.publish.bind(pool)` - -## `function restoreAccounts()` - -- Restores serialized accounts from localStorage. -- Ignores corrupted local data safely (catch-and-continue). -- Restores the active account id if it still exists in the restored account set. - -## `function activateAccount(account: IAccount)` - -- Adds the account to `accounts`. -- Sets it as active. -- Persists account state to localStorage via `persistAccounts()`. - -## `function persistAccounts()` - -- Serializes all accounts into `caravel.accounts`. -- Persists active account id to `caravel.activeAccount`. -- Removes `caravel.activeAccount` if no active account exists. - -## `function useActiveAccount()` - -- Solid helper that returns a reactive signal for current active account. -- Subscribes to `accounts.active$` and cleans up the subscription automatically. - -## `function useProfilePicture(pubkey: () => string | undefined)` - -- Reactive helper to resolve a user profile picture URL from a pubkey. -- If pubkey is missing, clears picture to `undefined`. -- Subscribes to profile updates in `eventStore` and maps profile to a picture via `getProfilePicture`. -- Calls `primeProfiles([pubkey])` to proactively fetch profile data. -- Cleans up both profile and network subscriptions when dependencies change/unmount. - -## `function primeProfiles(pubkeys: string[])` - -- Preloads Nostr profile events (`kind: 0`) for a set of pubkeys. -- Deduplicates and filters invalid pubkeys. -- Early-returns a no-op unsubscribable when no pubkeys are provided. -- Uses currently connected relays as seeds (`pool.relays.keys()`). -- Optionally fetches relay list events (`kind: 10002`) from seed relays to improve mailbox/outbox routing. -- Builds optimized relay routing by: - - including mailbox pointers from event store, - - applying fallback relays when needed, - - selecting optimal relays with connection limits, - - converting to an outbox map. -- Opens an outbox subscription for profile events and stores incoming events in `eventStore` (ignoring `EOSE`). -- Returns an object with `unsubscribe()` that tears down both profile and mailbox-seed subscriptions. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9020f9a..d96073f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,11 +14,10 @@ import AdminTenantDetail from "./pages/admin/AdminTenantDetail" import AdminRelayList from "./pages/admin/AdminRelayList" import AdminRelayDetail from "./pages/admin/AdminRelayDetail" import AdminRelayEdit from "./pages/admin/AdminRelayEdit" -import { useActiveAccount } from "./lib/nostr" +import { account, identity } from "./lib/hooks" function Layout(props: { children?: any }) { const location = useLocation() - const account = useActiveAccount() const usesAppShell = () => { const path = location.pathname @@ -41,50 +40,39 @@ function Layout(props: { children?: any }) { } export default function App() { - const withTenantAuth = (Page: Component): Component => { + const requireCondition = (Page: Component, condition: () => boolean): Component => { return () => { const navigate = useNavigate() - const account = useActiveAccount() createEffect(() => { - if (!account()) navigate("/login", { replace: true }) - }) - - return - } - } - - const withAdminAuth = (Page: Component): Component => { - return () => { - const navigate = useNavigate() - const account = useActiveAccount() - - createEffect(() => { - if (!account()) navigate("/login", { replace: true }) + if (!condition()) navigate("/", { replace: true }) }) return ( - + ) } } + const requireAdmin = (Page: Component) => requireCondition(Page, () => Boolean(account() && identity()?.is_admin)) + const requireTenant = (Page: Component) => requireCondition(Page, () => Boolean(account() && identity()?.is_tenant)) + return ( - - - - - - - - - - + + + + + + + + + + ) } diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx index 8279a99..4f12bb9 100644 --- a/frontend/src/components/AppShell.tsx +++ b/frontend/src/components/AppShell.tsx @@ -1,8 +1,7 @@ -import { A, useLocation, useNavigate } from "@solidjs/router" +import { A, useLocation } from "@solidjs/router" import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js" import Fuse from "fuse.js" -import { eventStore, primeProfiles, useActiveAccount, useProfilePicture } from "../lib/nostr" -import { useIdentity, useTenantRelays, type Relay } from "../lib/hooks" +import { account, eventStore, identity, primeProfiles, useProfilePicture, useTenantRelays, type Relay } from "../lib/hooks" import serverIcon from "../assets/server.svg" import Modal from "./Modal" @@ -32,16 +31,12 @@ function RelayIcon() { export default function AppShell(props: { children?: any }) { const location = useLocation() - const navigate = useNavigate() - const account = useActiveAccount() const picture = useProfilePicture(() => account()?.pubkey) - const [identity] = useIdentity(() => account()?.pubkey) const [tenantRelays] = useTenantRelays() const [profile, setProfile] = createSignal({}) const [searchOpen, setSearchOpen] = createSignal(false) const [searchQuery, setSearchQuery] = createSignal("") - const isAdmin = createMemo(() => !!identity()?.is_admin) const username = createMemo(() => profile().name || profile().display_name || shortenPubkey(account()?.pubkey)) const nip05 = createMemo(() => profile().nip05 || "No NIP-05") const searchedRelays = createMemo(() => { @@ -83,12 +78,6 @@ export default function AppShell(props: { children?: any }) { }) }) - createEffect(() => { - const currentIdentity = identity() - if (!currentIdentity || currentIdentity.is_admin || currentIdentity.is_tenant || location.pathname === "/") return - navigate("/", { replace: true }) - }) - const myResources = [{ href: "/relays", label: "My Relays" }] const adminResources = [ { href: "/admin/tenants", label: "Tenants" }, @@ -117,7 +106,7 @@ export default function AppShell(props: { children?: any }) { {(item) =>
  • {item.label}
  • }
    - +

    Administration

      diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 5dacf40..01ac36d 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,9 +1,8 @@ import { Show } from "solid-js" import { A } from "@solidjs/router" -import { PLATFORM_NAME, useActiveAccount, useProfilePicture } from "../lib/nostr" +import { PLATFORM_NAME, useProfilePicture, account } from "../lib/hooks" export default function Navbar() { - const account = useActiveAccount() const picture = useProfilePicture(() => account()?.pubkey) return ( diff --git a/frontend/src/components/RelayDetailCard.tsx b/frontend/src/components/RelayDetailCard.tsx index 6dd85c1..30ea4c7 100644 --- a/frontend/src/components/RelayDetailCard.tsx +++ b/frontend/src/components/RelayDetailCard.tsx @@ -1,6 +1,6 @@ import { A } from "@solidjs/router" import { Show, createEffect, createSignal, onCleanup } from "solid-js" -import type { Relay } from "../lib/hooks" +import type { Relay } from "../lib/api" import menuDotsIcon from "../assets/menu-dots-2.svg" import Modal from "./Modal" import PricingTable from "./PricingTable" diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 78a76f4..d99664d 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -2,13 +2,12 @@ import { render } from "solid-js/web" import "./index.css" import App from "./App" -import { PLATFORM_NAME, restoreAccounts } from "./lib/nostr" +import { PLATFORM_NAME } from "./lib/hooks" const root = document.getElementById("root")! document.title = PLATFORM_NAME import("preline").then(() => { - restoreAccounts() render(() => , root) }) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 19e7fc1..0f94f55 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,4 +1,6 @@ -import { API_URL, getActivePubkey, getActiveSigner } from "./nostr" +import { account } from "./hooks" + +const API_URL = import.meta.env.VITE_API_URL type ApiOk = { data: T @@ -108,19 +110,16 @@ export type UpdateRelayInput = { push_enabled?: number } -export async function makeAuth(): Promise { - void (undefined as T | undefined) - - const pubkey = getActivePubkey() - const signer = getActiveSigner() - if (!pubkey || !signer) return undefined +export async function makeAuth(): Promise { + const current = account() + if (!current) return undefined const now = Date.now() - if (authCache && authCache.pubkey === pubkey && authCache.expiresAt > now) { + if (authCache && authCache.pubkey === current.pubkey && authCache.expiresAt > now) { return authCache.value } - const event = await signer.signEvent({ + const event = await current.signer.signEvent({ kind: 27235, content: "", created_at: Math.floor(now / 1000), @@ -129,7 +128,7 @@ export async function makeAuth(): Promise { const value = `Nostr ${btoa(JSON.stringify(event))}` authCache = { - pubkey, + pubkey: current.pubkey, value, expiresAt: now + 10 * 60 * 1000, } @@ -141,7 +140,7 @@ export async function callApi( path: string, body?: TRequest, ): Promise { - const auth = await makeAuth() + const auth = await makeAuth() const url = new URL(path, API_URL).toString() const response = await fetch(url, { diff --git a/frontend/src/lib/hooks.ts b/frontend/src/lib/hooks.ts index 1753c2c..2dc2c8e 100644 --- a/frontend/src/lib/hooks.ts +++ b/frontend/src/lib/hooks.ts @@ -1,5 +1,16 @@ -import { createResource } from "solid-js" +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, @@ -13,130 +24,162 @@ import { updateRelay, updateTenantBilling, type CreateRelayInput, - type Identity, type Relay, type Tenant, type UpdateRelayInput, } from "./api" -import { getActivePubkey, getActiveSigner } from "./nostr" -type IdentityCache = { +export type UnsignedEvent = { + kind: number + content: string + created_at: number + tags: string[][] +} + +export type SignedEvent = UnsignedEvent & { + id: string pubkey: string - value: Identity + sig: string } -let identityCache: IdentityCache | undefined -let identityInflight: Promise | undefined -let identityInflightPubkey: string | undefined - -function requireActivePubkey() { - const pubkey = getActivePubkey() - if (!pubkey) throw new Error("Not logged in") - return pubkey +export type EventSigner = { + signEvent(event: UnsignedEvent): Promise } -async function loadIdentity(pubkey?: string) { - if (!pubkey) throw new Error("Not logged in") +export const PLATFORM_NAME = import.meta.env.VITE_PLATFORM_NAME || "Caravel" - if (identityCache?.pubkey === pubkey) { - return identityCache.value - } +export const eventStore = new EventStore() +export const pool = new RelayPool() - if (identityInflight && identityInflightPubkey === pubkey) { - return identityInflight - } +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 request = getIdentity().then((identity) => { - identityCache = { pubkey, value: identity } - return identity - }).finally(() => { - if (identityInflightPubkey === pubkey) { - identityInflight = undefined - identityInflightPubkey = undefined +NostrConnectSigner.subscriptionMethod = pool.subscription.bind(pool) +NostrConnectSigner.publishMethod = pool.publish.bind(pool) + +export const accountManager = new AccountManager() + +registerCommonAccountTypes(accountManager) + +export const [account, setAccount] = createSignal() + +export const [identity, setIdentity] = createSignal() + +;(() => { + accountManager.active$.subscribe(account => { + setAccount(account) + + localStorage.setItem("caravel.accounts", JSON.stringify(accountManager.toJSON(true))) + + if (account) { + localStorage.setItem("caravel.accounts.active", account.id) + getIdentity().then(setIdentity) + } else { + localStorage.removeItem("caravel.accounts.active") + setIdentity(undefined) } }) +})() - identityInflight = request - identityInflightPubkey = pubkey - return request -} +export function useProfilePicture(pubkey: () => string | undefined) { + const [picture, setPicture] = createSignal() -export function useIdentity(source: () => string | undefined) { - return createResource(source, loadIdentity) -} + createEffect(() => { + const pk = pubkey() -export function useTenant() { - return createResource(async () => { - const pubkey = requireActivePubkey() - return getTenant(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 useTenantRelays() { - return createResource(async () => { - const pubkey = requireActivePubkey() - return listTenantRelays(pubkey) +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 function useTenantInvoices() { - return createResource(async () => { - const pubkey = requireActivePubkey() - return listTenantInvoices(pubkey) - }) +type Identity = { + pubkey: string + is_admin: boolean + is_tenant: boolean } -export function useRelay(relayId: () => string) { - return createResource(relayId, getRelay) -} +export const useTenant = () => createResource(() => getTenant(account()!.pubkey)) -export function useAdminTenants() { - return createResource(listTenants) -} +export const useTenantRelays = () => createResource(() => listTenantRelays(account()!.pubkey)) -export function useAdminRelays() { - return createResource(listRelays) -} +export const useTenantInvoices = () => createResource(() => listTenantInvoices(account()!.pubkey)) -export function useAdminTenantDetail(pubkey: () => string) { - return createResource(pubkey, async (id) => { - const [tenant, relays] = await Promise.all([getTenant(id), listTenantRelays(id)]) - return { tenant, relays } - }) -} +export const useRelay = (relayId: () => string) => createResource(relayId, getRelay) -export async function createRelayForActiveTenant(input: CreateRelayInput) { - const pubkey = requireActivePubkey() - return createRelay({ ...input, tenant: pubkey }) -} +export const useAdminTenants = () => createResource(listTenants) -export function updateActiveTenantBilling(nwc_url: string) { - const pubkey = requireActivePubkey() - return updateTenantBilling(pubkey, { nwc_url }) -} +export const useAdminRelays = () => createResource(listRelays) -export function updateRelayById(id: string, input: UpdateRelayInput) { - return updateRelay(id, input) -} +export const useAdminTenant = (pubkey: () => string) => createResource(pubkey, getTenant) -export function updateRelayPlanById(id: string, plan: string) { - return updateRelay(id, { plan }) -} +export const useAdminTenantRelays = (pubkey: () => string) => createResource(pubkey, listTenantRelays) -export function deactivateRelayById(id: string) { - return deactivateRelay(id) -} +export const createRelayForActiveTenant = (input: CreateRelayInput) => createRelay({ ...input, tenant: account()!.pubkey }) -export async function getRelayMemberCount(relayUrl: string): Promise { - const signer = getActiveSigner() - if (!signer) throw new Error("Not logged in") +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) - const management = new RelayManagement(new NostrRelay(relayUrl), signer) try { - const members = await management.listAllowedPubkeys() - return members.length + return await management.listAllowedPubkeys() } catch { - return undefined + return [] } } diff --git a/frontend/src/lib/nostr.ts b/frontend/src/lib/nostr.ts deleted file mode 100644 index 8313780..0000000 --- a/frontend/src/lib/nostr.ts +++ /dev/null @@ -1,159 +0,0 @@ -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 -} - -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() - - 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() - }, - } -} diff --git a/frontend/src/pages/Account.tsx b/frontend/src/pages/Account.tsx index 61d7e29..e4fa3ed 100644 --- a/frontend/src/pages/Account.tsx +++ b/frontend/src/pages/Account.tsx @@ -1,13 +1,10 @@ import { createEffect, createMemo, createSignal, For, Show } from "solid-js" -import { useNavigate } from "@solidjs/router" import PageContainer from "../components/PageContainer" import LoadingState from "../components/LoadingState" import useMinLoading from "../components/useMinLoading" -import { accounts, persistAccounts } from "../lib/nostr" import { updateActiveTenantBilling, useTenant, useTenantInvoices } from "../lib/hooks" export default function Account() { - const navigate = useNavigate() const [tenant, { refetch: refetchTenant }] = useTenant() const [invoices] = useTenantInvoices() const [nwcUrl, setNwcUrl] = createSignal("") @@ -40,9 +37,8 @@ export default function Account() { } function logout() { - accounts.clearActive() - persistAccounts() - navigate("/") + localStorage.clear() + window.location.href = "/" } return ( diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 3cf7b34..7998ae3 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,6 +1,6 @@ import { A } from "@solidjs/router" import PricingTable from "../components/PricingTable" -import { useActiveAccount } from "../lib/nostr" +import { account } from "../lib/hooks" function CheckIcon() { return ( @@ -21,8 +21,6 @@ function ExternalLinkIcon() { } export default function Home() { - const account = useActiveAccount() - return (
      diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index bbcbefa..54ae88f 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -4,8 +4,7 @@ import { ExtensionAccount, NostrConnectAccount, PasswordAccount, PrivateKeyAccou import { PasswordSigner } from "applesauce-signers" import QrScanner from "qr-scanner" import QRCode from "qrcode" -import { activateAccount } from "../lib/nostr" -import { PLATFORM_NAME } from "../lib/nostr" +import { accountManager, identity, PLATFORM_NAME } from "../lib/hooks" const NIP46_RELAYS = ['wss://bucket.coracle.social', 'wss://ephemeral.snowflare.cc'] @@ -58,8 +57,15 @@ export default function Login() { let abortController: AbortController | undefined async function completeLogin(account: ExtensionAccount | NostrConnectAccount | PrivateKeyAccount | PasswordAccount) { - activateAccount(account) - navigate("/relays") + accountManager.addAccount(account) + accountManager.setActive(account) + + const dispose = createEffect(() => { + if (identity()) { + navigate("/relays") + dispose() + } + }) } async function loginWithNip07() { diff --git a/frontend/src/pages/admin/AdminTenantDetail.tsx b/frontend/src/pages/admin/AdminTenantDetail.tsx index 7bfc808..8439887 100644 --- a/frontend/src/pages/admin/AdminTenantDetail.tsx +++ b/frontend/src/pages/admin/AdminTenantDetail.tsx @@ -4,13 +4,14 @@ import BackLink from "../../components/BackLink" import PageContainer from "../../components/PageContainer" import ResourceState from "../../components/ResourceState" import useMinLoading from "../../components/useMinLoading" -import { useAdminTenantDetail } from "../../lib/hooks" +import { useAdminTenant, useAdminTenantRelays } from "../../lib/hooks" export default function AdminTenantDetail() { const params = useParams() const tenantId = () => params.id ?? "" - const [detail] = useAdminTenantDetail(tenantId) - const loading = useMinLoading(() => detail.loading) + const [tenant] = useAdminTenant(tenantId) + const [relays] = useAdminTenantRelays(tenantId) + const loading = useMinLoading(() => tenant.loading || relays.loading) return ( @@ -19,7 +20,7 @@ export default function AdminTenantDetail() {

      Status

      - +

      Current: tenant @@ -40,9 +41,9 @@ export default function AdminTenantDetail() {

      Relays

      - 0} fallback={

      No relays.

      }> + 0} fallback={

      No relays.

      }>