Show an access gate when the relay refuses reads
This commit is contained in:
parent
6ecd618bce
commit
9cc82dc1fe
5 changed files with 437 additions and 145 deletions
|
|
@ -12,9 +12,10 @@
|
|||
import NewDiscussionModal from "$lib/components/NewDiscussionModal.svelte";
|
||||
import DeleteModal from "$lib/components/DeleteModal.svelte";
|
||||
import Toast from "$lib/components/Toast.svelte";
|
||||
import RelayGate from "$lib/components/RelayGate.svelte";
|
||||
import { page } from "$app/state";
|
||||
import { afterNavigate } from "$app/navigation";
|
||||
import { onMount } from "svelte";
|
||||
import { onMount, untrack } from "svelte";
|
||||
import { auth, restoreSession } from "$lib/auth.svelte";
|
||||
import { loadGroup } from "$lib/group.svelte";
|
||||
import { ensureMembershipChecked } from "$lib/join.svelte";
|
||||
|
|
@ -25,6 +26,7 @@
|
|||
import { seedProfiles } from "$lib/profiles.svelte";
|
||||
import { startChat } from "$lib/chat.svelte";
|
||||
import { resetForumConnection } from "$lib/relay";
|
||||
import { relayAccess, probeRelayAccess } from "$lib/access.svelte";
|
||||
import { activeGroup, setActiveGroup } from "$lib/active.svelte";
|
||||
import {
|
||||
MODE,
|
||||
|
|
@ -68,21 +70,36 @@
|
|||
if (nav.type !== "popstate") mainEl?.scrollTo(0, 0);
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
// Nothing is fetched until the relay confirms it will serve this visitor:
|
||||
// the probe runs once the stored signer is in place (restoreSession sets it
|
||||
// synchronously, before its profile fetch), and every probe that ends open
|
||||
// (re)loads the forum below.
|
||||
onMount(() => {
|
||||
restoreSession();
|
||||
probeRelayAccess();
|
||||
});
|
||||
|
||||
async function loadForum() {
|
||||
loadResources();
|
||||
loadPartials();
|
||||
const tasks = [restoreSession(), loadGroup()];
|
||||
const tasks = [loadGroup()];
|
||||
if (mode === "full") tasks.push(loadGroups());
|
||||
await Promise.all(tasks);
|
||||
// Resources are filtered by the admin set; in full mode that means every
|
||||
// room's admins, needed on every page for the sidebar.
|
||||
if (mode === "full") loadRoomAdmins(groupsStore.list.map((g) => g.id));
|
||||
seedProfiles(auth.user?.pubkey ?? null);
|
||||
}
|
||||
|
||||
// Each completed probe that finds the relay open reloads the room views: on
|
||||
// first load, after a login (private/hidden rooms appear) or logout (they
|
||||
// vanish), and after joining a members-only relay.
|
||||
$effect(() => {
|
||||
relayAccess.probes;
|
||||
if (relayAccess.state === "open") untrack(loadForum);
|
||||
});
|
||||
|
||||
// Reload the room views when the user logs in or out: a logged-in member sees
|
||||
// their private/hidden rooms, so the listing must be re-fetched over the now
|
||||
// (de)authenticated connection. sessionEpoch only bumps on explicit
|
||||
// Re-probe when the user logs in or out. sessionEpoch only bumps on explicit
|
||||
// login/logout, never on the silent restore that onMount already covers.
|
||||
let lastEpoch = -1;
|
||||
$effect(() => {
|
||||
|
|
@ -91,17 +108,12 @@
|
|||
const first = lastEpoch === -1;
|
||||
const loggedOut = auth.user === null;
|
||||
lastEpoch = epoch;
|
||||
if (first) return; // initial mount: onMount already loaded everything
|
||||
if (first) return;
|
||||
// On logout, drop the authenticated connection so the relay stops serving
|
||||
// the previous user's private rooms; login reuses the open connection,
|
||||
// which ensureForumReady authenticates via its stored challenge.
|
||||
if (loggedOut) resetForumConnection();
|
||||
loadGroup();
|
||||
if (mode === "full") {
|
||||
loadGroups().then(() =>
|
||||
loadRoomAdmins(groupsStore.list.map((g) => g.id)),
|
||||
);
|
||||
}
|
||||
probeRelayAccess();
|
||||
});
|
||||
|
||||
// Full mode: the room route defines the active group. Thread pages set it
|
||||
|
|
@ -167,118 +179,122 @@
|
|||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="mx-auto flex max-w-[1540px] flex-col md:h-screen md:overflow-hidden {mobileView ===
|
||||
'chat'
|
||||
? 'h-dvh overflow-hidden'
|
||||
: ''}"
|
||||
>
|
||||
<Navbar onMenuToggle={() => (menuOpen = true)} />
|
||||
{#if relayAccess.state !== "open"}
|
||||
<RelayGate />
|
||||
{:else}
|
||||
<div
|
||||
class="relative md:flex md:flex-1 md:gap-5 md:overflow-hidden {mobileView ===
|
||||
class="mx-auto flex max-w-[1540px] flex-col md:h-screen md:overflow-hidden {mobileView ===
|
||||
'chat'
|
||||
? 'flex flex-1 overflow-hidden'
|
||||
? 'h-dvh overflow-hidden'
|
||||
: ''}"
|
||||
>
|
||||
<LeftSidebar {mode} {activeRoom} {activeResource} {contactsActive} />
|
||||
<main
|
||||
bind:this={mainEl}
|
||||
class="no-scrollbar md:min-h-0 md:overflow-y-auto {showDiscussions
|
||||
? 'md:flex-[3]'
|
||||
: 'md:flex-1'}
|
||||
{mobileView === 'chat' ? 'hidden md:block' : 'block'}"
|
||||
<Navbar onMenuToggle={() => (menuOpen = true)} />
|
||||
<div
|
||||
class="relative md:flex md:flex-1 md:gap-5 md:overflow-hidden {mobileView ===
|
||||
'chat'
|
||||
? 'flex flex-1 overflow-hidden'
|
||||
: ''}"
|
||||
>
|
||||
<!-- Gray top margin lives inside the scroll area, so scrolling collapses
|
||||
<LeftSidebar {mode} {activeRoom} {activeResource} {contactsActive} />
|
||||
<main
|
||||
bind:this={mainEl}
|
||||
class="no-scrollbar md:min-h-0 md:overflow-y-auto {showDiscussions
|
||||
? 'md:flex-[3]'
|
||||
: 'md:flex-1'}
|
||||
{mobileView === 'chat' ? 'hidden md:block' : 'block'}"
|
||||
>
|
||||
<!-- Gray top margin lives inside the scroll area, so scrolling collapses
|
||||
it first and it reappears once the top is reached. Desktop only. -->
|
||||
<div class="hidden md:block md:h-6" aria-hidden="true"></div>
|
||||
<div
|
||||
class="min-h-[calc(100dvh_-_4rem)] bg-white px-6 pt-4 pb-20 shadow-lg md:min-h-full md:rounded-t-xl md:px-10 md:pt-6 md:pt-8 dark:bg-neutral-900"
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
{#if showChat}
|
||||
<div class="hidden w-80 shrink-0 md:block" aria-hidden="true"></div>
|
||||
<ChatSidebar
|
||||
expanded={chatExpanded}
|
||||
onToggle={() => (chatExpanded = !chatExpanded)}
|
||||
mobileActive={mobileView === "chat"}
|
||||
/>
|
||||
{:else if showDiscussions}
|
||||
<!-- Own panel (40%) so the gray gutter matches the main↔chat gap.
|
||||
Hidden on mobile — it's supplementary to the main column. -->
|
||||
<div
|
||||
class="no-scrollbar hidden min-w-0 md:block md:min-h-0 md:flex-[2] md:overflow-y-auto"
|
||||
>
|
||||
<div class="hidden md:block md:h-6" aria-hidden="true"></div>
|
||||
<div
|
||||
class="bg-white px-6 pt-8 pb-20 shadow-lg md:min-h-full md:rounded-t-xl md:px-8 md:pt-6 dark:bg-neutral-900"
|
||||
class="min-h-[calc(100dvh_-_4rem)] bg-white px-6 pt-4 pb-20 shadow-lg md:min-h-full md:rounded-t-xl md:px-10 md:pt-6 md:pt-8 dark:bg-neutral-900"
|
||||
>
|
||||
<LatestDiscussions />
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{#if showChat}
|
||||
<div class="hidden w-80 shrink-0 md:block" aria-hidden="true"></div>
|
||||
<ChatSidebar
|
||||
expanded={chatExpanded}
|
||||
onToggle={() => (chatExpanded = !chatExpanded)}
|
||||
mobileActive={mobileView === "chat"}
|
||||
/>
|
||||
{:else if showDiscussions}
|
||||
<!-- Own panel (40%) so the gray gutter matches the main↔chat gap.
|
||||
Hidden on mobile — it's supplementary to the main column. -->
|
||||
<div
|
||||
class="no-scrollbar hidden min-w-0 md:block md:min-h-0 md:flex-[2] md:overflow-y-auto"
|
||||
>
|
||||
<div class="hidden md:block md:h-6" aria-hidden="true"></div>
|
||||
<div
|
||||
class="bg-white px-6 pt-8 pb-20 shadow-lg md:min-h-full md:rounded-t-xl md:px-8 md:pt-6 dark:bg-neutral-900"
|
||||
>
|
||||
<LatestDiscussions />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showChat}
|
||||
<nav
|
||||
class="fixed inset-x-0 bottom-0 z-30 flex border-t border-neutral-200 bg-neutral-100 md:hidden dark:border-neutral-700 dark:bg-neutral-800"
|
||||
aria-label="Switch view"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (mobileView = "forum")}
|
||||
aria-pressed={mobileView === "forum"}
|
||||
class="flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium
|
||||
{mobileView === 'forum'
|
||||
? 'text-accent'
|
||||
: 'text-neutral-500 dark:text-neutral-400'}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||
/>
|
||||
</svg>
|
||||
Forum
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (mobileView = "chat")}
|
||||
aria-pressed={mobileView === "chat"}
|
||||
class="flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium
|
||||
{mobileView === 'chat'
|
||||
? 'text-accent'
|
||||
: 'text-neutral-500 dark:text-neutral-400'}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8.625 9.75a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z"
|
||||
/>
|
||||
</svg>
|
||||
Chat
|
||||
</button>
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showChat}
|
||||
<nav
|
||||
class="fixed inset-x-0 bottom-0 z-30 flex border-t border-neutral-200 bg-neutral-100 md:hidden dark:border-neutral-700 dark:bg-neutral-800"
|
||||
aria-label="Switch view"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (mobileView = "forum")}
|
||||
aria-pressed={mobileView === "forum"}
|
||||
class="flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium
|
||||
{mobileView === 'forum'
|
||||
? 'text-accent'
|
||||
: 'text-neutral-500 dark:text-neutral-400'}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||
/>
|
||||
</svg>
|
||||
Forum
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (mobileView = "chat")}
|
||||
aria-pressed={mobileView === "chat"}
|
||||
class="flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium
|
||||
{mobileView === 'chat'
|
||||
? 'text-accent'
|
||||
: 'text-neutral-500 dark:text-neutral-400'}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8.625 9.75a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z"
|
||||
/>
|
||||
</svg>
|
||||
Chat
|
||||
</button>
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<MobileMenu
|
||||
open={menuOpen}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue