Move join before composing, and cache membership state
This commit is contained in:
parent
fadac80033
commit
30f52af4b1
10 changed files with 503 additions and 149 deletions
|
|
@ -16,6 +16,7 @@
|
|||
import { onMount } from "svelte";
|
||||
import { auth, restoreSession } from "$lib/auth.svelte";
|
||||
import { loadGroup } from "$lib/group.svelte";
|
||||
import { ensureMembershipChecked } from "$lib/join.svelte";
|
||||
import { loadGroups, groupsStore } from "$lib/groups.svelte";
|
||||
import { loadResources } from "$lib/resources.svelte";
|
||||
import { loadPartials } from "$lib/partials.svelte";
|
||||
|
|
@ -108,6 +109,14 @@
|
|||
if (chatEnabled && activeGroup.id) startChat(activeGroup.id);
|
||||
});
|
||||
|
||||
// Pre-warm membership for the active room so the join gates resolve before
|
||||
// the user reaches a composer (no flash, no wasted typing). Depends on
|
||||
// auth.user so a silent session restore re-runs the check.
|
||||
$effect(() => {
|
||||
auth.user;
|
||||
if (activeGroup.id) ensureMembershipChecked(activeGroup.id);
|
||||
});
|
||||
|
||||
let chatExpanded = $state(false);
|
||||
let menuOpen = $state(false);
|
||||
let mobileView = $state<"forum" | "chat">("forum");
|
||||
|
|
@ -177,7 +186,7 @@
|
|||
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 dark:bg-neutral-900 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"
|
||||
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>
|
||||
|
|
@ -197,7 +206,7 @@
|
|||
>
|
||||
<div class="hidden md:block md:h-6" aria-hidden="true"></div>
|
||||
<div
|
||||
class="bg-white dark:bg-neutral-900 px-6 pt-8 pb-20 shadow-lg md:min-h-full md:rounded-t-xl md:px-8 md:pt-6"
|
||||
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>
|
||||
|
|
@ -206,7 +215,7 @@
|
|||
</div>
|
||||
{#if showChat}
|
||||
<nav
|
||||
class="fixed inset-x-0 bottom-0 z-30 flex border-t border-neutral-200 dark:border-neutral-700 bg-neutral-100 dark:bg-neutral-800 md:hidden"
|
||||
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
|
||||
|
|
@ -214,7 +223,9 @@
|
|||
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'}"
|
||||
{mobileView === 'forum'
|
||||
? 'text-accent'
|
||||
: 'text-neutral-500 dark:text-neutral-400'}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -238,7 +249,9 @@
|
|||
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'}"
|
||||
{mobileView === 'chat'
|
||||
? 'text-accent'
|
||||
: 'text-neutral-500 dark:text-neutral-400'}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@
|
|||
import { isGroupAdmin } from "$lib/admins.svelte";
|
||||
import { requestDelete } from "$lib/moderation.svelte";
|
||||
import { showToast } from "$lib/toast.svelte";
|
||||
import { withJoin } from "$lib/join.svelte";
|
||||
import {
|
||||
withJoin,
|
||||
membershipOf,
|
||||
ensureMembershipChecked,
|
||||
openJoinModal,
|
||||
} from "$lib/join.svelte";
|
||||
import { setActiveGroup } from "$lib/active.svelte";
|
||||
import { RELAY_URL, MODE } from "$lib/config";
|
||||
import ThreadScrubber from "$lib/components/ThreadScrubber.svelte";
|
||||
|
|
@ -111,6 +116,28 @@
|
|||
const detail = $derived(threadDetailStore.detail);
|
||||
const profiles = $derived(threadDetailStore.profiles);
|
||||
|
||||
// Replying needs membership regardless of flags. Only gate a confirmed guest;
|
||||
// while membership resolves the editor stays and submit awaits the check, so a
|
||||
// member never flashes "Join to reply".
|
||||
const joinToReply = $derived(
|
||||
!!detail && !!auth.user && membershipOf(detail.groupId) === "guest",
|
||||
);
|
||||
|
||||
// The relay requires membership to reply, so resolve it up front. Depends on
|
||||
// auth.user so a silent session restore re-runs the check.
|
||||
$effect(() => {
|
||||
auth.user;
|
||||
if (detail?.groupId) ensureMembershipChecked(detail.groupId);
|
||||
});
|
||||
|
||||
function onJoinToReply() {
|
||||
if (!detail) return;
|
||||
openJoinModal(detail.groupId, async () => {
|
||||
await tick();
|
||||
editorEl?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
const canModerate = $derived(
|
||||
!!auth.user && !!detail && isGroupAdmin(auth.user.pubkey, detail.groupId),
|
||||
);
|
||||
|
|
@ -437,37 +464,52 @@
|
|||
class="mt-8 border-t border-neutral-200 pt-6 md:pl-18 dark:border-neutral-700"
|
||||
>
|
||||
{#if auth.user}
|
||||
{#if replyError}
|
||||
<div
|
||||
class="mb-4 rounded border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"
|
||||
>
|
||||
{replyError}
|
||||
{#if joinToReply}
|
||||
<div class="text-center">
|
||||
<p class="mb-3 text-neutral-600 dark:text-neutral-400">
|
||||
This room is restricted. Join to reply to this discussion.
|
||||
</p>
|
||||
<button
|
||||
onclick={onJoinToReply}
|
||||
class="bg-accent hover:bg-accent-hover rounded px-6 py-1.5 font-medium text-white"
|
||||
>
|
||||
Join to reply
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
{#if replyError}
|
||||
<div
|
||||
class="mb-4 rounded border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"
|
||||
>
|
||||
{replyError}
|
||||
</div>
|
||||
{/if}
|
||||
<MessageEditor
|
||||
bind:this={editorEl}
|
||||
bind:value={replyContent}
|
||||
disabled={replying}
|
||||
rows={8}
|
||||
minHeightClass="min-h-[12rem]"
|
||||
placeholder="Write a reply..."
|
||||
contextPubkeys={allPosts.map((p) => p.pubkey)}
|
||||
{threadEventAuthors}
|
||||
/>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button
|
||||
onclick={submitReply}
|
||||
disabled={replying || !replyContent.trim()}
|
||||
class="bg-accent hover:bg-accent-hover rounded px-6 py-1.5 font-medium text-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{replying ? "Posting…" : "post reply"}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<MessageEditor
|
||||
bind:this={editorEl}
|
||||
bind:value={replyContent}
|
||||
disabled={replying}
|
||||
rows={8}
|
||||
minHeightClass="min-h-[12rem]"
|
||||
placeholder="Write a reply..."
|
||||
contextPubkeys={allPosts.map((p) => p.pubkey)}
|
||||
{threadEventAuthors}
|
||||
/>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button
|
||||
onclick={submitReply}
|
||||
disabled={replying || !replyContent.trim()}
|
||||
class="bg-accent hover:bg-accent-hover rounded px-6 py-1.5 font-medium text-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{replying ? "Posting…" : "post reply"}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<p class=" text-center text-neutral-500 dark:text-neutral-400">
|
||||
To participate and reply, please
|
||||
<button onclick={openLogin} class="text-accent hover:underline"
|
||||
>login now</button
|
||||
<button
|
||||
onclick={() => openLogin()}
|
||||
class="text-accent hover:underline">login now</button
|
||||
>
|
||||
</p>
|
||||
{/if}
|
||||
|
|
@ -481,6 +523,25 @@
|
|||
bind:visible={scrubberVisible}
|
||||
/>
|
||||
</div>
|
||||
{:else if threadDetailStore.status === "notfound"}
|
||||
<div class="mx-auto max-w-md py-16 text-center">
|
||||
<h1 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
This discussion is private or unavailable
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
It may belong to a private room. {auth.user
|
||||
? "You may need to be a member to read it."
|
||||
: "Log in as a member to read it."}
|
||||
</p>
|
||||
{#if !auth.user}
|
||||
<button
|
||||
onclick={() => openLogin()}
|
||||
class="bg-accent hover:bg-accent-hover mt-5 rounded px-6 py-1.5 font-medium text-white"
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="py-12 text-center text-neutral-400 dark:text-neutral-500">
|
||||
Loading…
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue