Add url routing
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
The user logs in via NIP-07, NIP-46, or private key. See https://gitea.coracle.social/coracle/caravel/raw/branch/master/frontend/src/views/Login.tsx for an example login component.
|
The user logs in via NIP-07, NIP-46, or private key. See https://gitea.coracle.social/coracle/caravel/raw/branch/master/frontend/src/views/Login.tsx for an example login component.
|
||||||
|
|
||||||
The app has a sidebar with an "inbox", a list of existing quorums, and a "create quorum" button. The bottom shows the user's profile name/nip05 and a logout button. On mobile, an icon opens a quorum list in a drawer, and a plus button opens the quorum creation form.
|
The app has a sidebar with an "inbox", a list of existing quora, and a "create quorum" button. The bottom shows the user's profile name/nip05 and a logout button. On mobile, an icon opens a quorum list in a drawer, and a plus button opens the quorum creation form.
|
||||||
|
|
||||||
The inbox shows all events pending signature across all quorums, including events the user has not yet signed. Each event shows x/y signed. Events not yet signed by the user have options for signing and declining, each with an optional message posted to the NIP-17 quorum chat. The inbox also lists quorum invitations with options to join or decline, each with a message.
|
The inbox shows all events pending signature across all quora, including events the user has not yet signed. Each event shows x/y signed. Events not yet signed by the user have options for signing and declining, each with an optional message posted to the NIP-17 quorum chat. The inbox also lists quorum invitations with options to join or decline, each with a message.
|
||||||
|
|
||||||
Create quorum opens a modal for selecting quorum members and threshold, the quorum's outbox relays, and the quorum's kind 0 profile information.
|
Create quorum opens a modal for selecting quorum members and threshold, the quorum's outbox relays, and the quorum's kind 0 profile information.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ FROST Multisig Quorum Protocol
|
|||||||
|
|
||||||
## Abstract
|
## Abstract
|
||||||
|
|
||||||
This NIP defines a protocol for creating and operating FROST threshold signature quorums over Nostr keys. A quorum is a group of n participants who collectively control a shared Nostr keypair via a (t,n) threshold signing scheme, where any t members can produce a valid signature but no fewer. The quorum's private key is never known to any single party.
|
This NIP defines a protocol for creating and operating FROST threshold signature quora over Nostr keys. A quorum is a group of n participants who collectively control a shared Nostr keypair via a (t,n) threshold signing scheme, where any t members can produce a valid signature but no fewer. The quorum's private key is never known to any single party.
|
||||||
|
|
||||||
## Participant Indexing
|
## Participant Indexing
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# nq
|
# nq
|
||||||
|
|
||||||
This is a web application which allows for creating, rotating, and signing under multisig quorums for nostr keys.
|
This is a web application which allows for creating, rotating, and signing under multisig quora for nostr keys.
|
||||||
|
|
||||||
# Protocol
|
# Protocol
|
||||||
|
|
||||||
|
|||||||
+6
-35
@@ -7,6 +7,7 @@ import AccountPage from "./components/AccountPage"
|
|||||||
import { ProposeQuorum } from "./components/forms/DkgForms"
|
import { ProposeQuorum } from "./components/forms/DkgForms"
|
||||||
import { ProposeResharing } from "./components/forms/ResharingForms"
|
import { ProposeResharing } from "./components/forms/ResharingForms"
|
||||||
import { ProposeSign } from "./components/forms/SigningForms"
|
import { ProposeSign } from "./components/forms/SigningForms"
|
||||||
|
import QuoraLanding from "./components/QuoraLanding"
|
||||||
import Avatar from "./components/Avatar"
|
import Avatar from "./components/Avatar"
|
||||||
import Modal from "./components/Modal"
|
import Modal from "./components/Modal"
|
||||||
import { displayProfile } from "@welshman/util"
|
import { displayProfile } from "@welshman/util"
|
||||||
@@ -62,7 +63,7 @@ export default function Layout() {
|
|||||||
{/* Desktop sidebar */}
|
{/* Desktop sidebar */}
|
||||||
<aside class="hidden md:flex flex-col w-64 shrink-0 border-r border-gray-200 dark:border-neutral-700 bg-white dark:bg-neutral-800">
|
<aside class="hidden md:flex flex-col w-64 shrink-0 border-r border-gray-200 dark:border-neutral-700 bg-white dark:bg-neutral-800">
|
||||||
<div class="px-4 py-4 border-b border-gray-200 dark:border-neutral-700">
|
<div class="px-4 py-4 border-b border-gray-200 dark:border-neutral-700">
|
||||||
<span class="text-base font-bold text-gray-900 dark:text-white">NQ</span>
|
<button class="text-base font-bold text-gray-900 dark:text-white hover:opacity-70 transition-opacity" onClick={() => setView("home")}>NQ</button>
|
||||||
</div>
|
</div>
|
||||||
<SidebarContent onNew={() => setShowProposeQuorum(true)} onNavigate={() => setDrawerOpen(false)} />
|
<SidebarContent onNew={() => setShowProposeQuorum(true)} onNavigate={() => setDrawerOpen(false)} />
|
||||||
</aside>
|
</aside>
|
||||||
@@ -78,7 +79,7 @@ export default function Layout() {
|
|||||||
{/* Mobile drawer — always mounted, slides in/out via transform */}
|
{/* Mobile drawer — always mounted, slides in/out via transform */}
|
||||||
<aside class={`fixed inset-y-0 left-0 z-50 w-64 flex flex-col bg-white dark:bg-neutral-800 shadow-xl md:hidden transition-transform duration-300 ease-in-out ${drawerOpen() ? "translate-x-0" : "-translate-x-full"}`}>
|
<aside class={`fixed inset-y-0 left-0 z-50 w-64 flex flex-col bg-white dark:bg-neutral-800 shadow-xl md:hidden transition-transform duration-300 ease-in-out ${drawerOpen() ? "translate-x-0" : "-translate-x-full"}`}>
|
||||||
<div class="px-4 py-4 border-b border-gray-200 dark:border-neutral-700 flex items-center justify-between">
|
<div class="px-4 py-4 border-b border-gray-200 dark:border-neutral-700 flex items-center justify-between">
|
||||||
<span class="text-base font-bold text-gray-900 dark:text-white">NQ</span>
|
<button class="text-base font-bold text-gray-900 dark:text-white hover:opacity-70 transition-opacity" onClick={() => { setView("home"); setDrawerOpen(false) }}>NQ</button>
|
||||||
<button
|
<button
|
||||||
class="p-1 rounded-md text-gray-400 hover:text-gray-600"
|
class="p-1 rounded-md text-gray-400 hover:text-gray-600"
|
||||||
onClick={() => setDrawerOpen(false)}
|
onClick={() => setDrawerOpen(false)}
|
||||||
@@ -96,22 +97,7 @@ export default function Layout() {
|
|||||||
{/* Col 2: main content */}
|
{/* Col 2: main content */}
|
||||||
<div class="flex-1 min-w-0 overflow-y-auto">
|
<div class="flex-1 min-w-0 overflow-y-auto">
|
||||||
<Show when={view() === "home"}>
|
<Show when={view() === "home"}>
|
||||||
<div class="flex items-center justify-center min-h-full p-6">
|
<QuoraLanding onNew={() => setShowProposeQuorum(true)} />
|
||||||
<div class="flex flex-col items-center text-center gap-4 max-w-sm">
|
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Welcome to NQ</h1>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-neutral-400">
|
|
||||||
FROST threshold multisig for Nostr keys. Create a quorum to share control of a
|
|
||||||
key with a group, where any threshold of members can sign.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="px-5 py-2.5 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors"
|
|
||||||
onClick={() => setShowProposeQuorum(true)}
|
|
||||||
>
|
|
||||||
Create your first quorum
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={view() === "account"}>
|
<Show when={view() === "account"}>
|
||||||
<AccountPage />
|
<AccountPage />
|
||||||
@@ -148,27 +134,12 @@ export default function Layout() {
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="text-base font-semibold text-gray-900 dark:text-white">NQ</span>
|
<button class="text-base font-semibold text-gray-900 dark:text-white hover:opacity-70 transition-opacity" onClick={() => setView("home")}>NQ</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="flex-1 overflow-y-auto">
|
<main class="flex-1 overflow-y-auto">
|
||||||
<Show when={view() === "home"}>
|
<Show when={view() === "home"}>
|
||||||
<div class="flex items-center justify-center min-h-full p-6">
|
<QuoraLanding onNew={() => setShowProposeQuorum(true)} />
|
||||||
<div class="flex flex-col items-center text-center gap-4 max-w-sm">
|
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Welcome to NQ</h1>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-neutral-400">
|
|
||||||
FROST threshold multisig for Nostr keys. Create a quorum to share control of a
|
|
||||||
key with a group, where any threshold of members can sign.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="px-5 py-2.5 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors"
|
|
||||||
onClick={() => setShowProposeQuorum(true)}
|
|
||||||
>
|
|
||||||
Create your first quorum
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={view() === "account"}>
|
<Show when={view() === "account"}>
|
||||||
<AccountPage />
|
<AccountPage />
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { createSignal, Show } from "solid-js"
|
||||||
|
import toast from "solid-toast"
|
||||||
|
import { fullNpub, shortNpub } from "../lib/pubkey"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a pubkey as a truncated npub with a copy button. Clicking copies the FULL npub to
|
||||||
|
* the clipboard and shows a success toast. Use this anywhere a raw pubkey would otherwise be
|
||||||
|
* shown as hex; where a profile name is the primary label, show the name and use this for the
|
||||||
|
* secondary key line.
|
||||||
|
*/
|
||||||
|
export default function Npub(props: { pubkey: string; class?: string }) {
|
||||||
|
const [copied, setCopied] = createSignal(false)
|
||||||
|
|
||||||
|
async function copy(e: MouseEvent) {
|
||||||
|
// Stop the click from triggering an enclosing button/link (cards, nav, modal backdrops).
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(fullNpub(props.pubkey))
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 1500)
|
||||||
|
toast.success("Copied npub to clipboard")
|
||||||
|
} catch {
|
||||||
|
toast.error("Failed to copy to clipboard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span class={`inline-flex items-center gap-1 min-w-0 ${props.class ?? ""}`}>
|
||||||
|
<span class="font-mono truncate">{shortNpub(props.pubkey)}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Copy npub"
|
||||||
|
class="p-0.5 rounded text-gray-400 dark:text-neutral-500 hover:text-gray-600 dark:hover:text-neutral-300 transition-colors shrink-0"
|
||||||
|
onClick={copy}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={copied()}
|
||||||
|
fallback={
|
||||||
|
<svg class="size-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" />
|
||||||
|
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<svg class="size-3.5 text-green-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { For, Show } from "solid-js"
|
||||||
|
import { displayedQuora, openQuorum } from "../store"
|
||||||
|
import { useProfileDisplay } from "../hooks"
|
||||||
|
import { quorumHasActivity } from "../engine/notifications"
|
||||||
|
import Npub from "./Npub"
|
||||||
|
import type { DisplayedQuorum } from "../models"
|
||||||
|
|
||||||
|
const badgeClass: Record<DisplayedQuorum["status"], string> = {
|
||||||
|
complete: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300",
|
||||||
|
sending: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300",
|
||||||
|
failed: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300",
|
||||||
|
pending: "bg-gray-100 text-gray-600 dark:bg-neutral-700 dark:text-neutral-300",
|
||||||
|
}
|
||||||
|
|
||||||
|
function QuorumCard(props: { quorum: DisplayedQuorum }) {
|
||||||
|
const name = useProfileDisplay(() => props.quorum.quorumPubkey ?? "")
|
||||||
|
const label = () => (props.quorum.quorumPubkey ? name() : "New quorum")
|
||||||
|
// A div (not a button) so the Npub copy button can live inside it without nesting buttons.
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex={0}
|
||||||
|
class="flex flex-col gap-2 p-4 rounded-xl border border-gray-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 hover:border-blue-400 dark:hover:border-blue-500 hover:shadow-sm cursor-pointer transition-colors"
|
||||||
|
onClick={() => openQuorum(props.quorum.id)}
|
||||||
|
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openQuorum(props.quorum.id) } }}
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
|
<span class="font-semibold text-gray-900 dark:text-white truncate flex items-center gap-1.5 min-w-0">
|
||||||
|
<span class="truncate">{label()}</span>
|
||||||
|
<Show when={quorumHasActivity(props.quorum)}>
|
||||||
|
<span class="size-2 rounded-full bg-blue-500 shrink-0" title="New activity" />
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
<span class={`px-2 py-0.5 rounded-full text-[11px] font-medium shrink-0 ${badgeClass[props.quorum.status]}`}>
|
||||||
|
{props.quorum.statusLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
when={props.quorum.quorumPubkey}
|
||||||
|
fallback={<span class="text-xs italic text-gray-400 dark:text-neutral-500">Key not generated yet</span>}
|
||||||
|
>
|
||||||
|
{pk => <Npub pubkey={pk()} class="text-xs text-gray-400 dark:text-neutral-500 max-w-full" />}
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 dark:text-neutral-400">
|
||||||
|
{props.quorum.threshold}-of-{props.quorum.members.length} signatures required
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QuoraLanding(props: { onNew: () => void }) {
|
||||||
|
return (
|
||||||
|
<Show
|
||||||
|
when={displayedQuora().length > 0}
|
||||||
|
fallback={
|
||||||
|
<div class="flex items-center justify-center min-h-full p-6">
|
||||||
|
<div class="flex flex-col items-center text-center gap-4 max-w-sm">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Welcome to NQ</h1>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-neutral-400">
|
||||||
|
FROST threshold multisig for Nostr keys. Create a quorum to share control of a
|
||||||
|
key with a group, where any threshold of members can sign.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-5 py-2.5 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors"
|
||||||
|
onClick={props.onNew}
|
||||||
|
>
|
||||||
|
Create your first quorum
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="p-6 max-w-5xl mx-auto">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h1 class="text-xl font-bold text-gray-900 dark:text-white">Your Quora</h1>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors"
|
||||||
|
onClick={props.onNew}
|
||||||
|
>
|
||||||
|
New quorum
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<For each={displayedQuora()}>
|
||||||
|
{(q) => <QuorumCard quorum={q} />}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Pubkey, displayPubkey } from "@welshman/util"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full `npub1…` bech32 encoding of a hex pubkey — the value we put on the clipboard. Falls
|
||||||
|
* back to the raw input if it isn't a valid pubkey (so rendering can never throw).
|
||||||
|
*/
|
||||||
|
export function fullNpub(hex: string): string {
|
||||||
|
try {
|
||||||
|
return new Pubkey(hex).toNpub()
|
||||||
|
} catch {
|
||||||
|
return hex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncated npub for inline display (e.g. `npub1ab…wxyz`) — the same form welshman uses as
|
||||||
|
* its profile-display fallback, so npubs read consistently across named and unnamed pubkeys.
|
||||||
|
*/
|
||||||
|
export function shortNpub(hex: string): string {
|
||||||
|
try {
|
||||||
|
return displayPubkey(hex)
|
||||||
|
} catch {
|
||||||
|
return hex
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
-14
@@ -28,7 +28,10 @@ import type {
|
|||||||
SigningSession,
|
SigningSession,
|
||||||
} from "./models"
|
} from "./models"
|
||||||
|
|
||||||
export type View = "home" | "account" | { type: "quorum"; id: string; tab: "log" | "members" | "chat" }
|
export type QuorumTab = "log" | "members" | "chat"
|
||||||
|
export type View = "home" | "account" | { type: "quorum"; id: string; tab: QuorumTab }
|
||||||
|
|
||||||
|
const QUORUM_TABS: readonly QuorumTab[] = ["log", "members", "chat"]
|
||||||
|
|
||||||
export function logout(): void {
|
export function logout(): void {
|
||||||
// dropSession removes the session and its cached signer entirely; the sync()
|
// dropSession removes the session and its cached signer entirely; the sync()
|
||||||
@@ -38,14 +41,53 @@ export function logout(): void {
|
|||||||
if (pk) { dropSession(pk) }
|
if (pk) { dropSession(pk) }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const [view, setView] = createSignal<View>("home")
|
// ── URL-backed view ────────────────────────────────────────────────────────────
|
||||||
|
// The active view IS the URL path, so links are shareable and the browser back/forward
|
||||||
|
// buttons work. Routes: / → home (Your Quora landing) · /account · /quorum/<id>/<tab>.
|
||||||
|
// `setView` (and the openQuorum/setTab helpers built on it) pushes history; popstate syncs
|
||||||
|
// the signal back. Kept as the same `view()/setView()` API so engine + components are unchanged.
|
||||||
|
|
||||||
|
function pathToView(path: string): View {
|
||||||
|
const parts = path.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean)
|
||||||
|
if (parts[0] === "account") { return "account" }
|
||||||
|
if (parts[0] === "quorum" && parts[1]) {
|
||||||
|
const tab = QUORUM_TABS.includes(parts[2] as QuorumTab) ? (parts[2] as QuorumTab) : "log"
|
||||||
|
return { type: "quorum", id: decodeURIComponent(parts[1]), tab }
|
||||||
|
}
|
||||||
|
return "home"
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewToPath(v: View): string {
|
||||||
|
if (v === "account") { return "/account" }
|
||||||
|
if (typeof v === "object" && v.type === "quorum") { return `/quorum/${encodeURIComponent(v.id)}/${v.tab}` }
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
const [view, setViewSignal] = createSignal<View>(pathToView(window.location.pathname))
|
||||||
|
export { view }
|
||||||
|
|
||||||
|
const onPopState = () => setViewSignal(pathToView(window.location.pathname))
|
||||||
|
window.addEventListener("popstate", onPopState)
|
||||||
|
|
||||||
|
/** Navigate to a view, syncing the URL. Accepts a value or an updater, like a signal setter. */
|
||||||
|
export function setView(next: View | ((prev: View) => View), opts?: { replace?: boolean }): void {
|
||||||
|
const v = typeof next === "function" ? next(view()) : next
|
||||||
|
const path = viewToPath(v)
|
||||||
|
if (path !== window.location.pathname) {
|
||||||
|
if (opts?.replace) { window.history.replaceState(null, "", path) }
|
||||||
|
else { window.history.pushState(null, "", path) }
|
||||||
|
}
|
||||||
|
setViewSignal(v)
|
||||||
|
}
|
||||||
|
|
||||||
export function openQuorum(id: string): void {
|
export function openQuorum(id: string): void {
|
||||||
setView({ type: "quorum", id, tab: "log" })
|
setView({ type: "quorum", id, tab: "log" })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTab(tab: "log" | "members" | "chat"): void {
|
// Switching tabs replaces (rather than pushes) history, so Back leaves the quorum instead of
|
||||||
setView(v => (typeof v === "object" && v.type === "quorum") ? { ...v, tab } : v)
|
// cycling through every tab the user clicked.
|
||||||
|
export function setTab(tab: QuorumTab): void {
|
||||||
|
setView(v => (typeof v === "object" && v.type === "quorum") ? { ...v, tab } : v, { replace: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completed quorum records (carry the secret share) — populated when a DKG finishes.
|
// Completed quorum records (carry the secret share) — populated when a DKG finishes.
|
||||||
@@ -73,6 +115,9 @@ export function setDelivery(inviteId: string, status: "sending" | "saved" | "fai
|
|||||||
// they don't stack a duplicate on the surviving repository/session singletons each edit.
|
// they don't stack a duplicate on the surviving repository/session singletons each edit.
|
||||||
const track = trackForHmr(import.meta.hot)
|
const track = trackForHmr(import.meta.hot)
|
||||||
|
|
||||||
|
// Drop the popstate listener (registered at module top) on hot reload.
|
||||||
|
track(() => window.removeEventListener("popstate", onPopState))
|
||||||
|
|
||||||
const [me, setMe] = createSignal("")
|
const [me, setMe] = createSignal("")
|
||||||
const [inviteEvents, setInviteEvents] = createSignal<TrustedEvent[]>([])
|
const [inviteEvents, setInviteEvents] = createSignal<TrustedEvent[]>([])
|
||||||
const [round1Events, setRound1Events] = createSignal<TrustedEvent[]>([])
|
const [round1Events, setRound1Events] = createSignal<TrustedEvent[]>([])
|
||||||
@@ -282,16 +327,6 @@ export function membersAt(windows: MembershipWindow[], at: number): Set<string>
|
|||||||
return active
|
return active
|
||||||
}
|
}
|
||||||
|
|
||||||
// Land on the first quorum instead of the empty home view — e.g. once quora hydrate on
|
|
||||||
// load. Only fires while on "home", so it never overrides explicit navigation to a quorum
|
|
||||||
// or the account page. When there are no quora, "home" stays (and shows the hero/CTA).
|
|
||||||
trackEffect(track, () => {
|
|
||||||
const list = displayedQuora()
|
|
||||||
if (view() === "home" && list.length > 0) {
|
|
||||||
openQuorum(list[0].id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// ── Inbox subscription ────────────────────────────────────────────────────────
|
// ── Inbox subscription ────────────────────────────────────────────────────────
|
||||||
// One active subscription, restarted when the active pubkey or the user's
|
// One active subscription, restarted when the active pubkey or the user's
|
||||||
// kind-10050 relay list changes. Subscribing to userMessagingRelayList makes it
|
// kind-10050 relay list changes. Subscribing to userMessagingRelayList makes it
|
||||||
|
|||||||
Reference in New Issue
Block a user