forked from coracle/caravel
Remove a lot of ceremony from frontend state management
This commit is contained in:
@@ -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.
|
|
||||||
+17
-29
@@ -14,11 +14,10 @@ import AdminTenantDetail from "./pages/admin/AdminTenantDetail"
|
|||||||
import AdminRelayList from "./pages/admin/AdminRelayList"
|
import AdminRelayList from "./pages/admin/AdminRelayList"
|
||||||
import AdminRelayDetail from "./pages/admin/AdminRelayDetail"
|
import AdminRelayDetail from "./pages/admin/AdminRelayDetail"
|
||||||
import AdminRelayEdit from "./pages/admin/AdminRelayEdit"
|
import AdminRelayEdit from "./pages/admin/AdminRelayEdit"
|
||||||
import { useActiveAccount } from "./lib/nostr"
|
import { account, identity } from "./lib/hooks"
|
||||||
|
|
||||||
function Layout(props: { children?: any }) {
|
function Layout(props: { children?: any }) {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const account = useActiveAccount()
|
|
||||||
|
|
||||||
const usesAppShell = () => {
|
const usesAppShell = () => {
|
||||||
const path = location.pathname
|
const path = location.pathname
|
||||||
@@ -41,50 +40,39 @@ function Layout(props: { children?: any }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const withTenantAuth = (Page: Component): Component => {
|
const requireCondition = (Page: Component, condition: () => boolean): Component => {
|
||||||
return () => {
|
return () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const account = useActiveAccount()
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!account()) navigate("/login", { replace: true })
|
if (!condition()) navigate("/", { replace: true })
|
||||||
})
|
|
||||||
|
|
||||||
return <Show when={account()}><Page /></Show>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const withAdminAuth = (Page: Component): Component => {
|
|
||||||
return () => {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const account = useActiveAccount()
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!account()) navigate("/login", { replace: true })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={account()}>
|
<Show when={condition()}>
|
||||||
<Page />
|
<Page />
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requireAdmin = (Page: Component) => requireCondition(Page, () => Boolean(account() && identity()?.is_admin))
|
||||||
|
const requireTenant = (Page: Component) => requireCondition(Page, () => Boolean(account() && identity()?.is_tenant))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router root={Layout}>
|
<Router root={Layout}>
|
||||||
<Route path="/" component={Home} />
|
<Route path="/" component={Home} />
|
||||||
<Route path="/login" component={Login} />
|
<Route path="/login" component={Login} />
|
||||||
<Route path="/relays" component={withTenantAuth(RelayList)} />
|
<Route path="/relays" component={requireTenant(RelayList)} />
|
||||||
<Route path="/relays/new" component={withTenantAuth(RelayNew)} />
|
<Route path="/relays/new" component={requireTenant(RelayNew)} />
|
||||||
<Route path="/relays/:id" component={withTenantAuth(RelayDetail)} />
|
<Route path="/relays/:id" component={requireTenant(RelayDetail)} />
|
||||||
<Route path="/relays/:id/edit" component={withTenantAuth(RelayEdit)} />
|
<Route path="/relays/:id/edit" component={requireTenant(RelayEdit)} />
|
||||||
<Route path="/account" component={withTenantAuth(Account)} />
|
<Route path="/account" component={requireTenant(Account)} />
|
||||||
<Route path="/admin/tenants" component={withAdminAuth(AdminTenantList)} />
|
<Route path="/admin/tenants" component={requireAdmin(AdminTenantList)} />
|
||||||
<Route path="/admin/tenants/:id" component={withAdminAuth(AdminTenantDetail)} />
|
<Route path="/admin/tenants/:id" component={requireAdmin(AdminTenantDetail)} />
|
||||||
<Route path="/admin/relays" component={withAdminAuth(AdminRelayList)} />
|
<Route path="/admin/relays" component={requireAdmin(AdminRelayList)} />
|
||||||
<Route path="/admin/relays/:id" component={withAdminAuth(AdminRelayDetail)} />
|
<Route path="/admin/relays/:id" component={requireAdmin(AdminRelayDetail)} />
|
||||||
<Route path="/admin/relays/:id/edit" component={withAdminAuth(AdminRelayEdit)} />
|
<Route path="/admin/relays/:id/edit" component={requireAdmin(AdminRelayEdit)} />
|
||||||
</Router>
|
</Router>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"
|
||||||
import Fuse from "fuse.js"
|
import Fuse from "fuse.js"
|
||||||
import { eventStore, primeProfiles, useActiveAccount, useProfilePicture } from "../lib/nostr"
|
import { account, eventStore, identity, primeProfiles, useProfilePicture, useTenantRelays, type Relay } from "../lib/hooks"
|
||||||
import { useIdentity, useTenantRelays, type Relay } from "../lib/hooks"
|
|
||||||
import serverIcon from "../assets/server.svg"
|
import serverIcon from "../assets/server.svg"
|
||||||
import Modal from "./Modal"
|
import Modal from "./Modal"
|
||||||
|
|
||||||
@@ -32,16 +31,12 @@ function RelayIcon() {
|
|||||||
|
|
||||||
export default function AppShell(props: { children?: any }) {
|
export default function AppShell(props: { children?: any }) {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
|
||||||
const account = useActiveAccount()
|
|
||||||
const picture = useProfilePicture(() => account()?.pubkey)
|
const picture = useProfilePicture(() => account()?.pubkey)
|
||||||
const [identity] = useIdentity(() => account()?.pubkey)
|
|
||||||
const [tenantRelays] = useTenantRelays()
|
const [tenantRelays] = useTenantRelays()
|
||||||
const [profile, setProfile] = createSignal<Profile>({})
|
const [profile, setProfile] = createSignal<Profile>({})
|
||||||
const [searchOpen, setSearchOpen] = createSignal(false)
|
const [searchOpen, setSearchOpen] = createSignal(false)
|
||||||
const [searchQuery, setSearchQuery] = createSignal("")
|
const [searchQuery, setSearchQuery] = createSignal("")
|
||||||
|
|
||||||
const isAdmin = createMemo(() => !!identity()?.is_admin)
|
|
||||||
const username = createMemo(() => profile().name || profile().display_name || shortenPubkey(account()?.pubkey))
|
const username = createMemo(() => profile().name || profile().display_name || shortenPubkey(account()?.pubkey))
|
||||||
const nip05 = createMemo(() => profile().nip05 || "No NIP-05")
|
const nip05 = createMemo(() => profile().nip05 || "No NIP-05")
|
||||||
const searchedRelays = createMemo<Relay[]>(() => {
|
const searchedRelays = createMemo<Relay[]>(() => {
|
||||||
@@ -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 myResources = [{ href: "/relays", label: "My Relays" }]
|
||||||
const adminResources = [
|
const adminResources = [
|
||||||
{ href: "/admin/tenants", label: "Tenants" },
|
{ href: "/admin/tenants", label: "Tenants" },
|
||||||
@@ -117,7 +106,7 @@ export default function AppShell(props: { children?: any }) {
|
|||||||
<For each={myResources}>{(item) => <li><A href={item.href} class={navItemClass(item.href)}>{item.label}</A></li>}</For>
|
<For each={myResources}>{(item) => <li><A href={item.href} class={navItemClass(item.href)}>{item.label}</A></li>}</For>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Show when={isAdmin()}>
|
<Show when={identity()?.is_admin}>
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h2 class="px-3 text-xs font-semibold uppercase tracking-wider text-white/60">Administration</h2>
|
<h2 class="px-3 text-xs font-semibold uppercase tracking-wider text-white/60">Administration</h2>
|
||||||
<ul class="mt-2 space-y-1">
|
<ul class="mt-2 space-y-1">
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
import { A } from "@solidjs/router"
|
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() {
|
export default function Navbar() {
|
||||||
const account = useActiveAccount()
|
|
||||||
const picture = useProfilePicture(() => account()?.pubkey)
|
const picture = useProfilePicture(() => account()?.pubkey)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { A } from "@solidjs/router"
|
import { A } from "@solidjs/router"
|
||||||
import { Show, createEffect, createSignal, onCleanup } from "solid-js"
|
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 menuDotsIcon from "../assets/menu-dots-2.svg"
|
||||||
import Modal from "./Modal"
|
import Modal from "./Modal"
|
||||||
import PricingTable from "./PricingTable"
|
import PricingTable from "./PricingTable"
|
||||||
|
|||||||
@@ -2,13 +2,12 @@
|
|||||||
import { render } from "solid-js/web"
|
import { render } from "solid-js/web"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
import App from "./App"
|
import App from "./App"
|
||||||
import { PLATFORM_NAME, restoreAccounts } from "./lib/nostr"
|
import { PLATFORM_NAME } from "./lib/hooks"
|
||||||
|
|
||||||
const root = document.getElementById("root")!
|
const root = document.getElementById("root")!
|
||||||
|
|
||||||
document.title = PLATFORM_NAME
|
document.title = PLATFORM_NAME
|
||||||
|
|
||||||
import("preline").then(() => {
|
import("preline").then(() => {
|
||||||
restoreAccounts()
|
|
||||||
render(() => <App />, root)
|
render(() => <App />, root)
|
||||||
})
|
})
|
||||||
|
|||||||
+10
-11
@@ -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<T> = {
|
type ApiOk<T> = {
|
||||||
data: T
|
data: T
|
||||||
@@ -108,19 +110,16 @@ export type UpdateRelayInput = {
|
|||||||
push_enabled?: number
|
push_enabled?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function makeAuth<T = unknown>(): Promise<string | undefined> {
|
export async function makeAuth(): Promise<string | undefined> {
|
||||||
void (undefined as T | undefined)
|
const current = account()
|
||||||
|
if (!current) return undefined
|
||||||
const pubkey = getActivePubkey()
|
|
||||||
const signer = getActiveSigner()
|
|
||||||
if (!pubkey || !signer) return undefined
|
|
||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (authCache && authCache.pubkey === pubkey && authCache.expiresAt > now) {
|
if (authCache && authCache.pubkey === current.pubkey && authCache.expiresAt > now) {
|
||||||
return authCache.value
|
return authCache.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = await signer.signEvent({
|
const event = await current.signer.signEvent({
|
||||||
kind: 27235,
|
kind: 27235,
|
||||||
content: "",
|
content: "",
|
||||||
created_at: Math.floor(now / 1000),
|
created_at: Math.floor(now / 1000),
|
||||||
@@ -129,7 +128,7 @@ export async function makeAuth<T = unknown>(): Promise<string | undefined> {
|
|||||||
|
|
||||||
const value = `Nostr ${btoa(JSON.stringify(event))}`
|
const value = `Nostr ${btoa(JSON.stringify(event))}`
|
||||||
authCache = {
|
authCache = {
|
||||||
pubkey,
|
pubkey: current.pubkey,
|
||||||
value,
|
value,
|
||||||
expiresAt: now + 10 * 60 * 1000,
|
expiresAt: now + 10 * 60 * 1000,
|
||||||
}
|
}
|
||||||
@@ -141,7 +140,7 @@ export async function callApi<TRequest = unknown, TResponse = unknown>(
|
|||||||
path: string,
|
path: string,
|
||||||
body?: TRequest,
|
body?: TRequest,
|
||||||
): Promise<TResponse> {
|
): Promise<TResponse> {
|
||||||
const auth = await makeAuth<TRequest>()
|
const auth = await makeAuth()
|
||||||
const url = new URL(path, API_URL).toString()
|
const url = new URL(path, API_URL).toString()
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
|
|||||||
+130
-87
@@ -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 { Relay as NostrRelay, RelayManagement } from "applesauce-relay"
|
||||||
|
import { RelayPool } from "applesauce-relay"
|
||||||
|
import { NostrConnectSigner } from "applesauce-signers"
|
||||||
|
import { map, of } from "rxjs"
|
||||||
import {
|
import {
|
||||||
createRelay,
|
createRelay,
|
||||||
deactivateRelay,
|
deactivateRelay,
|
||||||
@@ -13,130 +24,162 @@ import {
|
|||||||
updateRelay,
|
updateRelay,
|
||||||
updateTenantBilling,
|
updateTenantBilling,
|
||||||
type CreateRelayInput,
|
type CreateRelayInput,
|
||||||
type Identity,
|
|
||||||
type Relay,
|
type Relay,
|
||||||
type Tenant,
|
type Tenant,
|
||||||
type UpdateRelayInput,
|
type UpdateRelayInput,
|
||||||
} from "./api"
|
} 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
|
pubkey: string
|
||||||
value: Identity
|
sig: string
|
||||||
}
|
}
|
||||||
|
|
||||||
let identityCache: IdentityCache | undefined
|
export type EventSigner = {
|
||||||
let identityInflight: Promise<Identity> | undefined
|
signEvent(event: UnsignedEvent): Promise<SignedEvent>
|
||||||
let identityInflightPubkey: string | undefined
|
|
||||||
|
|
||||||
function requireActivePubkey() {
|
|
||||||
const pubkey = getActivePubkey()
|
|
||||||
if (!pubkey) throw new Error("Not logged in")
|
|
||||||
return pubkey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadIdentity(pubkey?: string) {
|
export const PLATFORM_NAME = import.meta.env.VITE_PLATFORM_NAME || "Caravel"
|
||||||
if (!pubkey) throw new Error("Not logged in")
|
|
||||||
|
|
||||||
if (identityCache?.pubkey === pubkey) {
|
export const eventStore = new EventStore()
|
||||||
return identityCache.value
|
export const pool = new RelayPool()
|
||||||
}
|
|
||||||
|
|
||||||
if (identityInflight && identityInflightPubkey === pubkey) {
|
createEventLoaderForStore(eventStore, pool, {
|
||||||
return identityInflight
|
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) => {
|
NostrConnectSigner.subscriptionMethod = pool.subscription.bind(pool)
|
||||||
identityCache = { pubkey, value: identity }
|
NostrConnectSigner.publishMethod = pool.publish.bind(pool)
|
||||||
return identity
|
|
||||||
}).finally(() => {
|
export const accountManager = new AccountManager()
|
||||||
if (identityInflightPubkey === pubkey) {
|
|
||||||
identityInflight = undefined
|
registerCommonAccountTypes(accountManager)
|
||||||
identityInflightPubkey = undefined
|
|
||||||
|
export const [account, setAccount] = createSignal<IAccount | undefined>()
|
||||||
|
|
||||||
|
export const [identity, setIdentity] = createSignal<Identity | undefined>()
|
||||||
|
|
||||||
|
;(() => {
|
||||||
|
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
|
export function useProfilePicture(pubkey: () => string | undefined) {
|
||||||
identityInflightPubkey = pubkey
|
const [picture, setPicture] = createSignal<string | undefined>()
|
||||||
return request
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useIdentity(source: () => string | undefined) {
|
createEffect(() => {
|
||||||
return createResource(source, loadIdentity)
|
const pk = pubkey()
|
||||||
}
|
|
||||||
|
|
||||||
export function useTenant() {
|
if (!pk) {
|
||||||
return createResource(async () => {
|
setPicture(undefined)
|
||||||
const pubkey = requireActivePubkey()
|
return
|
||||||
return getTenant(pubkey)
|
}
|
||||||
|
|
||||||
|
const profileSub = eventStore.profile(pk).subscribe((profile) => {
|
||||||
|
setPicture(getProfilePicture(profile))
|
||||||
|
})
|
||||||
|
|
||||||
|
const reqSub = primeProfiles([pk])
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
profileSub.unsubscribe()
|
||||||
|
reqSub.unsubscribe()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return picture
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTenantRelays() {
|
export function primeProfiles(pubkeys: string[]) {
|
||||||
return createResource(async () => {
|
const uniquePubkeys = Array.from(new Set(pubkeys.filter(Boolean)))
|
||||||
const pubkey = requireActivePubkey()
|
if (uniquePubkeys.length === 0) {
|
||||||
return listTenantRelays(pubkey)
|
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() {
|
type Identity = {
|
||||||
return createResource(async () => {
|
pubkey: string
|
||||||
const pubkey = requireActivePubkey()
|
is_admin: boolean
|
||||||
return listTenantInvoices(pubkey)
|
is_tenant: boolean
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRelay(relayId: () => string) {
|
export const useTenant = () => createResource(() => getTenant(account()!.pubkey))
|
||||||
return createResource(relayId, getRelay)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAdminTenants() {
|
export const useTenantRelays = () => createResource(() => listTenantRelays(account()!.pubkey))
|
||||||
return createResource(listTenants)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAdminRelays() {
|
export const useTenantInvoices = () => createResource(() => listTenantInvoices(account()!.pubkey))
|
||||||
return createResource(listRelays)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAdminTenantDetail(pubkey: () => string) {
|
export const useRelay = (relayId: () => string) => createResource(relayId, getRelay)
|
||||||
return createResource(pubkey, async (id) => {
|
|
||||||
const [tenant, relays] = await Promise.all([getTenant(id), listTenantRelays(id)])
|
|
||||||
return { tenant, relays }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createRelayForActiveTenant(input: CreateRelayInput) {
|
export const useAdminTenants = () => createResource(listTenants)
|
||||||
const pubkey = requireActivePubkey()
|
|
||||||
return createRelay({ ...input, tenant: pubkey })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateActiveTenantBilling(nwc_url: string) {
|
export const useAdminRelays = () => createResource(listRelays)
|
||||||
const pubkey = requireActivePubkey()
|
|
||||||
return updateTenantBilling(pubkey, { nwc_url })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateRelayById(id: string, input: UpdateRelayInput) {
|
export const useAdminTenant = (pubkey: () => string) => createResource(pubkey, getTenant)
|
||||||
return updateRelay(id, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateRelayPlanById(id: string, plan: string) {
|
export const useAdminTenantRelays = (pubkey: () => string) => createResource(pubkey, listTenantRelays)
|
||||||
return updateRelay(id, { plan })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deactivateRelayById(id: string) {
|
export const createRelayForActiveTenant = (input: CreateRelayInput) => createRelay({ ...input, tenant: account()!.pubkey })
|
||||||
return deactivateRelay(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRelayMemberCount(relayUrl: string): Promise<number | undefined> {
|
export const updateActiveTenantBilling = (nwc_url: string) => updateTenantBilling(account()!.pubkey, { nwc_url })
|
||||||
const signer = getActiveSigner()
|
|
||||||
if (!signer) throw new Error("Not logged in")
|
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 {
|
try {
|
||||||
const members = await management.listAllowedPubkeys()
|
return await management.listAllowedPubkeys()
|
||||||
return members.length
|
|
||||||
} catch {
|
} catch {
|
||||||
return undefined
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<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()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
import { createEffect, createMemo, createSignal, For, Show } from "solid-js"
|
import { createEffect, createMemo, createSignal, For, Show } from "solid-js"
|
||||||
import { useNavigate } from "@solidjs/router"
|
|
||||||
import PageContainer from "../components/PageContainer"
|
import PageContainer from "../components/PageContainer"
|
||||||
import LoadingState from "../components/LoadingState"
|
import LoadingState from "../components/LoadingState"
|
||||||
import useMinLoading from "../components/useMinLoading"
|
import useMinLoading from "../components/useMinLoading"
|
||||||
import { accounts, persistAccounts } from "../lib/nostr"
|
|
||||||
import { updateActiveTenantBilling, useTenant, useTenantInvoices } from "../lib/hooks"
|
import { updateActiveTenantBilling, useTenant, useTenantInvoices } from "../lib/hooks"
|
||||||
|
|
||||||
export default function Account() {
|
export default function Account() {
|
||||||
const navigate = useNavigate()
|
|
||||||
const [tenant, { refetch: refetchTenant }] = useTenant()
|
const [tenant, { refetch: refetchTenant }] = useTenant()
|
||||||
const [invoices] = useTenantInvoices()
|
const [invoices] = useTenantInvoices()
|
||||||
const [nwcUrl, setNwcUrl] = createSignal("")
|
const [nwcUrl, setNwcUrl] = createSignal("")
|
||||||
@@ -40,9 +37,8 @@ export default function Account() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
accounts.clearActive()
|
localStorage.clear()
|
||||||
persistAccounts()
|
window.location.href = "/"
|
||||||
navigate("/")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { A } from "@solidjs/router"
|
import { A } from "@solidjs/router"
|
||||||
import PricingTable from "../components/PricingTable"
|
import PricingTable from "../components/PricingTable"
|
||||||
import { useActiveAccount } from "../lib/nostr"
|
import { account } from "../lib/hooks"
|
||||||
|
|
||||||
function CheckIcon() {
|
function CheckIcon() {
|
||||||
return (
|
return (
|
||||||
@@ -21,8 +21,6 @@ function ExternalLinkIcon() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const account = useActiveAccount()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="min-h-screen bg-white text-gray-900 overflow-x-hidden">
|
<div class="min-h-screen bg-white text-gray-900 overflow-x-hidden">
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { ExtensionAccount, NostrConnectAccount, PasswordAccount, PrivateKeyAccou
|
|||||||
import { PasswordSigner } from "applesauce-signers"
|
import { PasswordSigner } from "applesauce-signers"
|
||||||
import QrScanner from "qr-scanner"
|
import QrScanner from "qr-scanner"
|
||||||
import QRCode from "qrcode"
|
import QRCode from "qrcode"
|
||||||
import { activateAccount } from "../lib/nostr"
|
import { accountManager, identity, PLATFORM_NAME } from "../lib/hooks"
|
||||||
import { PLATFORM_NAME } from "../lib/nostr"
|
|
||||||
|
|
||||||
const NIP46_RELAYS = ['wss://bucket.coracle.social', 'wss://ephemeral.snowflare.cc']
|
const NIP46_RELAYS = ['wss://bucket.coracle.social', 'wss://ephemeral.snowflare.cc']
|
||||||
|
|
||||||
@@ -58,8 +57,15 @@ export default function Login() {
|
|||||||
let abortController: AbortController | undefined
|
let abortController: AbortController | undefined
|
||||||
|
|
||||||
async function completeLogin(account: ExtensionAccount | NostrConnectAccount | PrivateKeyAccount | PasswordAccount) {
|
async function completeLogin(account: ExtensionAccount | NostrConnectAccount | PrivateKeyAccount | PasswordAccount) {
|
||||||
activateAccount(account)
|
accountManager.addAccount(account)
|
||||||
navigate("/relays")
|
accountManager.setActive(account)
|
||||||
|
|
||||||
|
const dispose = createEffect(() => {
|
||||||
|
if (identity()) {
|
||||||
|
navigate("/relays")
|
||||||
|
dispose()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loginWithNip07() {
|
async function loginWithNip07() {
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import BackLink from "../../components/BackLink"
|
|||||||
import PageContainer from "../../components/PageContainer"
|
import PageContainer from "../../components/PageContainer"
|
||||||
import ResourceState from "../../components/ResourceState"
|
import ResourceState from "../../components/ResourceState"
|
||||||
import useMinLoading from "../../components/useMinLoading"
|
import useMinLoading from "../../components/useMinLoading"
|
||||||
import { useAdminTenantDetail } from "../../lib/hooks"
|
import { useAdminTenant, useAdminTenantRelays } from "../../lib/hooks"
|
||||||
|
|
||||||
export default function AdminTenantDetail() {
|
export default function AdminTenantDetail() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const tenantId = () => params.id ?? ""
|
const tenantId = () => params.id ?? ""
|
||||||
const [detail] = useAdminTenantDetail(tenantId)
|
const [tenant] = useAdminTenant(tenantId)
|
||||||
const loading = useMinLoading(() => detail.loading)
|
const [relays] = useAdminTenantRelays(tenantId)
|
||||||
|
const loading = useMinLoading(() => tenant.loading || relays.loading)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
@@ -19,7 +20,7 @@ export default function AdminTenantDetail() {
|
|||||||
|
|
||||||
<ResourceState
|
<ResourceState
|
||||||
loading={loading()}
|
loading={loading()}
|
||||||
error={detail.error}
|
error={tenant.error || relays.error}
|
||||||
loadingText="Loading tenant..."
|
loadingText="Loading tenant..."
|
||||||
errorText="Failed to load tenant."
|
errorText="Failed to load tenant."
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
@@ -29,7 +30,7 @@ export default function AdminTenantDetail() {
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
<h2 class="text-lg font-semibold mb-4">Status</h2>
|
<h2 class="text-lg font-semibold mb-4">Status</h2>
|
||||||
<Show when={detail()}>
|
<Show when={tenant()}>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<p class="text-sm text-gray-700">
|
<p class="text-sm text-gray-700">
|
||||||
Current: <span class="font-medium uppercase tracking-wide">tenant</span>
|
Current: <span class="font-medium uppercase tracking-wide">tenant</span>
|
||||||
@@ -40,9 +41,9 @@ export default function AdminTenantDetail() {
|
|||||||
|
|
||||||
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
<h2 class="text-lg font-semibold mb-4">Relays</h2>
|
<h2 class="text-lg font-semibold mb-4">Relays</h2>
|
||||||
<Show when={(detail()?.relays.length ?? 0) > 0} fallback={<p class="text-gray-500">No relays.</p>}>
|
<Show when={(relays()?.length ?? 0) > 0} fallback={<p class="text-gray-500">No relays.</p>}>
|
||||||
<ul class="space-y-3">
|
<ul class="space-y-3">
|
||||||
<For each={detail()?.relays ?? []}>
|
<For each={relays() ?? []}>
|
||||||
{(relay) => (
|
{(relay) => (
|
||||||
<li>
|
<li>
|
||||||
<A href={`/admin/relays/${relay.id}`} class="block rounded-lg border border-gray-200 p-3 hover:border-gray-300">
|
<A href={`/admin/relays/${relay.id}`} class="block rounded-lg border border-gray-200 p-3 hover:border-gray-300">
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { A } from "@solidjs/router"
|
|||||||
import Fuse from "fuse.js"
|
import Fuse from "fuse.js"
|
||||||
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"
|
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"
|
||||||
import { getProfilePicture } from "applesauce-core/helpers/profile"
|
import { getProfilePicture } from "applesauce-core/helpers/profile"
|
||||||
import { eventStore, primeProfiles } from "../../lib/nostr"
|
|
||||||
import PageContainer from "../../components/PageContainer"
|
import PageContainer from "../../components/PageContainer"
|
||||||
import ResourceState from "../../components/ResourceState"
|
import ResourceState from "../../components/ResourceState"
|
||||||
import useMinLoading from "../../components/useMinLoading"
|
import useMinLoading from "../../components/useMinLoading"
|
||||||
import { useAdminTenants } from "../../lib/hooks"
|
import { eventStore, primeProfiles, useAdminTenants } from "../../lib/hooks"
|
||||||
|
|
||||||
function shortenPubkey(pubkey: string) {
|
function shortenPubkey(pubkey: string) {
|
||||||
if (pubkey.length <= 16) return pubkey
|
if (pubkey.length <= 16) return pubkey
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import PageContainer from "../../components/PageContainer"
|
|||||||
import RelayDetailCard from "../../components/RelayDetailCard"
|
import RelayDetailCard from "../../components/RelayDetailCard"
|
||||||
import ResourceState from "../../components/ResourceState"
|
import ResourceState from "../../components/ResourceState"
|
||||||
import useMinLoading from "../../components/useMinLoading"
|
import useMinLoading from "../../components/useMinLoading"
|
||||||
import { deactivateRelayById, getRelayMemberCount, updateRelayById, updateRelayPlanById, useRelay, type Relay } from "../../lib/hooks"
|
import { deactivateRelayById, getRelayMembers, updateRelayById, updateRelayPlanById, useRelay, type Relay } from "../../lib/hooks"
|
||||||
|
|
||||||
export default function RelayDetail() {
|
export default function RelayDetail() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
@@ -16,7 +16,7 @@ export default function RelayDetail() {
|
|||||||
const subdomain = relay()?.subdomain
|
const subdomain = relay()?.subdomain
|
||||||
return subdomain ? `wss://${subdomain}.spaces.coracle.social` : undefined
|
return subdomain ? `wss://${subdomain}.spaces.coracle.social` : undefined
|
||||||
})
|
})
|
||||||
const [memberCount] = createResource(relayUrl, async (url) => getRelayMemberCount(url))
|
const [members] = createResource(relayUrl, getRelayMembers)
|
||||||
const [busy, setBusy] = createSignal(false)
|
const [busy, setBusy] = createSignal(false)
|
||||||
const [error, setError] = createSignal("")
|
const [error, setError] = createSignal("")
|
||||||
const loading = useMinLoading(() => relay.loading && !relay())
|
const loading = useMinLoading(() => relay.loading && !relay())
|
||||||
@@ -174,7 +174,7 @@ export default function RelayDetail() {
|
|||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<RelayDetailCard
|
<RelayDetailCard
|
||||||
relay={r()}
|
relay={r()}
|
||||||
currentMembers={memberCount()}
|
currentMembers={members.length}
|
||||||
editHref={`/relays/${params.id}/edit`}
|
editHref={`/relays/${params.id}/edit`}
|
||||||
onDeactivate={handleDeactivate}
|
onDeactivate={handleDeactivate}
|
||||||
deactivating={busy()}
|
deactivating={busy()}
|
||||||
|
|||||||
Reference in New Issue
Block a user