From 1eebe1698a554751357142e0a59f09018a1397cc Mon Sep 17 00:00:00 2001 From: Aditya Chaudhary <30+useradityaa@noreply.coracle.social> Date: Mon, 20 Jul 2026 23:35:24 +0000 Subject: [PATCH] feat: add room invite links with combined space and room join flow (#319) Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/319 Co-authored-by: Aditya Chaudhary <30+useradityaa@noreply.coracle.social> Co-committed-by: Aditya Chaudhary <30+useradityaa@noreply.coracle.social> --- src/app/access.ts | 417 +++++++++++++++++++ src/app/components/RoomDetail.svelte | 27 +- src/app/components/RoomInvite.svelte | 135 ++++++ src/app/components/SignUp.svelte | 2 +- src/app/components/SpaceAccessRequest.svelte | 7 +- src/app/components/SpaceAuthError.svelte | 2 +- src/app/components/SpaceExit.svelte | 2 +- src/app/components/SpaceInvite.svelte | 63 +-- src/app/components/SpaceInviteAccept.svelte | 70 ++-- src/app/components/SpaceJoin.svelte | 35 +- src/app/invites.ts | 27 -- src/app/relays.ts | 157 +------ src/routes/+layout.svelte | 4 +- src/routes/spaces/+page.svelte | 2 +- src/routes/spaces/[relay]/+layout.svelte | 7 +- src/routes/spaces/[relay]/[h]/+page.svelte | 11 +- 16 files changed, 646 insertions(+), 322 deletions(-) create mode 100644 src/app/access.ts create mode 100644 src/app/components/RoomInvite.svelte delete mode 100644 src/app/invites.ts diff --git a/src/app/access.ts b/src/app/access.ts new file mode 100644 index 00000000..c396e194 --- /dev/null +++ b/src/app/access.ts @@ -0,0 +1,417 @@ +import {derived, get, writable, type Readable} from "svelte/store" +import { + dissoc, + filterVals, + fromPairs, + isDefined, + last, + poll, + randomId, + sleep, + tryCatch, +} from "@welshman/lib" +import {AuthStatus, load, Pool, request, SocketStatus} from "@welshman/net" +import { + displayRelayUrl, + getTagValue, + isRelayUrl, + makeEvent, + normalizeRelayUrl, + RELAY_INVITE, + RELAY_JOIN, + RELAY_LEAVE, + ROOM_JOIN, +} from "@welshman/util" +import {publishThunk, sign, waitForThunkError} from "@welshman/app" +import {stripPrefix} from "@lib/util" +import {PLATFORM_URL} from "@app/env" +import {addSpace, userSpaceUrls} from "@app/groups" +import {relaysMostlyRestricted} from "@app/policies" +import {broadcastUserData} from "@app/profiles" +import {Push} from "@app/push" +import {resyncApplicationData} from "@app/sync" +import {notificationSettings, setSpaceNotifications} from "@app/settings" +import {deriveSocket} from "@app/relays" + +export const ROOM_CREATE_INVITE = 9009 + +export type InviteData = { + url: string + claim: string + h?: string + code?: string +} + +export type InviteCreateStatus = "loading" | "network" | "auth" | "failed" | "ready" + +export type JoinRequestParams = { + url: string + claim: string +} + +export const isNetworkAuthError = (error: string) => { + const lower = error.toLowerCase() + + return lower.includes("failed") || lower.includes("timeout") || lower.includes("network") +} + +export const isRequestNetworkError = (error: unknown) => + error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError") + +export const parseInviteLink = (invite: string): InviteData | undefined => { + if (invite.length < 3 || !invite.includes(".")) { + return + } + + return ( + tryCatch(() => { + const params = fromPairs(Array.from(new URL(invite).searchParams)) + const {r: relay = "", c: claim = "", h = "", code = ""} = params + const url = normalizeRelayUrl(relay) + + if (isRelayUrl(url)) { + return {url, claim, h: h || undefined, code: code || undefined} + } + }) || + tryCatch(() => { + const url = normalizeRelayUrl(invite) + + if (isRelayUrl(url)) { + return {url, claim: ""} + } + }) + ) +} + +export const makeInviteLink = (data: InviteData) => { + const params = new URLSearchParams({ + r: displayRelayUrl(data.url), + c: data.claim, + }) + + if (data.h) { + params.set("h", data.h) + } + + if (data.code) { + params.set("code", data.code) + } + + return PLATFORM_URL + "/join?" + params.toString() +} + +export const shouldIgnoreError = (error: string) => { + const isIgnored = error.startsWith("mute: ") + const isAborted = error.includes("Signing was aborted") + const isStrictNip29Relay = error.includes("missing group (`h`) tag") + + return isIgnored || isAborted || isStrictNip29Relay +} + +export const deriveRelayAuthError = (url: string) => { + Pool.get().get(url).auth.attemptAuth(sign) + + return derived( + [relaysMostlyRestricted, deriveSocket(url)], + ([$relaysMostlyRestricted, $socket]) => { + if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) { + return stripPrefix($socket.auth.details) + } + + if ($relaysMostlyRestricted[url]) { + return stripPrefix($relaysMostlyRestricted[url]) + } + }, + ) +} + +export const requestRelayClaim = async (url: string) => { + const filters = [{kinds: [RELAY_INVITE], limit: 1}] + const events = await load({filters, relays: [url]}) + + if (events.length > 0) { + return getTagValue("claim", events[0].tags) + } +} + +export const requestRelayClaims = async (urls: string[]) => + filterVals( + isDefined, + fromPairs(await Promise.all(urls.map(async url => [url, await requestRelayClaim(url)]))), + ) + +export const makeJoinRequest = (params: JoinRequestParams) => + makeEvent(RELAY_JOIN, {tags: [["claim", params.claim]]}) + +export const publishJoinRequest = (params: JoinRequestParams) => + publishThunk({event: makeJoinRequest(params), relays: [params.url]}) + +export type LeaveRequestParams = { + url: string +} + +export const publishLeaveRequest = (params: LeaveRequestParams) => + publishThunk({event: makeEvent(RELAY_LEAVE), relays: [params.url]}) + +export const publishRoomInvite = async (url: string, h: string) => { + const code = randomId() + const error = await waitForThunkError( + publishThunk({ + event: makeEvent(ROOM_CREATE_INVITE, { + tags: [ + ["h", h], + ["code", code], + ], + }), + relays: [url], + }), + ) + + if (error) { + return {error, code: undefined} + } + + return {error: undefined, code} +} + +export const publishRoomJoinRequest = (url: string, h: string, code?: string) => { + const tags = [["h", h]] + + if (code) { + tags.push(["code", code]) + } + + return publishThunk({ + event: makeEvent(ROOM_JOIN, {tags}), + relays: [url], + }) +} + +const authTerminalStatuses = [ + AuthStatus.None, + AuthStatus.Ok, + AuthStatus.Forbidden, + AuthStatus.DeniedSignature, +] + +const waitForAuth = async (socket: ReturnType["get"]>) => { + await poll({ + signal: AbortSignal.timeout(10_000), + condition: () => authTerminalStatuses.includes(socket.auth.status), + }).catch(() => {}) +} + +const formatAuthError = (status: AuthStatus, details?: string) => { + if (status === AuthStatus.DeniedSignature) { + return "Failed to authenticate — check your signer" + } + + if (status === AuthStatus.PendingSignature) { + return "Failed to authenticate — approve the signing request from your extension or signer" + } + + const message = details || last(status.split(":")) + + return `Failed to authenticate (${message})` +} + +export const attemptRelayAccess = async (url: string, claim = "") => { + const socket = Pool.get().get(url) + + socket.attemptToOpen() + + await poll({ + signal: AbortSignal.timeout(3000), + condition: () => socket.status === SocketStatus.Open, + }) + + if (socket.status !== SocketStatus.Open) { + return `Failed to connect` + } + + await poll({ + signal: AbortSignal.timeout(3000), + condition: () => socket.auth.status === AuthStatus.Requested, + }) + + for (let i = 0; i < 3 && ![AuthStatus.None, AuthStatus.Ok].includes(socket.auth.status); i++) { + await socket.auth.retryAuth(sign) + await waitForAuth(socket) + } + + if (![AuthStatus.None, AuthStatus.Ok].includes(socket.auth.status)) { + return formatAuthError(socket.auth.status, socket.auth.details) + } + + const error = await waitForThunkError(publishJoinRequest({url, claim})) + + if (shouldIgnoreError(error)) return + + if (claim) { + if (error.includes("invite code")) { + return "join request rejected" + } + } else if (error.includes("invite code")) { + return + } + + return stripPrefix(error) +} + +export class Access { + url: string + authError: Readable + networkError = writable(false) + loading = writable(true) + claim = writable("") + roomCode = writable() + roomInviteError = writable() + isExplicitAuthError: Readable + isGenericError: Readable + + constructor(url: string) { + this.url = url + this.authError = deriveRelayAuthError(url) + this.isExplicitAuthError = derived([this.authError], ([$authError]) => + Boolean($authError && !isNetworkAuthError($authError)), + ) + this.isGenericError = derived( + [this.authError, this.networkError, this.isExplicitAuthError], + ([$authError, $networkError, $isExplicitAuthError]) => + $networkError || Boolean($authError && !$isExplicitAuthError), + ) + } + + createInviteStatus(requireCode: boolean) { + return derived( + [ + this.loading, + this.isGenericError, + this.isExplicitAuthError, + this.roomInviteError, + this.roomCode, + ], + ([$loading, $isGeneric, $isExplicit, $roomInviteError, $roomCode]) => { + if ($loading) return "loading" as const + if ($isGeneric) return "network" as const + if ($isExplicit || $roomInviteError) return "auth" as const + if (requireCode && !$roomCode) return "failed" as const + + return "ready" as const + }, + ) + } + + clearRestricted() { + relaysMostlyRestricted.update(dissoc(this.url)) + } + + async attempt(claim = "") { + return attemptRelayAccess(this.url, claim) + } + + async configureNotifications(enabled: boolean) { + if (enabled) { + if (!notificationSettings.get().push) { + await setSpaceNotifications(this.url, true) + } else { + const permissions = await Push.request() + + if (permissions.startsWith("granted")) { + await setSpaceNotifications(this.url, true) + } + } + } else { + await setSpaceNotifications(this.url, false) + } + } + + async completeJoin(notifications: boolean) { + await this.configureNotifications(notifications) + await addSpace(this.url) + this.clearRestricted() + resyncApplicationData() + broadcastUserData([this.url]) + } + + async joinSpace({ + claim = "", + notifications, + alreadyJoined = false, + }: { + claim?: string + notifications: boolean + alreadyJoined?: boolean + }) { + if (!alreadyJoined) { + const error = await this.attempt(claim) + + if (error) { + return error + } + + await this.completeJoin(notifications) + } + } + + async joinRoom(h: string, code?: string) { + if (!code) return + + const message = await waitForThunkError(publishRoomJoinRequest(this.url, h, code)) + + if (message && !message.startsWith("duplicate:")) { + return message + } + } + + async acceptInvite(data: InviteData, notifications: boolean) { + const alreadyInSpace = get(userSpaceUrls).includes(data.url) + const error = await this.joinSpace({ + claim: data.claim, + notifications, + alreadyJoined: alreadyInSpace, + }) + + if (error) { + return error + } + + if (data.h && data.code) { + return this.joinRoom(data.h, data.code) + } + } + + async prepareInvite(h?: string) { + this.loading.set(true) + this.networkError.set(false) + + try { + const [[event], roomInviteResult] = await Promise.all([ + request({ + relays: [this.url], + autoClose: true, + signal: AbortSignal.timeout(10000), + filters: [{kinds: [RELAY_INVITE]}], + }), + h ? publishRoomInvite(this.url, h) : Promise.resolve({code: undefined, error: undefined}), + sleep(2000), + ]) + + this.claim.set(getTagValue("claim", event?.tags || []) || "") + + if (h) { + this.roomCode.set(roomInviteResult.code) + this.roomInviteError.set(roomInviteResult.error) + } + } catch (error) { + this.claim.set("") + this.roomCode.set(undefined) + + if (isRequestNetworkError(error)) { + this.networkError.set(true) + } + } finally { + this.loading.set(false) + } + } +} diff --git a/src/app/components/RoomDetail.svelte b/src/app/components/RoomDetail.svelte index 80d801f7..3f0c8133 100644 --- a/src/app/components/RoomDetail.svelte +++ b/src/app/components/RoomDetail.svelte @@ -1,6 +1,7 @@ + + + + + Create a Room Invite + + Get a link to invite people to + + in {displayRelayUrl(url)} + + +
+ {#if $inviteStatus === "loading"} +

+ Requesting an invite link... +

+ {:else if $inviteStatus === "network"} +

+ Unable to reach the space. Please check your connection and try again. +

+ {:else if $inviteStatus === "auth"} +

+ Oops! It looks like you're not allowed to create invites for this room. +

+ {:else if $inviteStatus === "failed"} +

Unable to create a room invite code.

+ {:else} +
+
+ +
+ + {#snippet input()} +
+ {#if canShare} + + {/if} + + +
+ {/snippet} + {#snippet info()} +

+ This invite link includes access to the space and room. Anyone with the link can + join by opening it in {PLATFORM_NAME}. + {#if !$claim} + This space did not issue a claim for this link, so additional steps might be + required. + {/if} +

+ {/snippet} +
+
+ {/if} +
+
+ + + +
diff --git a/src/app/components/SignUp.svelte b/src/app/components/SignUp.svelte index 4fd5662e..2d37d427 100644 --- a/src/app/components/SignUp.svelte +++ b/src/app/components/SignUp.svelte @@ -24,7 +24,7 @@ import SignUpEmail from "@app/components/SignUpEmail.svelte" import SignUpProfile from "@app/components/SignUpProfile.svelte" import SignUpComplete from "@app/components/SignUpComplete.svelte" - import {attemptRelayAccess} from "@app/relays" + import {attemptRelayAccess} from "@app/access" import { POMADE_SIGNERS, PLATFORM_NAME, diff --git a/src/app/components/SpaceAccessRequest.svelte b/src/app/components/SpaceAccessRequest.svelte index ac22cc25..7d30e270 100644 --- a/src/app/components/SpaceAccessRequest.svelte +++ b/src/app/components/SpaceAccessRequest.svelte @@ -15,8 +15,7 @@ import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte" import {pushToast} from "@app/toast" - import {attemptRelayAccess} from "@app/relays" - import {parseInviteLink} from "@app/invites" + import {Access, parseInviteLink} from "@app/access" type Props = { url: string @@ -25,6 +24,8 @@ const {url, callback}: Props = $props() + const access = new Access(url) + const back = () => history.back() const join = async () => { @@ -32,7 +33,7 @@ try { const claim = parseInviteLink(value)?.claim || value - const message = await attemptRelayAccess(url, claim) + const message = await access.attempt(claim) if (message) { return pushToast({theme: "error", message, timeout: 30_000}) diff --git a/src/app/components/SpaceAuthError.svelte b/src/app/components/SpaceAuthError.svelte index 83d5a0c1..1f179989 100644 --- a/src/app/components/SpaceAuthError.svelte +++ b/src/app/components/SpaceAuthError.svelte @@ -15,9 +15,9 @@ import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte" import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte" + import {publishLeaveRequest} from "@app/access" import {pushModal, clearModals} from "@app/modal" import {removeSpace} from "@app/groups" - import {publishLeaveRequest} from "@app/relays" import {removeTrustedRelay} from "@app/settings" const {url, error} = $props() diff --git a/src/app/components/SpaceExit.svelte b/src/app/components/SpaceExit.svelte index 0df8462f..5bd8ca89 100644 --- a/src/app/components/SpaceExit.svelte +++ b/src/app/components/SpaceExit.svelte @@ -11,8 +11,8 @@ import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte" + import {publishLeaveRequest} from "@app/access" import {removeSpace} from "@app/groups" - import {publishLeaveRequest} from "@app/relays" import {removeTrustedRelay} from "@app/settings" const {url} = $props() diff --git a/src/app/components/SpaceInvite.svelte b/src/app/components/SpaceInvite.svelte index f9e81170..cca1eaed 100644 --- a/src/app/components/SpaceInvite.svelte +++ b/src/app/components/SpaceInvite.svelte @@ -1,8 +1,6 @@ @@ -133,15 +96,15 @@
- {#if loading} + {#if $inviteStatus === "loading"}

- Requesting an invite link... + Requesting an invite link...

- {:else if isGenericError} + {:else if $inviteStatus === "network"}

Unable to reach the space. Please check your connection and try again.

- {:else if isExplicitAuthError} + {:else if $inviteStatus === "auth"}

Oops! It looks like you're not a member of this space.

@@ -173,7 +136,7 @@ {#snippet info()}

This invite link can be used by clicking "Add Space" and pasting it there. - {#if !claim} + {#if !$claim} This space did not issue a claim for this link, so additional steps might be required. {/if} diff --git a/src/app/components/SpaceInviteAccept.svelte b/src/app/components/SpaceInviteAccept.svelte index 02a9dcf4..4fdda615 100644 --- a/src/app/components/SpaceInviteAccept.svelte +++ b/src/app/components/SpaceInviteAccept.svelte @@ -1,6 +1,7 @@ {#if !hideHeader} - Join a Space - Enter a relay URL or invite link below to join an existing space. + {isRoomInvite ? "Join a Room" : "Join a Space"} + + {#if isRoomInvite} + Enter a room invite link below to join an existing room. + {:else} + Enter a relay URL or invite link below to join an existing space. + {/if} + {#snippet label()} @@ -145,7 +137,7 @@ Go back diff --git a/src/app/components/SpaceJoin.svelte b/src/app/components/SpaceJoin.svelte index 0a2bbb7a..b76ba397 100644 --- a/src/app/components/SpaceJoin.svelte +++ b/src/app/components/SpaceJoin.svelte @@ -1,6 +1,6 @@ diff --git a/src/app/invites.ts b/src/app/invites.ts deleted file mode 100644 index a38a9ba2..00000000 --- a/src/app/invites.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {fromPairs, tryCatch} from "@welshman/lib" -import {isRelayUrl, normalizeRelayUrl} from "@welshman/util" -export type InviteData = {url: string; claim: string} - -export const parseInviteLink = (invite: string): InviteData | undefined => { - if (invite.length < 3 || !invite.includes(".")) { - return - } - - return ( - tryCatch(() => { - const {r: relay = "", c: claim = ""} = fromPairs(Array.from(new URL(invite).searchParams)) - const url = normalizeRelayUrl(relay) - - if (isRelayUrl(url)) { - return {url, claim} - } - }) || - tryCatch(() => { - const url = normalizeRelayUrl(invite) - - if (isRelayUrl(url)) { - return {url, claim: ""} - } - }) - ) -} diff --git a/src/app/relays.ts b/src/app/relays.ts index ddb417ac..9f9160ea 100644 --- a/src/app/relays.ts +++ b/src/app/relays.ts @@ -1,38 +1,16 @@ import { - RELAY_INVITE, - RELAY_JOIN, - RELAY_LEAVE, ManagementMethod, getRelaysFromList, - getTagValue, isShareableRelayUrl, isSignedEvent, - makeEvent, normalizeRelayUrl, } from "@welshman/util" import type {List, RelayProfile} from "@welshman/util" -import {AuthStateEvent, AuthStatus, Pool, SocketEvent, SocketStatus, load} from "@welshman/net" +import {AuthStateEvent, AuthStatus, Pool, SocketEvent, SocketStatus} from "@welshman/net" import {derived, readable} from "svelte/store" -import { - call, - filterVals, - fromPairs, - isDefined, - last, - on, - poll, - simpleCache, - uniq, -} from "@welshman/lib" +import {call, on, simpleCache, uniq} from "@welshman/lib" import {throttled} from "@welshman/store" -import { - getRelay, - loadRelay, - manageRelay, - publishThunk, - sign, - waitForThunkError, -} from "@welshman/app" +import {getRelay, loadRelay, manageRelay, publishThunk, waitForThunkError} from "@welshman/app" import {checkRelayHasLivekit} from "$lib/livekit" import {stripPrefix} from "@lib/util" import {relaysMostlyRestricted} from "@app/policies" @@ -145,14 +123,6 @@ export const deriveHasLivekit = simpleCache(([url]: [string]) => }), ) -export const shouldIgnoreError = (error: string) => { - const isIgnored = error.startsWith("mute: ") - const isAborted = error.includes("Signing was aborted") - const isStrictNip29Relay = error.includes("missing group (`h`) tag") - - return isIgnored || isAborted || isStrictNip29Relay -} - export const discoverRelays = (lists: List[]) => Promise.all( uniq(lists.flatMap($l => getRelaysFromList($l))) @@ -160,125 +130,4 @@ export const discoverRelays = (lists: List[]) => .map(url => loadRelay(url)), ) -export const requestRelayClaim = async (url: string) => { - const filters = [{kinds: [RELAY_INVITE], limit: 1}] - const events = await load({filters, relays: [url]}) - - if (events.length > 0) { - return getTagValue("claim", events[0].tags) - } -} - -export const requestRelayClaims = async (urls: string[]) => - filterVals( - isDefined, - fromPairs(await Promise.all(urls.map(async url => [url, await requestRelayClaim(url)]))), - ) - export const canEnforceNip70 = (url: string) => hasNip70(getRelay(url)) - -const authTerminalStatuses = [ - AuthStatus.None, - AuthStatus.Ok, - AuthStatus.Forbidden, - AuthStatus.DeniedSignature, -] - -const waitForAuth = async (socket: ReturnType["get"]>) => { - await poll({ - signal: AbortSignal.timeout(10_000), - condition: () => authTerminalStatuses.includes(socket.auth.status), - }).catch(() => {}) -} - -const formatAuthError = (status: AuthStatus, details?: string) => { - if (status === AuthStatus.DeniedSignature) { - return "Failed to authenticate — check your signer" - } - - if (status === AuthStatus.PendingSignature) { - return "Failed to authenticate — approve the signing request from your extension or signer" - } - - const message = details || last(status.split(":")) - - return `Failed to authenticate (${message})` -} - -export const attemptRelayAccess = async (url: string, claim = "") => { - const socket = Pool.get().get(url) - - socket.attemptToOpen() - - await poll({ - signal: AbortSignal.timeout(3000), - condition: () => socket.status === SocketStatus.Open, - }) - - if (socket.status !== SocketStatus.Open) { - return `Failed to connect` - } - - await poll({ - signal: AbortSignal.timeout(3000), - condition: () => socket.auth.status === AuthStatus.Requested, - }) - - for (let i = 0; i < 3 && ![AuthStatus.None, AuthStatus.Ok].includes(socket.auth.status); i++) { - await socket.auth.retryAuth(sign) - await waitForAuth(socket) - } - - if (![AuthStatus.None, AuthStatus.Ok].includes(socket.auth.status)) { - return formatAuthError(socket.auth.status, socket.auth.details) - } - - const error = await waitForThunkError(publishJoinRequest({url, claim})) - - if (shouldIgnoreError(error)) return - - if (claim) { - if (error.includes("invite code")) { - return "join request rejected" - } - } else if (error.includes("invite code")) { - return - } - - return stripPrefix(error) -} - -export const deriveRelayAuthError = (url: string) => { - Pool.get().get(url).auth.attemptAuth(sign) - - return derived( - [relaysMostlyRestricted, deriveSocket(url)], - ([$relaysMostlyRestricted, $socket]) => { - if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) { - return stripPrefix($socket.auth.details) - } - - if ($relaysMostlyRestricted[url]) { - return stripPrefix($relaysMostlyRestricted[url]) - } - }, - ) -} - -export type JoinRequestParams = { - url: string - claim: string -} - -export const makeJoinRequest = (params: JoinRequestParams) => - makeEvent(RELAY_JOIN, {tags: [["claim", params.claim]]}) - -export const publishJoinRequest = (params: JoinRequestParams) => - publishThunk({event: makeJoinRequest(params), relays: [params.url]}) - -export type LeaveRequestParams = { - url: string -} - -export const publishLeaveRequest = (params: LeaveRequestParams) => - publishThunk({event: makeEvent(RELAY_LEAVE), relays: [params.url]}) diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index acd8a9e8..ac76f5fe 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -60,7 +60,7 @@ import * as appDevice from "@app/device" import * as actionItems from "@app/actionItems" import * as appFeeds from "@app/feeds" - import * as invites from "@app/invites" + import * as access from "@app/access" import * as healthChecks from "@app/healthChecks" import {theme} from "@app/theme" import {toast, pushToast} from "@app/toast" @@ -112,7 +112,7 @@ ...appDevice, ...actionItems, ...appFeeds, - ...invites, + ...access, ...healthChecks, ...comments, ...deletes, diff --git a/src/routes/spaces/+page.svelte b/src/routes/spaces/+page.svelte index da29b663..e0743ad2 100644 --- a/src/routes/spaces/+page.svelte +++ b/src/routes/spaces/+page.svelte @@ -29,7 +29,7 @@ import {userSpaceUrls, loadUserGroupList, groupListPubkeysByUrl, setSpaces} from "@app/groups" import {PLATFORM_RELAYS, DEFAULT_RELAYS} from "@app/env" import {bootstrapPubkeys} from "@app/social" - import {parseInviteLink} from "@app/invites" + import {parseInviteLink} from "@app/access" import {pushModal} from "@app/modal" import {goToSpace, makeSpacePath} from "@app/routes" import {notifications} from "@app/notifications" diff --git a/src/routes/spaces/[relay]/+layout.svelte b/src/routes/spaces/[relay]/+layout.svelte index d0f11f6c..3b56f5a0 100644 --- a/src/routes/spaces/[relay]/+layout.svelte +++ b/src/routes/spaces/[relay]/+layout.svelte @@ -12,11 +12,12 @@ import SpaceAuthError from "@app/components/SpaceAuthError.svelte" import SpaceTrustRelay from "@app/components/SpaceTrustRelay.svelte" import SpaceJoin from "@app/components/SpaceJoin.svelte" - import {modal, pushModal} from "@app/modal" - import {makeSpacePath} from "@app/routes" - import {decodeRelay, deriveRelayAuthError} from "@app/relays" + import {deriveRelayAuthError} from "@app/access" import {loadUserGroupList, userSpaceUrls} from "@app/groups" + import {modal, pushModal} from "@app/modal" import {relaysPendingTrust} from "@app/policies" + import {decodeRelay} from "@app/relays" + import {makeSpacePath} from "@app/routes" type Props = { children?: Snippet diff --git a/src/routes/spaces/[relay]/[h]/+page.svelte b/src/routes/spaces/[relay]/[h]/+page.svelte index 8a0f0413..e89c61f2 100644 --- a/src/routes/spaces/[relay]/[h]/+page.svelte +++ b/src/routes/spaces/[relay]/[h]/+page.svelte @@ -33,11 +33,11 @@ import ThunkToast from "@app/components/ThunkToast.svelte" import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte" import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte" - import {canEnforceNip70} from "@app/relays" - import {prependParent, deriveRoom, getRoomType, PROTECTED, RoomType} from "@app/groups" + import {publishRoomJoinRequest} from "@app/access" import {publishDelete} from "@app/deletes" - import {decodeRelay} from "@app/relays" + import {prependParent, deriveRoom, getRoomType, PROTECTED, RoomType} from "@app/groups" import {deriveUserRoomMembershipStatus, MembershipStatus} from "@app/members" + import {canEnforceNip70, decodeRelay} from "@app/relays" import {userSettingsValues} from "@app/settings" import VoiceWidget from "@app/components/VoiceWidget.svelte" import VideoCallContent from "@app/components/VideoCallContent.svelte" @@ -121,6 +121,7 @@ const shouldProtect = canEnforceNip70(url) const membershipStatus = deriveUserRoomMembershipStatus(url, h) const at = $derived(parseInt($page.url.searchParams.get("at")!)) + const inviteCode = $derived($page.url.searchParams.get("code") || "") const showRoomDetail = () => pushModal(RoomDetail, {url, h}) @@ -128,7 +129,9 @@ joining = true try { - const message = await waitForThunkError(joinRoom(url, makeRoomMeta({h}))) + const message = await waitForThunkError( + inviteCode ? publishRoomJoinRequest(url, h, inviteCode) : joinRoom(url, makeRoomMeta({h})), + ) if (message && !message.startsWith("duplicate:")) { return pushToast({theme: "error", message})