diff --git a/src/lib/auth.svelte.ts b/src/lib/auth.svelte.ts index 89c192c..2fc1a23 100644 --- a/src/lib/auth.svelte.ts +++ b/src/lib/auth.svelte.ts @@ -19,6 +19,9 @@ export type Signer = { let user = $state(null); let signer = $state(null); let loginModalOpen = $state(false); +// Optional action to run once login succeeds, so an intent like "post" started +// while logged out resumes (login => join => composer) instead of being dropped. +let afterLogin: (() => void) | null = null; // Bumped on explicit login/logout (not on silent session restore) so the app // can re-fetch identity-scoped data — e.g. reload the room list once the relay // will serve the user's private/hidden groups. @@ -43,12 +46,23 @@ const PUBKEY_KEY = "nostr_pubkey"; const METHOD_KEY = "nostr_login_method"; const NSEC_KEY = "nostr_nsec"; -export function openLogin() { +export function openLogin(after?: () => void) { + afterLogin = after ?? null; loginModalOpen = true; } export function closeLogin() { loginModalOpen = false; + afterLogin = null; +} + +// Closes the modal and runs the pending intent (if any). Called from the login +// flows so the continuation fires with the modal already gone (no stacking). +function runAfterLogin() { + const cb = afterLogin; + afterLogin = null; + loginModalOpen = false; + cb?.(); } function makeNsecSigner(secretKey: Uint8Array): Signer { @@ -97,6 +111,7 @@ export async function loginWithExtension() { localStorage.removeItem(NSEC_KEY); await setUser(pubkey); sessionEpoch++; + runAfterLogin(); } function parseSecretKey(input: string): Uint8Array { @@ -128,6 +143,7 @@ export async function loginWithNsec(input: string) { localStorage.setItem(NSEC_KEY, nsec); await setUser(pubkey); sessionEpoch++; + runAfterLogin(); } export function logout() { diff --git a/src/lib/components/ChatSidebar.svelte b/src/lib/components/ChatSidebar.svelte index 8a93841..67f3d37 100644 --- a/src/lib/components/ChatSidebar.svelte +++ b/src/lib/components/ChatSidebar.svelte @@ -10,7 +10,12 @@ import { auth, openLogin } from "$lib/auth.svelte"; import { isGroupAdmin } from "$lib/admins.svelte"; import { requestDelete } from "$lib/moderation.svelte"; - import { withJoin } from "$lib/join.svelte"; + import { + withJoin, + membershipOf, + ensureMembershipChecked, + openJoinModal, + } from "$lib/join.svelte"; import { activeGroup } from "$lib/active.svelte"; import type { NostrUser } from "@nostr/gadgets/metadata"; import MentionAutocomplete from "$lib/components/MentionAutocomplete.svelte"; @@ -46,6 +51,24 @@ !!auth.user && isGroupAdmin(auth.user.pubkey, activeGroup.id), ); + // Sending needs membership regardless of flags. Only gate a confirmed guest; + // while membership resolves the input stays (withJoin nets a stray send), so a + // member never flashes "Join to chat". + const joinToChat = $derived( + !!auth.user && membershipOf(activeGroup.id) === "guest", + ); + + // Resolve membership on entry. Depends on auth.user so a silent session + // restore re-runs the check. + $effect(() => { + auth.user; + if (activeGroup.id) ensureMembershipChecked(activeGroup.id); + }); + + function onJoinToChat() { + openJoinModal(activeGroup.id, () => tick().then(() => inputEl?.focus())); + } + function requestDeleteMessage(msg: ChatMessageData) { openMenuId = null; requestDelete( @@ -431,15 +454,24 @@ {sendError} {/if} - + {#if joinToChat} + + {:else} + + {/if} diff --git a/src/lib/components/DiscussionsFeed.svelte b/src/lib/components/DiscussionsFeed.svelte index d28e8a3..d039217 100644 --- a/src/lib/components/DiscussionsFeed.svelte +++ b/src/lib/components/DiscussionsFeed.svelte @@ -14,6 +14,12 @@ import type { NostrUser } from "@nostr/gadgets/metadata"; import { auth, openLogin } from "$lib/auth.svelte"; import { openDraft } from "$lib/draft.svelte"; + import { getGroupFlags } from "$lib/group.svelte"; + import { + membershipOf, + ensureMembershipChecked, + openJoinModal, + } from "$lib/join.svelte"; import { sortPref } from "$lib/sort.svelte"; import { page } from "$app/state"; @@ -38,17 +44,47 @@ if (urlSort) sortPref.value = urlSort; }); + const flags = $derived(getGroupFlags(groupId)); + const member = $derived(membershipOf(groupId)); + + // The relay requires membership to post to any group, so resolve it on entry + // for every room. Depends on auth.user so a silent session restore re-runs it. + $effect(() => { + auth.user; + ensureMembershipChecked(groupId); + }); + + const showPrivateGate = $derived(!!flags?.isPrivate && member === "guest"); + const checkingAccess = $derived(!!flags?.isPrivate && member === "unknown"); + // Posting needs membership regardless of flags. Only gate a confirmed guest; + // while membership is still resolving the click awaits the check, so a member + // never flashes "Join to post". + const joinToPost = $derived(!!auth.user && member === "guest"); + // Re-runs on mount and whenever the group or effective sort changes. $effect(() => { loadThreads(groupId, sort); }); - function onNewTopic() { + async function onNewTopic() { + if (!auth.user) { + openLogin(onNewTopic); + return; + } + await ensureMembershipChecked(groupId); + if (membershipOf(groupId) !== "member") { + openJoinModal(groupId, openDraft); + return; + } + openDraft(); + } + + function onPrivateJoin() { if (!auth.user) { openLogin(); return; } - openDraft(); + openJoinModal(groupId, () => loadThreads(groupId, sort)); } function relativeTime(ts: number): string { @@ -98,39 +134,72 @@

{title}

-
- - -
+ {#if !showPrivateGate && !checkingAccess} +
+ + +
+ {/if}
-
- {#each rows as thread} - - {/each} -
- - {#if threadStore.loading && rows.length === 0} -

- Loading discussions… + {#if checkingAccess} +

+ Checking access…

- {:else if !threadStore.exhausted} -
-
- {:else if rows.length > 0} -

No more discussions

+ {:else} +
+ {#each rows as thread} + + {/each} +
+ + {#if threadStore.loading && rows.length === 0} +

+ Loading discussions… +

+ {:else if !threadStore.exhausted} +
+ +
+ {:else if rows.length > 0} +

+ No more discussions +

+ {/if} {/if}
diff --git a/src/lib/components/JoinModal.svelte b/src/lib/components/JoinModal.svelte index c427bc3..4fe7757 100644 --- a/src/lib/components/JoinModal.svelte +++ b/src/lib/components/JoinModal.svelte @@ -1,19 +1,19 @@