Share the invite link UI through InviteLink, move adding members directly into its own modal, and diff role and method assignments with one helper
This commit is contained in:
parent
0eebfc7dc1
commit
97d1b1dc49
12 changed files with 319 additions and 379 deletions
|
|
@ -156,7 +156,7 @@ Invite links are `${PLATFORM_URL}/join?r=<relay>&c=<claim>`, plus `h` and `code`
|
||||||
(`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 `Access.acceptInvite` to join the space and then the room.
|
||||||
`Access.prepareInvite` gets a claim over NIP-86 (`supportedmethods`, then `listclaims`, then
|
`Access.prepareInvite` gets a claim over NIP-86 (`supportedmethods`, then `listclaims`, then
|
||||||
`createclaim`); this replaced reading `RELAY_INVITE` events. Leaving (`SpaceExit`,
|
`createclaim`), and `InviteLink` renders the result for both `SpaceInvite` and `RoomInvite`; 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
|
||||||
|
|
@ -168,9 +168,9 @@ relay's URL, each call signed with a fresh NIP-98 event. Every method resolves t
|
||||||
| Methods | Called from |
|
| Methods | Called from |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `supportedMethods`, `listMethodAssignees`, `listBannedPubkeys` (cached) | `SpaceManagement` (`src/app/management.ts`) |
|
| `supportedMethods`, `listMethodAssignees`, `listBannedPubkeys` (cached) | `SpaceManagement` (`src/app/management.ts`) |
|
||||||
| `banPubkey`, `unbanPubkey`, `allowPubkey`, `unallowPubkey`, `listBannedPubkeys` | `ProfileDetail`, `SpaceMemberMenu`, `SpaceMemberBannedMenu`, `SpaceInvite`, `ReportMenuList`, `addRoomMembers` |
|
| `banPubkey`, `unbanPubkey`, `allowPubkey`, `unallowPubkey`, `listBannedPubkeys` | `ProfileDetail`, `SpaceMemberMenu`, `SpaceMemberBannedMenu`, `ReportMenuList`, `allowPubkeys` (`SpaceMembersAdd`, `RoleAddMembers`, `addRoomMembers`) |
|
||||||
| `banEvent` | `RoomItemMenu`, `EventMenu`, `ReportMenuList`, `RoomJoinItem` (dismissing a join request) |
|
| `banEvent` | `RoomItemMenu`, `EventMenu`, `ReportMenuList`, `RoomJoinItem` (dismissing a join request) |
|
||||||
| `createRole`, `editRole`, `deleteRole`, `assignRole`, `unassignRole` | `RoleCreate`, `RoleEdit`, `SpaceRoleMenu`, `SpaceMemberRoles`, `RoleAddMembers` |
|
| `createRole`, `editRole`, `deleteRole`, `assignRole`, `unassignRole` | `RoleCreate`, `RoleEdit`, `SpaceRoleMenu`, `SpaceMemberRoles` (via `syncAssignments`), `RoleAddMembers` |
|
||||||
| `listClaims`, `createClaim` | `Access.prepareInvite` |
|
| `listClaims`, `createClaim` | `Access.prepareInvite` |
|
||||||
| `changeRelayName`, `changeRelayDescription`, `changeRelayIcon` | `SpaceEdit` |
|
| `changeRelayName`, `changeRelayDescription`, `changeRelayIcon` | `SpaceEdit` |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1469,8 +1469,8 @@ Acceptance:
|
||||||
|
|
||||||
- "Create Invite" shows a QR code and a copyable link, and copying puts it on
|
- "Create Invite" shows a QR code and a copyable link, and copying puts it on
|
||||||
the clipboard.
|
the clipboard.
|
||||||
- Where the relay supports it, the invite dialog's member search adds someone
|
- Where the relay supports it, "Add members" in the invite dialog opens a member
|
||||||
straight into the member list.
|
search that adds someone straight into the member list.
|
||||||
- A room-scoped invite link carries both the space and that room's join code.
|
- A room-scoped invite link carries both the space and that room's join code.
|
||||||
|
|
||||||
### US-095 — Remove, ban, and restore members
|
### US-095 — Remove, ban, and restore members
|
||||||
|
|
|
||||||
|
|
@ -281,9 +281,13 @@ test("US-094 invite people to a space", async ({seed, as}) => {
|
||||||
|
|
||||||
await expect(invite.locator("input[readonly]")).toHaveValue(/\/join\?r=space\.test&c=.+/)
|
await expect(invite.locator("input[readonly]")).toHaveValue(/\/join\?r=space\.test&c=.+/)
|
||||||
|
|
||||||
await invite.getByPlaceholder("Search for profiles...").fill("Nadia")
|
await invite.getByRole("button", {name: "Add members"}).click()
|
||||||
|
|
||||||
|
const addMembers = dialog(page, "Add Members")
|
||||||
|
|
||||||
|
await addMembers.getByPlaceholder("Search for profiles...").fill("Nadia")
|
||||||
await page.locator(".tiptap-suggestions").getByRole("button", {name: newcomer.pubkey}).click()
|
await page.locator(".tiptap-suggestions").getByRole("button", {name: newcomer.pubkey}).click()
|
||||||
await invite.getByRole("button", {name: "Save", exact: true}).click()
|
await addMembers.getByRole("button", {name: "Save", exact: true}).click()
|
||||||
|
|
||||||
await expect(page.getByRole("alert")).toContainText("Members have successfully been added!")
|
await expect(page.getByRole("alert")).toContainText("Members have successfully been added!")
|
||||||
|
|
||||||
|
|
|
||||||
115
src/app/components/InviteLink.svelte
Normal file
115
src/app/components/InviteLink.svelte
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import {Share} from "@capacitor/share"
|
||||||
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
|
import Upload from "@assets/icons/upload.svg?dataurl"
|
||||||
|
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import QRCode from "@app/components/QRCode.svelte"
|
||||||
|
import {Access, makeInviteLink} from "@app/access"
|
||||||
|
import {PLATFORM_NAME} from "@app/env"
|
||||||
|
import {clip} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
h?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h}: Props = $props()
|
||||||
|
|
||||||
|
const access = new Access(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 shareInvite = async () => {
|
||||||
|
try {
|
||||||
|
await Share.share({url: invite})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let canShare = $state(false)
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
const {value} = await Share.canShare()
|
||||||
|
canShare = value
|
||||||
|
} catch {
|
||||||
|
canShare = false
|
||||||
|
}
|
||||||
|
|
||||||
|
await access.prepareInvite(h)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if $inviteStatus === "loading"}
|
||||||
|
<p class="flex justify-center items-center">
|
||||||
|
<Spinner loading={$loading}>Requesting an invite link...</Spinner>
|
||||||
|
</p>
|
||||||
|
{:else if $inviteStatus === "network"}
|
||||||
|
<p class="flex justify-center items-center">
|
||||||
|
Unable to reach the space. Please check your connection and try again.
|
||||||
|
</p>
|
||||||
|
{:else if $inviteStatus === "auth"}
|
||||||
|
<p class="flex justify-center items-center">
|
||||||
|
{#if h}
|
||||||
|
Oops! It looks like you're not allowed to create invites for this room.
|
||||||
|
{:else}
|
||||||
|
Oops! It looks like you're not a member of this space.
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
{:else if $inviteStatus === "failed"}
|
||||||
|
<p class="flex justify-center items-center">Unable to create a room invite code.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col items-center gap-6">
|
||||||
|
<div class="w-48">
|
||||||
|
<QRCode code={invite} />
|
||||||
|
</div>
|
||||||
|
<Field>
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="flex w-full gap-2">
|
||||||
|
{#if canShare}
|
||||||
|
<Button
|
||||||
|
class="input flex w-12 shrink-0 items-center justify-center p-0"
|
||||||
|
onclick={shareInvite}>
|
||||||
|
<Icon icon={Upload} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<label class="input flex min-w-0 flex-1 items-center gap-2">
|
||||||
|
<Icon icon={LinkRound} class="shrink-0" />
|
||||||
|
<input value={invite} class="min-w-0 flex-1 truncate" type="text" readonly />
|
||||||
|
<Button class="shrink-0" onclick={copyInvite}>
|
||||||
|
<Icon icon={Copy} />
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>
|
||||||
|
{#if h}
|
||||||
|
This invite link includes access to the space and room. Anyone with the link can join
|
||||||
|
by opening it in {PLATFORM_NAME}.
|
||||||
|
{:else}
|
||||||
|
This invite link can be used by clicking "Add Space" and pasting it there.
|
||||||
|
{/if}
|
||||||
|
{#if $inviteStatus === "noclaim"}
|
||||||
|
We weren't able to get an invite code from this space, so additional steps might be
|
||||||
|
required.
|
||||||
|
{:else if !$claim}
|
||||||
|
This space did not issue a claim for this link, so additional steps might be required.
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
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 ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
||||||
import {relayManagement, relayMemberLists} from "@app/core"
|
import {relayManagement} from "@app/core"
|
||||||
|
import {allowPubkeys} from "@app/management"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -24,41 +25,34 @@
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
let loading = $state(false)
|
|
||||||
let pubkeys: string[] = $state([])
|
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const members = $relayMemberLists.get(url)
|
|
||||||
const management = $relayManagement.forUrl(url)
|
const management = $relayManagement.forUrl(url)
|
||||||
|
|
||||||
|
// Members have to be allowed at the relay before a role there means anything
|
||||||
|
let error = await allowPubkeys(url, pubkeys)
|
||||||
|
|
||||||
for (const pubkey of pubkeys) {
|
for (const pubkey of pubkeys) {
|
||||||
// Ensure they're space members first, then assign the role
|
if (!error) {
|
||||||
if (!members?.isMember(pubkey)) {
|
error = (await management.assignRole(pubkey, role.identifier()!)).error
|
||||||
const {error} = await management.allowPubkey(pubkey)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {error} = await management.assignRole(pubkey, role.identifier()!)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pushToast({message: "Members assigned!"})
|
if (error) {
|
||||||
back()
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Members assigned!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
let pubkeys: string[] = $state([])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import {Share} from "@capacitor/share"
|
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
|
||||||
import Upload from "@assets/icons/upload.svg?dataurl"
|
|
||||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import Field from "@lib/components/Field.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
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 QRCode from "@app/components/QRCode.svelte"
|
import InviteLink from "@app/components/InviteLink.svelte"
|
||||||
import RoomName from "@app/components/RoomName.svelte"
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
import {Access, makeInviteLink} from "@app/access"
|
|
||||||
import {PLATFORM_NAME} from "@app/env"
|
|
||||||
import {clip} from "@app/toast"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -28,38 +17,7 @@
|
||||||
|
|
||||||
const {url, h}: Props = $props()
|
const {url, h}: Props = $props()
|
||||||
|
|
||||||
const access = new Access(url)
|
|
||||||
const {claim, loading, roomCode} = access
|
|
||||||
const inviteStatus = access.createInviteStatus(true)
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
const copyInvite = () => clip(invite)
|
|
||||||
|
|
||||||
const shareInvite = async () => {
|
|
||||||
if (!canShare) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Share.share({url: invite})
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let canShare = $state(false)
|
|
||||||
let invite = $derived(makeInviteLink({url, claim: $claim, h, code: $roomCode}))
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
const {value} = await Share.canShare()
|
|
||||||
canShare = value
|
|
||||||
} catch {
|
|
||||||
canShare = false
|
|
||||||
}
|
|
||||||
|
|
||||||
await access.prepareInvite(h)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
|
|
@ -72,63 +30,7 @@
|
||||||
in <span class="text-primary">{displayRelayUrl(url)}</span>
|
in <span class="text-primary">{displayRelayUrl(url)}</span>
|
||||||
</ModalSubtitle>
|
</ModalSubtitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<div>
|
<InviteLink {url} {h} />
|
||||||
{#if $inviteStatus === "loading"}
|
|
||||||
<p class="flex justify-center items-center">
|
|
||||||
<Spinner loading={$loading}>Requesting an invite link...</Spinner>
|
|
||||||
</p>
|
|
||||||
{:else if $inviteStatus === "network"}
|
|
||||||
<p class="flex justify-center items-center">
|
|
||||||
Unable to reach the space. Please check your connection and try again.
|
|
||||||
</p>
|
|
||||||
{:else if $inviteStatus === "auth"}
|
|
||||||
<p class="flex justify-center items-center">
|
|
||||||
Oops! It looks like you're not allowed to create invites for this room.
|
|
||||||
</p>
|
|
||||||
{:else if $inviteStatus === "failed"}
|
|
||||||
<p class="flex justify-center items-center">Unable to create a room invite code.</p>
|
|
||||||
{:else}
|
|
||||||
<div class="flex flex-col items-center gap-6">
|
|
||||||
<div class="w-48">
|
|
||||||
<QRCode code={invite} />
|
|
||||||
</div>
|
|
||||||
<Field>
|
|
||||||
{#snippet input()}
|
|
||||||
<div class="flex w-full gap-2">
|
|
||||||
{#if canShare}
|
|
||||||
<Button
|
|
||||||
class="input flex w-12 shrink-0 items-center justify-center p-0"
|
|
||||||
onclick={shareInvite}>
|
|
||||||
<Icon icon={Upload} />
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<label class="input flex min-w-0 flex-1 items-center gap-2">
|
|
||||||
<Icon icon={LinkRound} class="shrink-0" />
|
|
||||||
<input bind:value={invite} class="min-w-0 flex-1 truncate" type="text" readonly />
|
|
||||||
<Button class="shrink-0" onclick={copyInvite}>
|
|
||||||
<Icon icon={Copy} />
|
|
||||||
</Button>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<p>
|
|
||||||
This invite link includes access to the space and room. Anyone with the link can
|
|
||||||
join by opening it in {PLATFORM_NAME}.
|
|
||||||
{#if $inviteStatus === "noclaim"}
|
|
||||||
We weren't able to get an invite code from this space, so additional steps might
|
|
||||||
be required.
|
|
||||||
{:else if !$claim}
|
|
||||||
This space did not issue a claim for this link, so additional steps might be
|
|
||||||
required.
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="button button-primary grow" onclick={back}>Done</Button>
|
<Button class="button button-primary grow" onclick={back}>Done</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
|
||||||
import {Share} from "@capacitor/share"
|
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
import UserPlus from "@assets/icons/user-plus.svg?dataurl"
|
||||||
import Upload from "@assets/icons/upload.svg?dataurl"
|
|
||||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import Field from "@lib/components/Field.svelte"
|
|
||||||
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 Divider from "@lib/components/Divider.svelte"
|
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
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 ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
import InviteLink from "@app/components/InviteLink.svelte"
|
||||||
|
import SpaceMembersAdd from "@app/components/SpaceMembersAdd.svelte"
|
||||||
import {deriveSpacePermissions} from "@app/management"
|
import {deriveSpacePermissions} from "@app/management"
|
||||||
import QRCode from "@app/components/QRCode.svelte"
|
import {pushModal} from "@app/modal"
|
||||||
import {Access, makeInviteLink} from "@app/access"
|
|
||||||
import {relayManagement, relayMemberLists} from "@app/core"
|
|
||||||
import {clip, pushToast} from "@app/toast"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -30,67 +20,11 @@
|
||||||
|
|
||||||
const {url}: Props = $props()
|
const {url}: Props = $props()
|
||||||
|
|
||||||
const access = new Access(url)
|
|
||||||
const {claim, loading} = access
|
|
||||||
const inviteStatus = access.createInviteStatus(false)
|
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
const copyInvite = () => clip(invite)
|
|
||||||
|
|
||||||
const shareInvite = async () => {
|
|
||||||
if (!canShare) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Share.share({url: invite})
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addMembers = async () => {
|
|
||||||
adding = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const members = $relayMemberLists.get(url)
|
|
||||||
const management = $relayManagement.forUrl(url)
|
|
||||||
|
|
||||||
const responses = await Promise.all(
|
|
||||||
pubkeys.filter(pk => !members?.isMember(pk)).map(pk => management.allowPubkey(pk)),
|
|
||||||
)
|
|
||||||
|
|
||||||
const error = responses.map(response => response.error).find(Boolean)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "Members have successfully been added!"})
|
|
||||||
back()
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
adding = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const permissions = deriveSpacePermissions(url)
|
const permissions = deriveSpacePermissions(url)
|
||||||
|
|
||||||
let canShare = $state(false)
|
const back = () => history.back()
|
||||||
let invite = $derived(makeInviteLink({url, claim: $claim}))
|
|
||||||
|
|
||||||
let adding = $state(false)
|
const addMembers = () => pushModal(SpaceMembersAdd, {url}, {replaceState: true})
|
||||||
let pubkeys: string[] = $state([])
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
const {value} = await Share.canShare()
|
|
||||||
canShare = value
|
|
||||||
} catch {
|
|
||||||
canShare = false
|
|
||||||
}
|
|
||||||
|
|
||||||
await access.prepareInvite()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
|
|
@ -102,84 +36,15 @@
|
||||||
<span class="text-primary">{displayRelayUrl(url)}</span>
|
<span class="text-primary">{displayRelayUrl(url)}</span>
|
||||||
</ModalSubtitle>
|
</ModalSubtitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<div>
|
<InviteLink {url} />
|
||||||
{#if $inviteStatus === "loading"}
|
|
||||||
<p class="flex justify-center items-center">
|
|
||||||
<Spinner loading={$loading}>Requesting an invite link...</Spinner>
|
|
||||||
</p>
|
|
||||||
{:else if $inviteStatus === "network"}
|
|
||||||
<p class="flex justify-center items-center">
|
|
||||||
Unable to reach the space. Please check your connection and try again.
|
|
||||||
</p>
|
|
||||||
{:else if $inviteStatus === "auth"}
|
|
||||||
<p class="flex justify-center items-center">
|
|
||||||
Oops! It looks like you're not a member of this space.
|
|
||||||
</p>
|
|
||||||
{:else}
|
|
||||||
<div class="flex flex-col items-center gap-6">
|
|
||||||
<div class="w-48">
|
|
||||||
<QRCode code={invite} />
|
|
||||||
</div>
|
|
||||||
<Field>
|
|
||||||
{#snippet input()}
|
|
||||||
<div class="flex w-full gap-2">
|
|
||||||
{#if canShare}
|
|
||||||
<Button
|
|
||||||
class="input flex w-12 shrink-0 items-center justify-center p-0"
|
|
||||||
onclick={shareInvite}>
|
|
||||||
<Icon icon={Upload} />
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<label class="input flex min-w-0 flex-1 items-center gap-2">
|
|
||||||
<Icon icon={LinkRound} class="shrink-0" />
|
|
||||||
<input bind:value={invite} class="min-w-0 flex-1 truncate" type="text" readonly />
|
|
||||||
<Button class="shrink-0" onclick={copyInvite}>
|
|
||||||
<Icon icon={Copy} />
|
|
||||||
</Button>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<p>
|
|
||||||
This invite link can be used by clicking "Add Space" and pasting it there.
|
|
||||||
{#if $inviteStatus === "noclaim"}
|
|
||||||
We weren't able to get an invite code from this space, so additional steps might
|
|
||||||
be required.
|
|
||||||
{:else if !$claim}
|
|
||||||
This space did not issue a claim for this link, so additional steps might be
|
|
||||||
required.
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if $permissions.addMembers}
|
|
||||||
<Divider>or</Divider>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Add members directly</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<ProfileMultiSelect bind:value={pubkeys} />
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
{/if}
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{#if $permissions.addMembers}
|
{#if $permissions.addMembers}
|
||||||
<Button class="button button-link" onclick={back}>
|
<Button class="button button-link" onclick={addMembers}>
|
||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={UserPlus} />
|
||||||
Go back
|
Add members
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
class="button button-primary"
|
|
||||||
onclick={addMembers}
|
|
||||||
disabled={adding || pubkeys.length === 0}>
|
|
||||||
<Spinner loading={adding}>Save</Spinner>
|
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button class="button button-primary" onclick={back}>Done</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button class="button button-primary grow" onclick={back}>Done</Button>
|
<Button class="button button-primary grow" onclick={back}>Done</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import {spec} from "@welshman/lib"
|
import {difference, spec} from "@welshman/lib"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
|
@ -13,7 +13,12 @@
|
||||||
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 {profiles, relayManagement} from "@app/core"
|
import {profiles, relayManagement} from "@app/core"
|
||||||
import {MANAGEMENT_METHOD_GROUPS, deriveSpacePermissions, spaceManagement} from "@app/management"
|
import {
|
||||||
|
MANAGEMENT_METHOD_GROUPS,
|
||||||
|
deriveSpacePermissions,
|
||||||
|
spaceManagement,
|
||||||
|
syncAssignments,
|
||||||
|
} from "@app/management"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -29,7 +34,7 @@
|
||||||
// A relay can grant methods this client doesn't know, and leaving them out would revoke them.
|
// A relay can grant methods this client doesn't know, and leaving them out would revoke them.
|
||||||
const groups = $derived.by(() => {
|
const groups = $derived.by(() => {
|
||||||
const known = MANAGEMENT_METHOD_GROUPS.flatMap(group => group.methods.map(({method}) => method))
|
const known = MANAGEMENT_METHOD_GROUPS.flatMap(group => group.methods.map(({method}) => method))
|
||||||
const extra = [...assigned].filter(method => !known.includes(method))
|
const extra = difference(assigned, known)
|
||||||
|
|
||||||
if (extra.length > 0) {
|
if (extra.length > 0) {
|
||||||
return [
|
return [
|
||||||
|
|
@ -43,57 +48,33 @@
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const toggle = (method: string) => {
|
|
||||||
const next = new Set(selected)
|
|
||||||
|
|
||||||
if (next.has(method)) {
|
|
||||||
next.delete(method)
|
|
||||||
} else {
|
|
||||||
next.add(method)
|
|
||||||
}
|
|
||||||
|
|
||||||
selected = next
|
|
||||||
}
|
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
saving = true
|
saving = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const management = $relayManagement.forUrl(url)
|
const management = $relayManagement.forUrl(url)
|
||||||
|
const error = await syncAssignments(
|
||||||
|
assigned,
|
||||||
|
selected,
|
||||||
|
method => management.assignMethod(pubkey, method),
|
||||||
|
method => management.unassignMethod(pubkey, method),
|
||||||
|
)
|
||||||
|
|
||||||
for (const method of selected) {
|
if (error) {
|
||||||
if (!assigned.has(method)) {
|
pushToast({theme: "error", message: error})
|
||||||
const {error} = await management.assignMethod(pubkey, method)
|
} else {
|
||||||
|
await $spaceManagement.loadMethodAssignees(url)
|
||||||
|
|
||||||
if (error) {
|
pushToast({message: "Permissions updated!"})
|
||||||
pushToast({theme: "error", message: error})
|
back()
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const method of assigned) {
|
|
||||||
if (!selected.has(method)) {
|
|
||||||
const {error} = await management.unassignMethod(pubkey, method)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await $spaceManagement.loadMethodAssignees(url)
|
|
||||||
|
|
||||||
pushToast({message: "Permissions updated!"})
|
|
||||||
back()
|
|
||||||
} finally {
|
} finally {
|
||||||
saving = false
|
saving = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let assigned = $state(new Set<string>())
|
let assigned: string[] = $state([])
|
||||||
let selected = $state(new Set<string>())
|
let selected: string[] = $state([])
|
||||||
let loading = $state(true)
|
let loading = $state(true)
|
||||||
let saving = $state(false)
|
let saving = $state(false)
|
||||||
|
|
||||||
|
|
@ -104,10 +85,8 @@
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
}
|
}
|
||||||
|
|
||||||
assigned = new Set(
|
assigned = get($spaceManagement.methodAssignees(url)).find(spec({pubkey}))?.methods ?? []
|
||||||
get($spaceManagement.methodAssignees(url)).find(spec({pubkey}))?.methods ?? [],
|
selected = [...assigned]
|
||||||
)
|
|
||||||
selected = new Set(assigned)
|
|
||||||
loading = false
|
loading = false
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -133,11 +112,11 @@
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox"
|
class="checkbox"
|
||||||
checked={selected.has(method)}
|
value={method}
|
||||||
disabled={selected.has(method)
|
bind:group={selected}
|
||||||
|
disabled={selected.includes(method)
|
||||||
? !$permissions.unassignMethods
|
? !$permissions.unassignMethods
|
||||||
: !$permissions.assignMethods}
|
: !$permissions.assignMethods} />
|
||||||
onchange={() => toggle(method)} />
|
|
||||||
</label>
|
</label>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
import RoleItem from "@app/components/RoleItem.svelte"
|
import RoleItem from "@app/components/RoleItem.svelte"
|
||||||
import SpaceRoles from "@app/components/SpaceRoles.svelte"
|
import SpaceRoles from "@app/components/SpaceRoles.svelte"
|
||||||
import {app, profiles, relayManagement} from "@app/core"
|
import {app, profiles, relayManagement} from "@app/core"
|
||||||
import {deriveSpacePermissions} from "@app/management"
|
import {deriveSpacePermissions, syncAssignments} from "@app/management"
|
||||||
import {deriveSpaceMemberRoles} from "@app/roles"
|
import {deriveSpaceMemberRoles} from "@app/roles"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
@ -33,61 +33,37 @@
|
||||||
|
|
||||||
const profileDisplay = $profiles.display(pubkey, [url]).$
|
const profileDisplay = $profiles.display(pubkey, [url]).$
|
||||||
const memberRoles = deriveSpaceMemberRoles(url)
|
const memberRoles = deriveSpaceMemberRoles(url)
|
||||||
const initial = new Set(get(memberRoles).get(pubkey) ?? [])
|
const initial = get(memberRoles).get(pubkey) ?? []
|
||||||
const permissions = deriveSpacePermissions(url)
|
const permissions = deriveSpacePermissions(url)
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const manageRoles = () => pushModal(SpaceRoles, {url})
|
const manageRoles = () => pushModal(SpaceRoles, {url})
|
||||||
|
|
||||||
const toggle = (id: string) => {
|
|
||||||
const next = new Set(selected)
|
|
||||||
|
|
||||||
if (next.has(id)) {
|
|
||||||
next.delete(id)
|
|
||||||
} else {
|
|
||||||
next.add(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
selected = next
|
|
||||||
}
|
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const management = $relayManagement.forUrl(url)
|
const management = $relayManagement.forUrl(url)
|
||||||
|
const error = await syncAssignments(
|
||||||
|
initial,
|
||||||
|
selected,
|
||||||
|
id => management.assignRole(pubkey, id),
|
||||||
|
id => management.unassignRole(pubkey, id),
|
||||||
|
)
|
||||||
|
|
||||||
for (const id of selected) {
|
if (error) {
|
||||||
if (!initial.has(id)) {
|
pushToast({theme: "error", message: error})
|
||||||
const {error} = await management.assignRole(pubkey, id)
|
} else {
|
||||||
|
pushToast({message: "Roles updated!"})
|
||||||
if (error) {
|
back()
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const id of initial) {
|
|
||||||
if (!selected.has(id)) {
|
|
||||||
const {error} = await management.unassignRole(pubkey, id)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pushToast({message: "Roles updated!"})
|
|
||||||
back()
|
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let selected = $state(new Set(initial))
|
let selected = $state([...initial])
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -109,8 +85,8 @@
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox"
|
class="checkbox"
|
||||||
checked={selected.has(role.identifier() ?? "")}
|
value={role.identifier() ?? ""}
|
||||||
onchange={() => toggle(role.identifier() ?? "")} />
|
bind:group={selected} />
|
||||||
</label>
|
</label>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
76
src/app/components/SpaceMembersAdd.svelte
Normal file
76
src/app/components/SpaceMembersAdd.svelte
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
||||||
|
import {allowPubkeys} from "@app/management"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const error = await allowPubkeys(url, pubkeys)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Members have successfully been added!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
let pubkeys: string[] = $state([])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Add Members</ModalTitle>
|
||||||
|
<ModalSubtitle>
|
||||||
|
to <span class="text-primary">{displayRelayUrl(url)}</span>
|
||||||
|
</ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Search for People</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<ProfileMultiSelect bind:value={pubkeys} />
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="button button-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="button button-primary"
|
||||||
|
onclick={submit}
|
||||||
|
disabled={loading || pubkeys.length === 0}>
|
||||||
|
<Spinner {loading}>Save</Spinner>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import {derived, readable, writable} from "svelte/store"
|
import {derived, readable, writable} from "svelte/store"
|
||||||
import {ago, MINUTE, now, simpleCache} from "@welshman/lib"
|
import {ago, difference, MINUTE, now, simpleCache} from "@welshman/lib"
|
||||||
import {ROOM_CREATE_PERMISSION, hexTags, tagValues} from "@welshman/util"
|
import {ROOM_CREATE_PERMISSION, hexTags, tagValues} from "@welshman/util"
|
||||||
import {RelayManagement, Relays} from "@welshman/app"
|
import {RelayManagement, Relays} from "@welshman/app"
|
||||||
import type {IApp} from "@welshman/app"
|
import type {IApp} from "@welshman/app"
|
||||||
import {events, fromApp, usePlugin, user} from "@app/core"
|
import {events, fromApp, relayManagement, relayMemberLists, usePlugin, user} from "@app/core"
|
||||||
|
|
||||||
// `supportedmethods` answers for the pubkey that signed the request, so this catalog is the client's.
|
// `supportedmethods` answers for the pubkey that signed the request, so this catalog is the client's.
|
||||||
export const MANAGEMENT_METHOD_GROUPS = [
|
export const MANAGEMENT_METHOD_GROUPS = [
|
||||||
|
|
@ -199,6 +199,44 @@ const getSpacePermissions = (methods: string[]) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
|
||||||
|
export const allowPubkeys = async (url: string, pubkeys: string[]) => {
|
||||||
|
const members = relayMemberLists.get().get(url)
|
||||||
|
const management = relayManagement.get().forUrl(url)
|
||||||
|
const responses = await Promise.all(
|
||||||
|
pubkeys.filter(pk => !members?.isMember(pk)).map(pk => management.allowPubkey(pk)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return responses.map(response => response.error).find(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Assign = (id: string) => Promise<{error?: string}>
|
||||||
|
|
||||||
|
// Stops at the first refusal, leaving whatever was already applied in place.
|
||||||
|
export const syncAssignments = async (
|
||||||
|
current: string[],
|
||||||
|
next: string[],
|
||||||
|
assign: Assign,
|
||||||
|
unassign: Assign,
|
||||||
|
) => {
|
||||||
|
for (const id of difference(next, current)) {
|
||||||
|
const {error} = await assign(id)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of difference(current, next)) {
|
||||||
|
const {error} = await unassign(id)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Each control is gated on the NIP-86 method behind it, as reported for the signed-in user.
|
// Each control is gated on the NIP-86 method behind it, as reported for the signed-in user.
|
||||||
export const deriveSpacePermissions = (url?: string) =>
|
export const deriveSpacePermissions = (url?: string) =>
|
||||||
derived(deriveSpaceSupportedMethods(url), getSpacePermissions)
|
derived(deriveSpaceSupportedMethods(url), getSpacePermissions)
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@ import type {Room, RoomMeta} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
deriveUserItem,
|
deriveUserItem,
|
||||||
command,
|
command,
|
||||||
relayManagement,
|
|
||||||
relayMemberLists,
|
|
||||||
roomLists,
|
roomLists,
|
||||||
rooms,
|
rooms,
|
||||||
router,
|
router,
|
||||||
|
|
@ -31,7 +29,7 @@ import {
|
||||||
user,
|
user,
|
||||||
writer,
|
writer,
|
||||||
} from "@app/core"
|
} from "@app/core"
|
||||||
import {deriveSpacePermissions, deriveUserIsSpaceStaff} from "@app/management"
|
import {allowPubkeys, deriveSpacePermissions, deriveUserIsSpaceStaff} from "@app/management"
|
||||||
import {makeRoomPath} from "@app/routes"
|
import {makeRoomPath} from "@app/routes"
|
||||||
import {loadSendDelay} from "@app/settings"
|
import {loadSendDelay} from "@app/settings"
|
||||||
|
|
||||||
|
|
@ -57,17 +55,10 @@ export const deriveRoomMembers = (url: string, h: string) =>
|
||||||
|
|
||||||
// A room member also has to be allowed at the relay level to read the room at all.
|
// A room member also has to be allowed at the relay level to read the room at all.
|
||||||
export const addRoomMembers = async (url: string, room: RoomMeta, pubkeys: string[]) => {
|
export const addRoomMembers = async (url: string, room: RoomMeta, pubkeys: string[]) => {
|
||||||
const members = relayMemberLists.get().get(url)
|
const allowError = await allowPubkeys(url, pubkeys)
|
||||||
const management = relayManagement.get().forUrl(url)
|
|
||||||
|
|
||||||
const responses = await Promise.all(
|
if (allowError) {
|
||||||
pubkeys.filter(pk => !members?.isMember(pk)).map(pk => management.allowPubkey(pk)),
|
return allowError
|
||||||
)
|
|
||||||
|
|
||||||
for (const {error} of responses) {
|
|
||||||
if (error) {
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors = await Promise.all(
|
const errors = await Promise.all(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue