Improve space join flow

This commit is contained in:
Jon Staab 2026-01-08 16:01:36 -08:00
parent f86afc08fa
commit 9a0ad0c663
7 changed files with 54 additions and 55 deletions

View file

@ -86,7 +86,7 @@
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"throttle-debounce": "^5.0.2", "throttle-debounce": "^5.0.2",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"@pomade/core": "^0.0.5" "@pomade/core": "^0.0.7"
}, },
"pnpm": { "pnpm": {
"ignoredBuiltDependencies": [ "ignoredBuiltDependencies": [
@ -106,8 +106,7 @@
"@welshman/router": "link:../welshman/packages/router", "@welshman/router": "link:../welshman/packages/router",
"@welshman/signer": "link:../welshman/packages/signer", "@welshman/signer": "link:../welshman/packages/signer",
"@welshman/store": "link:../welshman/packages/store", "@welshman/store": "link:../welshman/packages/store",
"@welshman/util": "link:../welshman/packages/util", "@welshman/util": "link:../welshman/packages/util"
"@pomade/core": "link:../pomade/packages/core"
} }
} }
} }

View file

@ -10,10 +10,10 @@
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte"
import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte" import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte"
import {attemptRelayAccess} from "@app/core/commands" import {attemptRelayAccess} from "@app/core/commands"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
const {url} = $props() const {url} = $props()
@ -21,7 +21,7 @@
const next = async () => { const next = async () => {
if (error) { if (error) {
return pushToast({theme: "error", message: error, timeout: 30_000}) return pushModal(SpaceAccessRequest, {url})
} }
if (Pool.get().get(url).auth.status === AuthStatus.None) { if (Pool.get().get(url).auth.status === AuthStatus.None) {
@ -35,7 +35,7 @@
let loading = $state(true) let loading = $state(true)
onMount(async () => { onMount(async () => {
;[error] = await Promise.all([attemptRelayAccess(url), sleep(3000)]) ;[error] = await Promise.all([attemptRelayAccess(url), sleep(1000)])
loading = false loading = false
}) })
</script> </script>
@ -77,7 +77,11 @@
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading}> <Button type="submit" class="btn btn-primary" disabled={loading}>
{#if error}
Request Access
{:else}
Join Space Join Space
{/if}
<Icon icon={AltArrowRight} /> <Icon icon={AltArrowRight} />
</Button> </Button>
</ModalFooter> </ModalFooter>

View file

@ -21,7 +21,7 @@
type Props = { type Props = {
url: string url: string
initialValues: RelayProfile initialValues: Partial<RelayProfile>
} }
const {url, initialValues = {}}: Props = $props() const {url, initialValues = {}}: Props = $props()

View file

@ -13,7 +13,8 @@
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import QRCode from "@app/components/QRCode.svelte" import QRCode from "@app/components/QRCode.svelte"
import {clip} from "@app/util/toast" import {clip} from "@app/util/toast"
import {PLATFORM_URL, deriveRelayAuthError} from "@app/core/state" import {PLATFORM_URL} from "@app/core/state"
import {deriveRelayAuthError} from "@app/core/commands"
const {url} = $props() const {url} = $props()

View file

@ -1,6 +1,6 @@
import {nwc} from "@getalby/sdk" import {nwc} from "@getalby/sdk"
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import {get} from "svelte/store" import {get, derived} from "svelte/store"
import type {Override, MakeOptional} from "@welshman/lib" import type {Override, MakeOptional} from "@welshman/lib"
import { import {
first, first,
@ -90,6 +90,7 @@ import {
getPubkeyRelays, getPubkeyRelays,
userBlossomServerList, userBlossomServerList,
shouldUnwrap, shouldUnwrap,
getThunkError,
} from "@welshman/app" } from "@welshman/app"
import {compressFile} from "@lib/html" import {compressFile} from "@lib/html"
import {kv, db} from "@app/core/storage" import {kv, db} from "@app/core/storage"
@ -106,6 +107,9 @@ import {
getSetting, getSetting,
userGroupList, userGroupList,
shouldIgnoreError, shouldIgnoreError,
stripPrefix,
relaysMostlyRestricted,
deriveSocket,
} from "@app/core/state" } from "@app/core/state"
import {loadAlertStatuses} from "@app/core/requests" import {loadAlertStatuses} from "@app/core/requests"
import {platform, platformName, getPushInfo} from "@app/util/push" import {platform, platformName, getPushInfo} from "@app/util/push"
@ -281,12 +285,40 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
if (shouldIgnoreError(error)) return if (shouldIgnoreError(error)) return
if (claim) { if (error.includes("invite code")) return "join request rejected"
const ignoreClaimError =
error.includes("invalid invite code size") || error.includes("failed to validate invite code")
if (!ignoreClaimError) return error?.replace(/^\w+: /, "") return stripPrefix(error)
}
export const deriveRelayAuthError = (url: string, claim = "") => {
// Kick off the auth process
Pool.get().get(url).auth.attemptAuth(sign)
// Attempt to join the relay
const thunk = publishJoinRequest({url, claim})
return derived(
[thunk, relaysMostlyRestricted, deriveSocket(url)],
([$thunk, $relaysMostlyRestricted, $socket]) => {
if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) {
return stripPrefix($socket.auth.details)
} }
if ($relaysMostlyRestricted[url]) {
return stripPrefix($relaysMostlyRestricted[url])
}
const error = getThunkError($thunk)
if (error) {
const isEmptyInvite = !claim && error.includes("invite code")
if (!shouldIgnoreError(error) && !isEmptyInvite) {
return stripPrefix(error) || "join request rejected"
}
}
},
)
} }
// Deletions // Deletions

View file

@ -97,7 +97,6 @@ import {
getTagValue, getTagValue,
getTagValues, getTagValues,
isRelayUrl, isRelayUrl,
makeEvent,
normalizeRelayUrl, normalizeRelayUrl,
readList, readList,
verifyEvent, verifyEvent,
@ -115,12 +114,9 @@ import {
createSearch, createSearch,
userFollowList, userFollowList,
ensurePlaintext, ensurePlaintext,
sign,
signer, signer,
makeOutboxLoader, makeOutboxLoader,
appContext, appContext,
getThunkError,
publishThunk,
deriveRelay, deriveRelay,
makeUserData, makeUserData,
makeUserLoader, makeUserLoader,
@ -986,41 +982,7 @@ export const shouldIgnoreError = (error: string) => {
return isIgnored || isAborted || isStrictNip29Relay return isIgnored || isAborted || isStrictNip29Relay
} }
export const deriveRelayAuthError = (url: string, claim = "") => { export const stripPrefix = (m: string) => m.replace(/^\w+: /, "")
const stripPrefix = (m: string) => m.replace(/^\w+: /, "")
// Kick off the auth process
Pool.get().get(url).auth.attemptAuth(sign)
// Attempt to join the relay
const thunk = publishThunk({
event: makeEvent(RELAY_JOIN, {tags: [["claim", claim]]}),
relays: [url],
})
return derived(
[thunk, relaysMostlyRestricted, deriveSocket(url)],
([$thunk, $relaysMostlyRestricted, $socket]) => {
if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) {
return stripPrefix($socket.auth.details)
}
if ($relaysMostlyRestricted[url]) {
return stripPrefix($relaysMostlyRestricted[url])
}
const error = getThunkError($thunk)
if (error) {
const isEmptyInvite = !claim && error.includes("invite code")
if (!shouldIgnoreError(error) && !isEmptyInvite) {
return stripPrefix(error) || "join request rejected"
}
}
},
)
}
export type InviteData = {url: string; claim: string} export type InviteData = {url: string; claim: string}

View file

@ -9,7 +9,8 @@
import SpaceTrustRelay from "@app/components/SpaceTrustRelay.svelte" import SpaceTrustRelay from "@app/components/SpaceTrustRelay.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {setChecked} from "@app/util/notifications" import {setChecked} from "@app/util/notifications"
import {decodeRelay, deriveRelayAuthError, relaysPendingTrust} from "@app/core/state" import {decodeRelay, relaysPendingTrust} from "@app/core/state"
import {deriveRelayAuthError} from "@app/core/commands"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
type Props = { type Props = {