Replace the Access class with plain join functions, move invite preparation into InviteLink and space notification opt-in into push
This commit is contained in:
parent
b924523455
commit
f1946b9a55
8 changed files with 123 additions and 216 deletions
|
|
@ -144,19 +144,23 @@ has no getter for them yet. Roles are only ever changed over
|
||||||
NIP-86 (`createRole`, `editRole`, `deleteRole`, `assignRole`, `unassignRole`), never by publishing
|
NIP-86 (`createRole`, `editRole`, `deleteRole`, `assignRole`, `unassignRole`), never by publishing
|
||||||
33534.
|
33534.
|
||||||
|
|
||||||
Joining a space (`attemptRelayAccess` and `Access` in `src/app/access.ts`):
|
Joining a space (`attemptRelayAccess` and `completeSpaceJoin` in `src/app/access.ts`):
|
||||||
|
|
||||||
1. Open the socket and drive NIP-42 auth, retrying up to three times.
|
1. Open the socket and drive NIP-42 auth, retrying up to three times.
|
||||||
2. Publish `RelayJoin` with the claim. The writer protects the event and requires a forced relay.
|
2. Publish `RelayJoin` with the claim. The writer protects the event and requires a forced relay.
|
||||||
3. Translate refusals: "invite code" means rejected, "claim" means the space needs an invite.
|
3. Translate refusals: a `mute:` reply (`matchReason`) is ignored, and the free-text "invite code"
|
||||||
4. `completeJoin`: `roomLists.addRelay(url)`, restart sync, and `Sync.push` the user's `RELAYS`,
|
means rejected and "claim" means the space needs an invite.
|
||||||
|
4. `completeSpaceJoin`: opt into the space's notifications (`optInSpaceNotifications` in
|
||||||
|
`src/app/push`, which asks for push permission first when push is on), `roomLists.addRelay(url)`,
|
||||||
|
clear the relay from `relaysMostlyRestricted`, restart sync, and `Sync.push` the user's `RELAYS`,
|
||||||
`MESSAGING_RELAYS`, `FOLLOWS` and `PROFILE` to the space so other members can see them.
|
`MESSAGING_RELAYS`, `FOLLOWS` and `PROFILE` to the space so other members can see them.
|
||||||
|
|
||||||
Invite links are `${PLATFORM_URL}/join?r=<relay>&c=<claim>`, plus `h` and `code` for a room
|
Invite links are `${PLATFORM_URL}/join?r=<relay>&c=<claim>`, plus `h` and `code` for a room
|
||||||
(`makeInviteLink`; `parseInviteLink` also accepts a bare relay URL). `src/routes/join` renders
|
(`makeInviteLink`; `parseInviteLink` also accepts a bare relay URL). `src/routes/join` renders
|
||||||
`SpaceInviteAccept`, which calls `Access.acceptInvite` to join the space and then the room.
|
`SpaceInviteAccept`, which calls `acceptInvite` to join the space (skipped if already joined) and
|
||||||
`Access.prepareInvite` gets a claim over NIP-86 (`supportedmethods`, then `listclaims`, then
|
then the room. `InviteLink`, rendered by both `SpaceInvite` and `RoomInvite`, gets a claim over
|
||||||
`createclaim`), and `InviteLink` renders the result for both `SpaceInvite` and `RoomInvite`; this replaced reading `RELAY_INVITE` events. Leaving (`SpaceExit`,
|
NIP-86 (`supportedmethods`, then `listclaims`, then `createclaim`), publishes a room invite code
|
||||||
|
for a room, and holds that state itself; this replaced reading `RELAY_INVITE` events. Leaving (`SpaceExit`,
|
||||||
`SpaceAuthError`) is `roomLists.removeRelay(url)` plus `publishLeaveRequest(url)`.
|
`SpaceAuthError`) is `roomLists.removeRelay(url)` plus `publishLeaveRequest(url)`.
|
||||||
|
|
||||||
## NIP-86 relay management
|
## NIP-86 relay management
|
||||||
|
|
@ -171,7 +175,7 @@ relay's URL, each call signed with a fresh NIP-98 event. Every method resolves t
|
||||||
| `banPubkey`, `unbanPubkey`, `allowPubkey`, `unallowPubkey`, `listBannedPubkeys` | `ProfileDetail`, `SpaceMemberMenu`, `SpaceMemberBannedMenu`, `ReportMenu`, `allowPubkeys` (`SpaceMembersAdd`, `RoleAddMembers`, `addRoomMembers`) |
|
| `banPubkey`, `unbanPubkey`, `allowPubkey`, `unallowPubkey`, `listBannedPubkeys` | `ProfileDetail`, `SpaceMemberMenu`, `SpaceMemberBannedMenu`, `ReportMenu`, `allowPubkeys` (`SpaceMembersAdd`, `RoleAddMembers`, `addRoomMembers`) |
|
||||||
| `banEvent` | `RoomItemMenu`, `EventMenu`, `ReportMenu`, `RoomJoinItem` (dismissing a join request) |
|
| `banEvent` | `RoomItemMenu`, `EventMenu`, `ReportMenu`, `RoomJoinItem` (dismissing a join request) |
|
||||||
| `createRole`, `editRole`, `deleteRole`, `assignRole`, `unassignRole` | `RoleCreate`, `RoleEdit`, `SpaceRoleMenu`, `SpaceMemberRoles` (via `syncAssignments`), `RoleAddMembers` |
|
| `createRole`, `editRole`, `deleteRole`, `assignRole`, `unassignRole` | `RoleCreate`, `RoleEdit`, `SpaceRoleMenu`, `SpaceMemberRoles` (via `syncAssignments`), `RoleAddMembers` |
|
||||||
| `listClaims`, `createClaim` | `Access.prepareInvite` |
|
| `listClaims`, `createClaim` | `InviteLink` |
|
||||||
| `changeRelayName`, `changeRelayDescription`, `changeRelayIcon` | `SpaceEdit` |
|
| `changeRelayName`, `changeRelayDescription`, `changeRelayIcon` | `SpaceEdit` |
|
||||||
|
|
||||||
A relay answers `supportedmethods` with what the authenticated pubkey may call, so every control
|
A relay answers `supportedmethods` with what the authenticated pubkey may call, so every control
|
||||||
|
|
@ -179,7 +183,7 @@ is gated on the method behind it through `deriveSpacePermissions(url)`, a store
|
||||||
(`$permissions.ban`, `$permissions.editRoles`). The composite gates a parent uses to decide whether
|
(`$permissions.ban`, `$permissions.editRoles`). The composite gates a parent uses to decide whether
|
||||||
to render a menu (`memberMenu`, `roleMenu`, `bannedMenu`, `directoryMenu`) are defined there too,
|
to render a menu (`memberMenu`, `roleMenu`, `bannedMenu`, `directoryMenu`) are defined there too,
|
||||||
so parent and menu can't disagree. The `SpaceManagement` plugin (`spaceManagement`) caches the
|
so parent and menu can't disagree. The `SpaceManagement` plugin (`spaceManagement`) caches the
|
||||||
methods per app, re-checked at most every five minutes per URL, and `Access.prepareInvite` reads
|
methods per app, re-checked at most every five minutes per URL, and `InviteLink` reads
|
||||||
them through `loadSupportedMethods`. It also holds the admin and ban lists, which
|
them through `loadSupportedMethods`. It also holds the admin and ban lists, which
|
||||||
`loadMethodAssignees`/`loadBannedPubkeys` refresh after a change. Still handle an error from the
|
`loadMethodAssignees`/`loadBannedPubkeys` refresh after a change. Still handle an error from the
|
||||||
call, since a listed method can be refused for a particular event or target.
|
call, since a listed method can be refused for a particular event or target.
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,7 @@ its composer has gone.
|
||||||
## Background sync (`src/app/sync.ts`)
|
## Background sync (`src/app/sync.ts`)
|
||||||
|
|
||||||
The root layout calls `syncApplicationData()` once the session is restored and storage is ready,
|
The root layout calls `syncApplicationData()` once the session is restored and storage is ready,
|
||||||
and again after every app swap. `Access.completeJoin` calls it after a space is joined. Each
|
and again after every app swap. `completeSpaceJoin` in `access.ts` calls it after a space is joined. Each
|
||||||
call tears down the previous run.
|
call tears down the previous run.
|
||||||
|
|
||||||
- `syncRelays` loads NIP-11 for the indexer relays, the current route's relay and the user's
|
- `syncRelays` loads NIP-11 for the indexer relays, the current route's relay and the user's
|
||||||
|
|
@ -410,9 +410,8 @@ NIP-86 calls (`relayManagement.get().forUrl(url)`) are not thunks. They return
|
||||||
- **Singleton session state.** `call.ts` keeps call state in plain writables (`callState`,
|
- **Singleton session state.** `call.ts` keeps call state in plain writables (`callState`,
|
||||||
`currentCallSession`, …).
|
`currentCallSession`, …).
|
||||||
- **UI signals.** `toast` in `toast.ts`, and `relaysPendingTrust` in `policies.ts`.
|
- **UI signals.** `toast` in `toast.ts`, and `relaysPendingTrust` in `policies.ts`.
|
||||||
- **A controller per flow.** `Access` (`access.ts`) and `Nip46Controller` (`nip46.ts`) are
|
- **A controller per flow.** `Nip46Controller` (`nip46.ts`) is a class a component instantiates.
|
||||||
classes a component instantiates (`new Access(url)`). They hold the writables and actions for
|
It holds the writables and actions for a multi-step flow.
|
||||||
a multi-step flow.
|
|
||||||
- **Module-owned values.** `wallet` in `lightning.ts` is a `withGetter(writable(...))` that the
|
- **Module-owned values.** `wallet` in `lightning.ts` is a `withGetter(writable(...))` that the
|
||||||
root layout persists.
|
root layout persists.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import {derived, writable, type Readable} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {dissoc, fromPairs, last, poll, randomId, sleep, tryCatch} from "@welshman/lib"
|
import {dissoc, fromPairs, last, poll, randomId, tryCatch} from "@welshman/lib"
|
||||||
import {AuthStatus, SocketStatus} from "@welshman/net"
|
import {AuthStatus, RelayReasonPrefix, SocketStatus, matchReason} from "@welshman/net"
|
||||||
import type {Socket} from "@welshman/net"
|
import type {Socket} from "@welshman/net"
|
||||||
import {
|
import {
|
||||||
displayRelayUrl,
|
displayRelayUrl,
|
||||||
|
|
@ -16,13 +16,12 @@ import {
|
||||||
import {RelayJoin, RelayLeave, RoomJoin, RoomLeave} from "@welshman/domain"
|
import {RelayJoin, RelayLeave, RoomJoin, RoomLeave} from "@welshman/domain"
|
||||||
import {Sync, User, publish} from "@welshman/app"
|
import {Sync, User, publish} from "@welshman/app"
|
||||||
import {stripPrefix} from "@lib/util"
|
import {stripPrefix} from "@lib/util"
|
||||||
import {app, command, relayManagement, roomLists, thunks, writer} from "@app/core"
|
import {app, command, roomLists, thunks, writer} from "@app/core"
|
||||||
import {PLATFORM_URL} from "@app/env"
|
import {PLATFORM_URL} from "@app/env"
|
||||||
import {spaceManagement} from "@app/management"
|
|
||||||
import {relaysMostlyRestricted} from "@app/policies"
|
import {relaysMostlyRestricted} from "@app/policies"
|
||||||
import {Push} from "@app/push"
|
import {optInSpaceNotifications} from "@app/push"
|
||||||
import {deriveSocket} from "@app/relays"
|
import {deriveSocket} from "@app/relays"
|
||||||
import {notificationSettings, removeTrustedRelay, setSpaceNotifications} from "@app/settings"
|
import {removeTrustedRelay} from "@app/settings"
|
||||||
import {syncApplicationData} from "@app/sync"
|
import {syncApplicationData} from "@app/sync"
|
||||||
|
|
||||||
export const ROOM_CREATE_INVITE = 9009
|
export const ROOM_CREATE_INVITE = 9009
|
||||||
|
|
@ -34,12 +33,6 @@ export type InviteData = {
|
||||||
code?: string
|
code?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isNetworkAuthError = (error: string) => {
|
|
||||||
const lower = error.toLowerCase()
|
|
||||||
|
|
||||||
return lower.includes("failed") || lower.includes("timeout") || lower.includes("network")
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseInviteLink = (invite: string): InviteData | undefined => {
|
export const parseInviteLink = (invite: string): InviteData | undefined => {
|
||||||
if (invite.length < 3 || !invite.includes(".")) {
|
if (invite.length < 3 || !invite.includes(".")) {
|
||||||
return
|
return
|
||||||
|
|
@ -83,7 +76,7 @@ export const makeInviteLink = (data: InviteData) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shouldIgnoreError = (error: string) => {
|
export const shouldIgnoreError = (error: string) => {
|
||||||
const isIgnored = error.startsWith("mute: ")
|
const isIgnored = matchReason(RelayReasonPrefix.Mute, error)
|
||||||
const isAborted = error.includes("Signing was aborted")
|
const isAborted = error.includes("Signing was aborted")
|
||||||
const isStrictNip29Relay = error.includes("missing group (`h`) tag")
|
const isStrictNip29Relay = error.includes("missing group (`h`) tag")
|
||||||
|
|
||||||
|
|
@ -249,177 +242,39 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
|
||||||
return stripPrefix(error)
|
return stripPrefix(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Access {
|
export const completeSpaceJoin = async (url: string, notifications: boolean) => {
|
||||||
url: string
|
await optInSpaceNotifications(url, notifications)
|
||||||
authError: Readable<string | undefined>
|
await roomLists.get().addRelay(url).then(publish)
|
||||||
loading = writable(true)
|
relaysMostlyRestricted.update(dissoc(url))
|
||||||
claim = writable("")
|
syncApplicationData()
|
||||||
claimFailed = writable(false)
|
app
|
||||||
roomCode = writable<string | undefined>()
|
.get()
|
||||||
roomInviteError = writable<string | undefined>()
|
.use(Sync)
|
||||||
isExplicitAuthError: Readable<boolean>
|
.push({
|
||||||
isGenericError: Readable<boolean>
|
relays: [url],
|
||||||
|
filters: [
|
||||||
constructor(url: string) {
|
{
|
||||||
this.url = url
|
kinds: [RELAYS, MESSAGING_RELAYS, FOLLOWS, PROFILE],
|
||||||
this.authError = deriveRelayAuthError(url)
|
authors: [User.require(app.get()).pubkey],
|
||||||
this.isExplicitAuthError = derived([this.authError], ([$authError]) =>
|
},
|
||||||
Boolean($authError && !isNetworkAuthError($authError)),
|
|
||||||
)
|
|
||||||
this.isGenericError = derived([this.authError], ([$authError]) =>
|
|
||||||
Boolean($authError && isNetworkAuthError($authError)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
createInviteStatus(requireCode: boolean) {
|
|
||||||
return derived(
|
|
||||||
[
|
|
||||||
this.loading,
|
|
||||||
this.isGenericError,
|
|
||||||
this.isExplicitAuthError,
|
|
||||||
this.roomInviteError,
|
|
||||||
this.roomCode,
|
|
||||||
this.claimFailed,
|
|
||||||
],
|
],
|
||||||
([$loading, $isGeneric, $isExplicit, $roomInviteError, $roomCode, $claimFailed]) => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
if ($claimFailed) {
|
|
||||||
return "noclaim" 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 roomLists.get().addRelay(this.url).then(publish)
|
|
||||||
this.clearRestricted()
|
|
||||||
syncApplicationData()
|
|
||||||
app
|
|
||||||
.get()
|
|
||||||
.use(Sync)
|
|
||||||
.push({
|
|
||||||
relays: [this.url],
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
kinds: [RELAYS, MESSAGING_RELAYS, FOLLOWS, PROFILE],
|
|
||||||
authors: [User.require(app.get()).pubkey],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 acceptInvite(data: InviteData, notifications: boolean) {
|
|
||||||
const spaceUrls = roomLists.get().urls(User.require(app.get()).pubkey).get()
|
|
||||||
const error = await this.joinSpace({
|
|
||||||
claim: data.claim,
|
|
||||||
notifications,
|
|
||||||
alreadyJoined: spaceUrls.includes(data.url),
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const acceptInvite = async (data: InviteData, notifications: boolean) => {
|
||||||
|
const spaceUrls = roomLists.get().urls(User.require(app.get()).pubkey).get()
|
||||||
|
|
||||||
|
if (!spaceUrls.includes(data.url)) {
|
||||||
|
const error = await attemptRelayAccess(data.url, data.claim)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.h && data.code) {
|
await completeSpaceJoin(data.url, notifications)
|
||||||
return joinRoom(this.url, data.h, data.code)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async prepareInvite(h?: string) {
|
if (data.h && data.code) {
|
||||||
this.loading.set(true)
|
return joinRoom(data.url, data.h, data.code)
|
||||||
|
|
||||||
try {
|
|
||||||
const management = relayManagement.get().forUrl(this.url)
|
|
||||||
|
|
||||||
const [methods, roomInviteResult] = await Promise.all([
|
|
||||||
spaceManagement.get().loadSupportedMethods(this.url),
|
|
||||||
h ? publishRoomInvite(this.url, h) : Promise.resolve({code: undefined, error: undefined}),
|
|
||||||
// Keep the spinner up long enough that a fast relay doesn't make it flash
|
|
||||||
sleep(300),
|
|
||||||
])
|
|
||||||
|
|
||||||
// A relay reporting methods relay-wide can still come back "blocked" for this user.
|
|
||||||
if (methods.includes("createclaim")) {
|
|
||||||
const {result: claims} = await management.listClaims()
|
|
||||||
|
|
||||||
if (claims?.[0]) {
|
|
||||||
this.claim.set(claims[0])
|
|
||||||
} else {
|
|
||||||
const claim = randomId()
|
|
||||||
const {error} = await management.createClaim(claim)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
this.claimFailed.set(true)
|
|
||||||
} else {
|
|
||||||
this.claim.set(claim)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (h) {
|
|
||||||
this.roomCode.set(roomInviteResult.code)
|
|
||||||
this.roomInviteError.set(roomInviteResult.error)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.loading.set(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {Share} from "@capacitor/share"
|
import {Share} from "@capacitor/share"
|
||||||
|
import {randomId, sleep} from "@welshman/lib"
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
import Upload from "@assets/icons/upload.svg?dataurl"
|
import Upload from "@assets/icons/upload.svg?dataurl"
|
||||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||||
|
|
@ -9,8 +10,10 @@
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import QRCode from "@app/components/QRCode.svelte"
|
import QRCode from "@app/components/QRCode.svelte"
|
||||||
import {Access, makeInviteLink} from "@app/access"
|
import {deriveRelayAuthError, makeInviteLink, publishRoomInvite} from "@app/access"
|
||||||
|
import {relayManagement} from "@app/core"
|
||||||
import {PLATFORM_NAME} from "@app/env"
|
import {PLATFORM_NAME} from "@app/env"
|
||||||
|
import {spaceManagement} from "@app/management"
|
||||||
import {clip} from "@app/toast"
|
import {clip} from "@app/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -20,10 +23,7 @@
|
||||||
|
|
||||||
const {url, h}: Props = $props()
|
const {url, h}: Props = $props()
|
||||||
|
|
||||||
const access = new Access(url)
|
const authError = deriveRelayAuthError(url)
|
||||||
const {claim, loading, roomCode} = access
|
|
||||||
const inviteStatus = access.createInviteStatus(Boolean(h))
|
|
||||||
const invite = $derived(makeInviteLink({url, claim: $claim, h, code: $roomCode}))
|
|
||||||
|
|
||||||
const copyInvite = () => clip(invite)
|
const copyInvite = () => clip(invite)
|
||||||
|
|
||||||
|
|
@ -35,8 +35,51 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prepareInvite = async () => {
|
||||||
|
try {
|
||||||
|
const management = relayManagement.get().forUrl(url)
|
||||||
|
|
||||||
|
const [methods, roomInvite] = await Promise.all([
|
||||||
|
spaceManagement.get().loadSupportedMethods(url),
|
||||||
|
h ? publishRoomInvite(url, h) : Promise.resolve({code: undefined, error: undefined}),
|
||||||
|
// Keep the spinner up long enough that a fast relay doesn't make it flash
|
||||||
|
sleep(300),
|
||||||
|
])
|
||||||
|
|
||||||
|
// A relay reporting methods relay-wide can still come back "blocked" for this user.
|
||||||
|
if (methods.includes("createclaim")) {
|
||||||
|
const {result: claims} = await management.listClaims()
|
||||||
|
|
||||||
|
if (claims?.[0]) {
|
||||||
|
claim = claims[0]
|
||||||
|
} else {
|
||||||
|
const newClaim = randomId()
|
||||||
|
const {error} = await management.createClaim(newClaim)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
claimFailed = true
|
||||||
|
} else {
|
||||||
|
claim = newClaim
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roomCode = roomInvite.code
|
||||||
|
roomInviteError = roomInvite.error
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(true)
|
||||||
|
let claim = $state("")
|
||||||
|
let claimFailed = $state(false)
|
||||||
|
let roomCode = $state<string>()
|
||||||
|
let roomInviteError = $state<string>()
|
||||||
let canShare = $state(false)
|
let canShare = $state(false)
|
||||||
|
|
||||||
|
const invite = $derived(makeInviteLink({url, claim, h, code: roomCode}))
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
const {value} = await Share.canShare()
|
const {value} = await Share.canShare()
|
||||||
|
|
@ -45,20 +88,20 @@
|
||||||
canShare = false
|
canShare = false
|
||||||
}
|
}
|
||||||
|
|
||||||
await access.prepareInvite(h)
|
await prepareInvite()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#if $inviteStatus === "loading"}
|
{#if loading}
|
||||||
<p class="flex justify-center items-center">
|
<p class="flex justify-center items-center">
|
||||||
<Spinner loading={$loading}>Requesting an invite link...</Spinner>
|
<Spinner {loading}>Requesting an invite link...</Spinner>
|
||||||
</p>
|
</p>
|
||||||
{:else if $inviteStatus === "network"}
|
{:else if $authError && /failed|timeout|network/i.test($authError)}
|
||||||
<p class="flex justify-center items-center">
|
<p class="flex justify-center items-center">
|
||||||
Unable to reach the space. Please check your connection and try again.
|
Unable to reach the space. Please check your connection and try again.
|
||||||
</p>
|
</p>
|
||||||
{:else if $inviteStatus === "auth"}
|
{:else if $authError || roomInviteError}
|
||||||
<p class="flex justify-center items-center">
|
<p class="flex justify-center items-center">
|
||||||
{#if h}
|
{#if h}
|
||||||
Oops! It looks like you're not allowed to create invites for this room.
|
Oops! It looks like you're not allowed to create invites for this room.
|
||||||
|
|
@ -66,7 +109,7 @@
|
||||||
Oops! It looks like you're not a member of this space.
|
Oops! It looks like you're not a member of this space.
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
{:else if $inviteStatus === "failed"}
|
{:else if h && !roomCode}
|
||||||
<p class="flex justify-center items-center">Unable to create a room invite code.</p>
|
<p class="flex justify-center items-center">Unable to create a room invite code.</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center gap-6">
|
<div class="flex flex-col items-center gap-6">
|
||||||
|
|
@ -101,10 +144,10 @@
|
||||||
{:else}
|
{:else}
|
||||||
This invite link can be used by clicking "Add Space" and pasting it there.
|
This invite link can be used by clicking "Add Space" and pasting it there.
|
||||||
{/if}
|
{/if}
|
||||||
{#if $inviteStatus === "noclaim"}
|
{#if claimFailed}
|
||||||
We weren't able to get an invite code from this space, so additional steps might be
|
We weren't able to get an invite code from this space, so additional steps might be
|
||||||
required.
|
required.
|
||||||
{:else if !$claim}
|
{:else if !claim}
|
||||||
This space did not issue a claim for this link, so additional steps might be required.
|
This space did not issue a claim for this link, so additional steps might be required.
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {Access, parseInviteLink} from "@app/access"
|
import {attemptRelayAccess, parseInviteLink} from "@app/access"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -24,8 +24,6 @@
|
||||||
|
|
||||||
const {url, callback}: Props = $props()
|
const {url, callback}: Props = $props()
|
||||||
|
|
||||||
const access = new Access(url)
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const join = async () => {
|
const join = async () => {
|
||||||
|
|
@ -33,7 +31,7 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const claim = parseInviteLink(value)?.claim || value
|
const claim = parseInviteLink(value)?.claim || value
|
||||||
const message = await access.attempt(claim)
|
const message = await attemptRelayAccess(url, claim)
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
return pushToast({theme: "error", message, timeout: 30_000})
|
return pushToast({theme: "error", message, timeout: 30_000})
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
import SpaceJoinStatus from "@app/components/SpaceJoinStatus.svelte"
|
import SpaceJoinStatus from "@app/components/SpaceJoinStatus.svelte"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {goToSpace, makeRoomPath} from "@app/routes"
|
import {goToSpace, makeRoomPath} from "@app/routes"
|
||||||
import {Access, parseInviteLink} from "@app/access"
|
import {acceptInvite, parseInviteLink} from "@app/access"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
invite: string
|
invite: string
|
||||||
|
|
@ -45,9 +45,7 @@
|
||||||
|
|
||||||
const joinRelay = async () => {
|
const joinRelay = async () => {
|
||||||
const data = inviteData!
|
const data = inviteData!
|
||||||
const access = new Access(data.url)
|
error = await acceptInvite(data, notifications)
|
||||||
|
|
||||||
error = await access.acceptInvite(data, notifications)
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte"
|
import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte"
|
||||||
import SpaceJoinNotifications from "@app/components/SpaceJoinNotifications.svelte"
|
import SpaceJoinNotifications from "@app/components/SpaceJoinNotifications.svelte"
|
||||||
import SpaceJoinStatus from "@app/components/SpaceJoinStatus.svelte"
|
import SpaceJoinStatus from "@app/components/SpaceJoinStatus.svelte"
|
||||||
import {Access} from "@app/access"
|
import {attemptRelayAccess, completeSpaceJoin} from "@app/access"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {goToSpace} from "@app/routes"
|
import {goToSpace} from "@app/routes"
|
||||||
|
|
@ -25,8 +25,6 @@
|
||||||
|
|
||||||
const {url}: Props = $props()
|
const {url}: Props = $props()
|
||||||
|
|
||||||
const access = new Access(url)
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const join = async () => {
|
const join = async () => {
|
||||||
|
|
@ -37,7 +35,7 @@
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await access.completeJoin(notifications)
|
await completeSpaceJoin(url, notifications)
|
||||||
|
|
||||||
pushToast({message: "Welcome to the space!"})
|
pushToast({message: "Welcome to the space!"})
|
||||||
await goToSpace(url, {replaceState: true})
|
await goToSpace(url, {replaceState: true})
|
||||||
|
|
@ -54,7 +52,7 @@
|
||||||
let notifications = $state(true)
|
let notifications = $state(true)
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
error = await access.attempt()
|
error = await attemptRelayAccess(url)
|
||||||
loading = false
|
loading = false
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import {Capacitor} from "@capacitor/core"
|
import {Capacitor} from "@capacitor/core"
|
||||||
import {notificationSettings} from "@app/settings"
|
import {notificationSettings, setSpaceNotifications} from "@app/settings"
|
||||||
import {pushState} from "@app/push/adapters/common"
|
import {pushState} from "@app/push/adapters/common"
|
||||||
import {ElectronNotifications} from "@app/push/adapters/electron"
|
import {ElectronNotifications} from "@app/push/adapters/electron"
|
||||||
import {WebNotifications} from "@app/push/adapters/web"
|
import {WebNotifications} from "@app/push/adapters/web"
|
||||||
|
|
@ -64,3 +64,15 @@ export class Push {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const optInSpaceNotifications = async (url: string, notify: boolean) => {
|
||||||
|
if (notify && notificationSettings.get().push) {
|
||||||
|
const permission = await Push.request()
|
||||||
|
|
||||||
|
if (permission.startsWith("granted")) {
|
||||||
|
await setSpaceNotifications(url, true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await setSpaceNotifications(url, notify)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue