Migrate popovers to tippys
This commit is contained in:
parent
8d7d021594
commit
79c154bdcb
17 changed files with 736 additions and 672 deletions
|
|
@ -1,21 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {Address} from "@welshman/util"
|
import {Address} from "@welshman/util"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
|
||||||
import TrashBin from "@assets/icons/trash-bin-2.svg?dataurl"
|
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Badge from "@lib/components/Badge.svelte"
|
import Badge from "@lib/components/Badge.svelte"
|
||||||
import Popover from "@lib/components/Popover.svelte"
|
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import PinContentEvent from "@app/components/PinContentEvent.svelte"
|
import PinContentEvent from "@app/components/PinContentEvent.svelte"
|
||||||
import PinEdit from "@app/components/PinEdit.svelte"
|
import PinMenu from "@app/components/PinMenu.svelte"
|
||||||
import {deletePin, type Pin} from "@app/pinboards"
|
import {type Pin} from "@app/pinboards"
|
||||||
import {pushModal} from "@app/modal"
|
|
||||||
import {pushToast} from "@app/toast"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -42,61 +33,17 @@
|
||||||
|
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
|
|
||||||
let menuOpen = $state(false)
|
|
||||||
|
|
||||||
const edit = () => {
|
|
||||||
menuOpen = false
|
|
||||||
pushModal(PinEdit, {url, pin})
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmDelete = () => {
|
|
||||||
menuOpen = false
|
|
||||||
pushModal(Confirm, {
|
|
||||||
title: "Delete Link",
|
|
||||||
message: "Delete this link?",
|
|
||||||
confirm: async () => {
|
|
||||||
const error = await deletePin(url, pin.id)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "Link deleted!"})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="{props.class} relative flex h-full flex-col gap-2">
|
<div class="{props.class} relative flex h-full flex-col gap-2">
|
||||||
{#if showMenu}
|
{#if showMenu}
|
||||||
<div class="absolute right-2 top-2 z-feature">
|
<div class="absolute right-2 top-2 z-feature">
|
||||||
<Button
|
<MenuButton
|
||||||
class="button button-neutral button-sm button-square"
|
class="button button-neutral button-sm button-square"
|
||||||
aria-label="More options"
|
aria-label="More options"
|
||||||
onclick={() => (menuOpen = !menuOpen)}>
|
iconSize={4}
|
||||||
<Icon size={4} icon={MenuDots} />
|
component={PinMenu}
|
||||||
</Button>
|
componentProps={{url, pin}} />
|
||||||
{#if menuOpen}
|
|
||||||
<Popover hideOnClick onClose={() => (menuOpen = false)}>
|
|
||||||
<ul
|
|
||||||
transition:fly
|
|
||||||
class="menu bg-surface absolute right-0 z-popover mt-2 w-48 gap-1 rounded-2xl p-2">
|
|
||||||
<li>
|
|
||||||
<Button onclick={edit}>
|
|
||||||
<Icon icon={Pen} />
|
|
||||||
Edit link
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Button class="text-error" onclick={confirmDelete}>
|
|
||||||
<Icon icon={TrashBin} />
|
|
||||||
Delete link
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Popover>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{#if pin.title}
|
{#if pin.title}
|
||||||
<strong class="truncate min-w-0 pr-8">{pin.title}</strong>
|
<strong class="truncate min-w-0 pr-8">{pin.title}</strong>
|
||||||
|
|
|
||||||
58
src/app/components/PinMenu.svelte
Normal file
58
src/app/components/PinMenu.svelte
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
|
import TrashBin from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import PinEdit from "@app/components/PinEdit.svelte"
|
||||||
|
import {deletePin, type Pin} from "@app/pinboards"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
pin: Pin
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, pin, onClick}: Props = $props()
|
||||||
|
|
||||||
|
const edit = () => pushModal(PinEdit, {url, pin})
|
||||||
|
|
||||||
|
const confirmDelete = () =>
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Delete Link",
|
||||||
|
message: "Delete this link?",
|
||||||
|
confirm: async () => {
|
||||||
|
const error = await deletePin(url, pin.id)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Link deleted!"})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let ul: Element
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ul.addEventListener("click", onClick)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
|
||||||
|
<li>
|
||||||
|
<Button onclick={edit}>
|
||||||
|
<Icon icon={Pen} />
|
||||||
|
Edit link
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={confirmDelete}>
|
||||||
|
<Icon icon={TrashBin} />
|
||||||
|
Delete link
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
28
src/app/components/PomadeSessionMenu.svelte
Normal file
28
src/app/components/PomadeSessionMenu.svelte
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onDelete: () => void
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {onDelete, onClick}: Props = $props()
|
||||||
|
|
||||||
|
let ul: Element
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ul.addEventListener("click", onClick)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
|
||||||
|
<li>
|
||||||
|
<Button onclick={onDelete}>
|
||||||
|
<Icon icon={TrashBin2} />
|
||||||
|
Delete Session
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
@ -2,26 +2,13 @@
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {Client} from "@pomade/core"
|
import {Client} from "@pomade/core"
|
||||||
import {session, isPomadeSession} from "@welshman/app"
|
import {session, isPomadeSession} from "@welshman/app"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Badge from "@lib/components/Badge.svelte"
|
import Badge from "@lib/components/Badge.svelte"
|
||||||
import Popover from "@lib/components/Popover.svelte"
|
import PomadeSessionMenu from "@app/components/PomadeSessionMenu.svelte"
|
||||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {loadOtherPomadeSessions} from "@app/pomade"
|
import {loadOtherPomadeSessions} from "@app/pomade"
|
||||||
import type {PomadeSessionWithPeers} from "@app/pomade"
|
import type {PomadeSessionWithPeers} from "@app/pomade"
|
||||||
|
|
||||||
const toggleMenu = (client: string) => {
|
|
||||||
menuClient = menuClient === client ? "" : client
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMenu = () => {
|
|
||||||
menuClient = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
let menuClient = $state("")
|
|
||||||
let sessions = $state<PomadeSessionWithPeers[]>([])
|
let sessions = $state<PomadeSessionWithPeers[]>([])
|
||||||
|
|
||||||
const deleteSession = async (sessionItem: PomadeSessionWithPeers) => {
|
const deleteSession = async (sessionItem: PomadeSessionWithPeers) => {
|
||||||
|
|
@ -83,27 +70,9 @@
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<MenuButton
|
||||||
<Button
|
component={PomadeSessionMenu}
|
||||||
class="button button-ghost button-sm button-circle"
|
componentProps={{onDelete: () => deleteSession(sessionItem)}} />
|
||||||
onclick={() => toggleMenu(sessionItem.client)}>
|
|
||||||
<Icon icon={MenuDots} />
|
|
||||||
</Button>
|
|
||||||
{#if menuClient === sessionItem.client}
|
|
||||||
<Popover hideOnClick onClose={closeMenu}>
|
|
||||||
<ul
|
|
||||||
transition:fly
|
|
||||||
class="menu bg-surface absolute right-0 z-popover mt-2 w-48 gap-1 rounded-2xl p-2">
|
|
||||||
<li>
|
|
||||||
<Button onclick={() => deleteSession(sessionItem)}>
|
|
||||||
<Icon icon={TrashBin2} />
|
|
||||||
Delete Session
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Popover>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<Badge variant="neutral">
|
<Badge variant="neutral">
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {getTag, ManagementMethod} from "@welshman/util"
|
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {pubkey, manageRelay, repository, displayProfileByPubkey} from "@welshman/app"
|
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||||
import InboxOut from "@assets/icons/inbox-out.svg?dataurl"
|
import ReportMenuList from "@app/components/ReportMenuList.svelte"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
|
||||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
|
||||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
|
||||||
import Close from "@assets/icons/close.svg?dataurl"
|
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Popover from "@lib/components/Popover.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
|
||||||
import {deriveUserIsSpaceAdmin, banSpaceMembers} from "@app/members"
|
|
||||||
import {publishDelete} from "@app/deletes"
|
|
||||||
import {canEnforceNip70} from "@app/relays"
|
|
||||||
import {pushToast} from "@app/toast"
|
|
||||||
import {pushModal} from "@app/modal"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -25,131 +10,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, event, onResolved}: Props = $props()
|
const {url, event, onResolved}: Props = $props()
|
||||||
|
|
||||||
const shouldProtect = canEnforceNip70(url)
|
|
||||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
|
||||||
const etag = getTag("e", event.tags)
|
|
||||||
const ptag = getTag("p", event.tags)
|
|
||||||
|
|
||||||
const toggleMenu = () => {
|
|
||||||
isOpen = !isOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMenu = () => {
|
|
||||||
isOpen = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteReport = async () => {
|
|
||||||
publishDelete({event, relays: [url], protect: await shouldProtect})
|
|
||||||
onResolved?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
const dismissReport = async () => {
|
|
||||||
const {error} = await manageRelay(url, {
|
|
||||||
method: ManagementMethod.BanEvent,
|
|
||||||
params: [event.id, "Dismissed by admin"],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "Content has successfully been deleted!"})
|
|
||||||
repository.removeEvent(event.id)
|
|
||||||
onResolved?.()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const banContent = () => {
|
|
||||||
const [_, id, reason = ""] = etag!
|
|
||||||
|
|
||||||
pushModal(Confirm, {
|
|
||||||
title: `Remove Content`,
|
|
||||||
message: `Are you sure you want to delete this content from the space?`,
|
|
||||||
confirm: async () => {
|
|
||||||
const {error} = await manageRelay(url, {
|
|
||||||
method: ManagementMethod.BanEvent,
|
|
||||||
params: [id, reason],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "Content has successfully been deleted!"})
|
|
||||||
repository.removeEvent(event.id)
|
|
||||||
repository.removeEvent(id)
|
|
||||||
history.back()
|
|
||||||
setTimeout(() => onResolved?.())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const banMember = () => {
|
|
||||||
const [_, pubkey, reason = ""] = ptag!
|
|
||||||
|
|
||||||
pushModal(Confirm, {
|
|
||||||
title: "Ban User",
|
|
||||||
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
|
||||||
confirm: async () => {
|
|
||||||
const error = await banSpaceMembers(url, [pubkey], reason)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "User has successfully been banned!"})
|
|
||||||
repository.removeEvent(event.id)
|
|
||||||
history.back()
|
|
||||||
setTimeout(() => onResolved?.())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let isOpen = $state(false)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative">
|
<MenuButton component={ReportMenuList} componentProps={{url, event, onResolved}} />
|
||||||
<Button class="button button-ghost button-sm button-circle" onclick={toggleMenu}>
|
|
||||||
<Icon icon={MenuDots} />
|
|
||||||
</Button>
|
|
||||||
{#if isOpen}
|
|
||||||
<Popover hideOnClick onClose={closeMenu}>
|
|
||||||
<ul
|
|
||||||
transition:fly
|
|
||||||
class="menu bg-surface absolute right-0 z-popover mt-2 w-48 gap-1 rounded-2xl p-2">
|
|
||||||
{#if event.pubkey === $pubkey}
|
|
||||||
<li>
|
|
||||||
<Button onclick={deleteReport}>
|
|
||||||
<Icon icon={Close} />
|
|
||||||
Delete Report
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{#if $userIsAdmin}
|
|
||||||
<li>
|
|
||||||
<Button onclick={dismissReport}>
|
|
||||||
<Icon icon={InboxOut} />
|
|
||||||
Dismiss Report
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{#if etag}
|
|
||||||
<li>
|
|
||||||
<Button class="text-error" onclick={banContent}>
|
|
||||||
<Icon icon={TrashBin2} />
|
|
||||||
Remove Content
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{#if ptag}
|
|
||||||
<li>
|
|
||||||
<Button class="text-error" onclick={banMember}>
|
|
||||||
<Icon icon={MinusCircle} />
|
|
||||||
Ban User
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
||||||
</Popover>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
|
||||||
139
src/app/components/ReportMenuList.svelte
Normal file
139
src/app/components/ReportMenuList.svelte
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import {getTag, ManagementMethod} from "@welshman/util"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {pubkey, manageRelay, repository, displayProfileByPubkey} from "@welshman/app"
|
||||||
|
import InboxOut from "@assets/icons/inbox-out.svg?dataurl"
|
||||||
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
|
import Close from "@assets/icons/close.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import {deriveUserIsSpaceAdmin, banSpaceMembers} from "@app/members"
|
||||||
|
import {publishDelete} from "@app/deletes"
|
||||||
|
import {canEnforceNip70} from "@app/relays"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
onResolved?: () => void
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event, onResolved, onClick}: Props = $props()
|
||||||
|
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
|
const etag = getTag("e", event.tags)
|
||||||
|
const ptag = getTag("p", event.tags)
|
||||||
|
|
||||||
|
const deleteReport = async () => {
|
||||||
|
publishDelete({event, relays: [url], protect: await shouldProtect})
|
||||||
|
onResolved?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const dismissReport = async () => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.BanEvent,
|
||||||
|
params: [event.id, "Dismissed by admin"],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Content has successfully been deleted!"})
|
||||||
|
repository.removeEvent(event.id)
|
||||||
|
onResolved?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const banContent = () => {
|
||||||
|
const [_, id, reason = ""] = etag!
|
||||||
|
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: `Remove Content`,
|
||||||
|
message: `Are you sure you want to delete this content from the space?`,
|
||||||
|
confirm: async () => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.BanEvent,
|
||||||
|
params: [id, reason],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Content has successfully been deleted!"})
|
||||||
|
repository.removeEvent(event.id)
|
||||||
|
repository.removeEvent(id)
|
||||||
|
history.back()
|
||||||
|
setTimeout(() => onResolved?.())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const banMember = () => {
|
||||||
|
const [_, pubkey, reason = ""] = ptag!
|
||||||
|
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Ban User",
|
||||||
|
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
|
confirm: async () => {
|
||||||
|
const error = await banSpaceMembers(url, [pubkey], reason)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "User has successfully been banned!"})
|
||||||
|
repository.removeEvent(event.id)
|
||||||
|
history.back()
|
||||||
|
setTimeout(() => onResolved?.())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let ul: Element
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ul.addEventListener("click", onClick)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
|
||||||
|
{#if event.pubkey === $pubkey}
|
||||||
|
<li>
|
||||||
|
<Button onclick={deleteReport}>
|
||||||
|
<Icon icon={Close} />
|
||||||
|
Delete Report
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#if $userIsAdmin}
|
||||||
|
<li>
|
||||||
|
<Button onclick={dismissReport}>
|
||||||
|
<Icon icon={InboxOut} />
|
||||||
|
Dismiss Report
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{#if etag}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={banContent}>
|
||||||
|
<Icon icon={TrashBin2} />
|
||||||
|
Remove Content
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#if ptag}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={banMember}>
|
||||||
|
<Icon icon={MinusCircle} />
|
||||||
|
Ban User
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
|
@ -1,14 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import type {RoomMeta} from "@welshman/util"
|
|
||||||
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
|
|
||||||
import type {Thunk} from "@welshman/app"
|
|
||||||
import {deleteRoom, waitForThunkError, repository, joinRoom, leaveRoom} from "@welshman/app"
|
|
||||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
|
||||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
|
||||||
import Login3 from "@assets/icons/login-3.svg?dataurl"
|
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
|
||||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import EyeClosed from "@assets/icons/eye-closed.svg?dataurl"
|
import EyeClosed from "@assets/icons/eye-closed.svg?dataurl"
|
||||||
import Eye from "@assets/icons/eye.svg?dataurl"
|
import Eye from "@assets/icons/eye.svg?dataurl"
|
||||||
|
|
@ -17,32 +8,22 @@
|
||||||
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||||
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
||||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||||
import Popover from "@lib/components/Popover.svelte"
|
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
|
||||||
import Tooltip from "@lib/components/Tooltip.svelte"
|
import Tooltip from "@lib/components/Tooltip.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 ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import RoomMembers from "@app/components/RoomMembers.svelte"
|
import RoomMembers from "@app/components/RoomMembers.svelte"
|
||||||
import RoomEdit from "@app/components/RoomEdit.svelte"
|
import RoomDetailMenu from "@app/components/RoomDetailMenu.svelte"
|
||||||
import RoomName from "@app/components/RoomName.svelte"
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
import RoomImage from "@app/components/RoomImage.svelte"
|
import RoomImage from "@app/components/RoomImage.svelte"
|
||||||
import {deriveRoom, deriveUserRooms, addRoom, removeRoom} from "@app/groups"
|
import {deriveRoom, deriveUserRooms, addRoom, removeRoom} from "@app/groups"
|
||||||
import {
|
import {deriveRoomMembers} from "@app/members"
|
||||||
deriveRoomMembers,
|
|
||||||
deriveUserIsRoomAdmin,
|
|
||||||
deriveUserRoomMembershipStatus,
|
|
||||||
MembershipStatus,
|
|
||||||
} from "@app/members"
|
|
||||||
import {deriveShouldNotify, toggleRoomNotifications} from "@app/settings"
|
import {deriveShouldNotify, toggleRoomNotifications} from "@app/settings"
|
||||||
import {makeSpacePath} from "@app/routes"
|
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -53,8 +34,6 @@
|
||||||
|
|
||||||
const room = deriveRoom(url, h)
|
const room = deriveRoom(url, h)
|
||||||
const members = deriveRoomMembers(url, h)
|
const members = deriveRoomMembers(url, h)
|
||||||
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
|
|
||||||
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
|
|
||||||
const userRooms = deriveUserRooms(url)
|
const userRooms = deriveUserRooms(url)
|
||||||
|
|
||||||
const isFavorite = $derived($userRooms.includes(h))
|
const isFavorite = $derived($userRooms.includes(h))
|
||||||
|
|
@ -62,34 +41,6 @@
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const toggleMenu = () => {
|
|
||||||
showMenu = !showMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMenu = () => {
|
|
||||||
showMenu = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const startEdit = () => pushModal(RoomEdit, {url, h})
|
|
||||||
|
|
||||||
const handleLoading = async (f: (url: string, room: RoomMeta) => Thunk) => {
|
|
||||||
loading = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const message = await waitForThunkError(f(url, makeRoomMeta({h})))
|
|
||||||
|
|
||||||
if (message && !message.startsWith("duplicate:")) {
|
|
||||||
pushToast({theme: "error", message})
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const join = () => handleLoading(joinRoom)
|
|
||||||
|
|
||||||
const leave = () => handleLoading(leaveRoom)
|
|
||||||
|
|
||||||
const showMembers = () => pushModal(RoomMembers, {url, h})
|
const showMembers = () => pushModal(RoomMembers, {url, h})
|
||||||
|
|
||||||
const toggleFavorite = () => {
|
const toggleFavorite = () => {
|
||||||
|
|
@ -103,28 +54,6 @@
|
||||||
const toggleShouldNotify = () => {
|
const toggleShouldNotify = () => {
|
||||||
toggleRoomNotifications(url, h)
|
toggleRoomNotifications(url, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
const startDelete = () =>
|
|
||||||
pushModal(Confirm, {
|
|
||||||
title: "Are you sure you want to delete this room?",
|
|
||||||
message:
|
|
||||||
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
|
|
||||||
confirm: async () => {
|
|
||||||
const thunk = deleteRoom(url, $room)
|
|
||||||
const message = await waitForThunkError(thunk)
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
repository.removeEvent(thunk.event.id)
|
|
||||||
pushToast({theme: "error", message})
|
|
||||||
} else {
|
|
||||||
await removeRoom(url, h)
|
|
||||||
goto(makeSpacePath(url))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
let loading = $state(false)
|
|
||||||
let showMenu = $state(false)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
|
|
@ -139,62 +68,7 @@
|
||||||
<span class="text-primary">{displayRelayUrl(url)}</span>
|
<span class="text-primary">{displayRelayUrl(url)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<MenuButton component={RoomDetailMenu} componentProps={{url, h}} />
|
||||||
<Button class="button button-ghost button-sm button-circle" onclick={toggleMenu}>
|
|
||||||
<Icon icon={MenuDots} />
|
|
||||||
</Button>
|
|
||||||
{#if showMenu}
|
|
||||||
<Popover hideOnClick onClose={closeMenu}>
|
|
||||||
<ul
|
|
||||||
transition:fly
|
|
||||||
class="bg-surface menu absolute right-0 z-popover w-48 gap-1 rounded-2xl p-2 shadow-md">
|
|
||||||
{#if $userIsAdmin}
|
|
||||||
<li>
|
|
||||||
<Button onclick={startEdit}>
|
|
||||||
<Icon icon={Pen} />
|
|
||||||
Edit Room
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Button class="text-error" onclick={startDelete}>
|
|
||||||
<Icon icon={TrashBin2} />
|
|
||||||
Delete Room
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{:else if $membershipStatus === MembershipStatus.Initial}
|
|
||||||
<li>
|
|
||||||
<Button disabled={loading} onclick={join}>
|
|
||||||
{#if loading}
|
|
||||||
<Spinner size="sm" />
|
|
||||||
{:else}
|
|
||||||
<Icon icon={Login3} />
|
|
||||||
{/if}
|
|
||||||
Join member list
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{:else if $membershipStatus === MembershipStatus.Pending}
|
|
||||||
<li>
|
|
||||||
<Button>
|
|
||||||
<Icon icon={ClockCircle} />
|
|
||||||
Membership pending
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{:else}
|
|
||||||
<li>
|
|
||||||
<Button disabled={loading} onclick={leave}>
|
|
||||||
{#if loading}
|
|
||||||
<Spinner size="sm" />
|
|
||||||
{:else}
|
|
||||||
<Icon icon={Login3} />
|
|
||||||
{/if}
|
|
||||||
Leave member list
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
||||||
</Popover>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{#if $room?.about}
|
{#if $room?.about}
|
||||||
<p>{$room.about}</p>
|
<p>{$room.about}</p>
|
||||||
|
|
|
||||||
130
src/app/components/RoomDetailMenu.svelte
Normal file
130
src/app/components/RoomDetailMenu.svelte
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
|
import type {RoomMeta} from "@welshman/util"
|
||||||
|
import {makeRoomMeta} from "@welshman/util"
|
||||||
|
import type {Thunk} from "@welshman/app"
|
||||||
|
import {deleteRoom, waitForThunkError, repository, joinRoom, leaveRoom} from "@welshman/app"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
|
import Login3 from "@assets/icons/login-3.svg?dataurl"
|
||||||
|
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import RoomEdit from "@app/components/RoomEdit.svelte"
|
||||||
|
import {deriveRoom, removeRoom} from "@app/groups"
|
||||||
|
import {
|
||||||
|
deriveUserIsRoomAdmin,
|
||||||
|
deriveUserRoomMembershipStatus,
|
||||||
|
MembershipStatus,
|
||||||
|
} from "@app/members"
|
||||||
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
h: string
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h, onClick}: Props = $props()
|
||||||
|
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
|
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
|
||||||
|
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
|
||||||
|
|
||||||
|
const startEdit = () => pushModal(RoomEdit, {url, h})
|
||||||
|
|
||||||
|
const handleLoading = async (f: (url: string, room: RoomMeta) => Thunk) => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await waitForThunkError(f(url, makeRoomMeta({h})))
|
||||||
|
|
||||||
|
if (message && !message.startsWith("duplicate:")) {
|
||||||
|
pushToast({theme: "error", message})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const join = () => handleLoading(joinRoom)
|
||||||
|
|
||||||
|
const leave = () => handleLoading(leaveRoom)
|
||||||
|
|
||||||
|
const startDelete = () =>
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Are you sure you want to delete this room?",
|
||||||
|
message:
|
||||||
|
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
|
||||||
|
confirm: async () => {
|
||||||
|
const thunk = deleteRoom(url, $room)
|
||||||
|
const message = await waitForThunkError(thunk)
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
repository.removeEvent(thunk.event.id)
|
||||||
|
pushToast({theme: "error", message})
|
||||||
|
} else {
|
||||||
|
await removeRoom(url, h)
|
||||||
|
goto(makeSpacePath(url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
let ul: Element
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ul.addEventListener("click", onClick)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
|
||||||
|
{#if $userIsAdmin}
|
||||||
|
<li>
|
||||||
|
<Button onclick={startEdit}>
|
||||||
|
<Icon icon={Pen} />
|
||||||
|
Edit Room
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={startDelete}>
|
||||||
|
<Icon icon={TrashBin2} />
|
||||||
|
Delete Room
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{:else if $membershipStatus === MembershipStatus.Initial}
|
||||||
|
<li>
|
||||||
|
<Button disabled={loading} onclick={join}>
|
||||||
|
{#if loading}
|
||||||
|
<Spinner size="sm" />
|
||||||
|
{:else}
|
||||||
|
<Icon icon={Login3} />
|
||||||
|
{/if}
|
||||||
|
Join member list
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{:else if $membershipStatus === MembershipStatus.Pending}
|
||||||
|
<li>
|
||||||
|
<Button>
|
||||||
|
<Icon icon={ClockCircle} />
|
||||||
|
Membership pending
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{:else}
|
||||||
|
<li>
|
||||||
|
<Button disabled={loading} onclick={leave}>
|
||||||
|
{#if loading}
|
||||||
|
<Spinner size="sm" />
|
||||||
|
{:else}
|
||||||
|
<Icon icon={Login3} />
|
||||||
|
{/if}
|
||||||
|
Leave member list
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
53
src/app/components/RoomMemberMenu.svelte
Normal file
53
src/app/components/RoomMemberMenu.svelte
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import {waitForThunkError, removeRoomMember} from "@welshman/app"
|
||||||
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import {deriveRoom} from "@app/groups"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
h: string
|
||||||
|
pubkey: string
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h, pubkey, onClick}: Props = $props()
|
||||||
|
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
|
|
||||||
|
const removeMember = () =>
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Remove Member",
|
||||||
|
message: "Are you sure you want to remove this user from the room?",
|
||||||
|
confirm: async () => {
|
||||||
|
const error = await waitForThunkError(removeRoomMember(url, $room, pubkey))
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Member has successfully been removed!"})
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let ul: Element
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ul.addEventListener("click", onClick)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={removeMember}>
|
||||||
|
<Icon icon={MinusCircle} />
|
||||||
|
Remove Member
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {waitForThunkError, removeRoomMember} from "@welshman/app"
|
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
|
||||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
|
||||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
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 Popover from "@lib/components/Popover.svelte"
|
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||||
import Confirm from "@lib/components/Confirm.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 ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
|
@ -18,10 +13,9 @@
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import RoomName from "@app/components/RoomName.svelte"
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
import RoomMembersAdd from "@app/components/RoomMembersAdd.svelte"
|
import RoomMembersAdd from "@app/components/RoomMembersAdd.svelte"
|
||||||
import {deriveRoom} from "@app/groups"
|
import RoomMemberMenu from "@app/components/RoomMemberMenu.svelte"
|
||||||
import {deriveRoomMembers, deriveUserIsRoomAdmin} from "@app/members"
|
import {deriveRoomMembers, deriveUserIsRoomAdmin} from "@app/members"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -30,39 +24,12 @@
|
||||||
|
|
||||||
const {url, h}: Props = $props()
|
const {url, h}: Props = $props()
|
||||||
|
|
||||||
const room = deriveRoom(url, h)
|
|
||||||
const members = deriveRoomMembers(url, h)
|
const members = deriveRoomMembers(url, h)
|
||||||
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
|
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const toggleMenu = (pubkey: string) => {
|
|
||||||
menuPubkey = menuPubkey === pubkey ? undefined : pubkey
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMenu = () => {
|
|
||||||
menuPubkey = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const addMember = () => pushModal(RoomMembersAdd, {url, h})
|
const addMember = () => pushModal(RoomMembersAdd, {url, h})
|
||||||
|
|
||||||
const removeMember = (pubkey: string) =>
|
|
||||||
pushModal(Confirm, {
|
|
||||||
title: "Remove Member",
|
|
||||||
message: "Are you sure you want to remove this user from the room?",
|
|
||||||
confirm: async () => {
|
|
||||||
const error = await waitForThunkError(removeRoomMember(url, $room, pubkey))
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "Member has successfully been removed!"})
|
|
||||||
back()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
let menuPubkey = $state<string | undefined>()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
|
|
@ -89,27 +56,7 @@
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<Profile {pubkey} {url} />
|
<Profile {pubkey} {url} />
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<MenuButton component={RoomMemberMenu} componentProps={{url, h, pubkey}} />
|
||||||
<Button
|
|
||||||
class="button button-ghost button-sm button-circle"
|
|
||||||
onclick={() => toggleMenu(pubkey)}>
|
|
||||||
<Icon icon={MenuDots} />
|
|
||||||
</Button>
|
|
||||||
{#if menuPubkey === pubkey}
|
|
||||||
<Popover hideOnClick onClose={closeMenu}>
|
|
||||||
<ul
|
|
||||||
transition:fly
|
|
||||||
class="menu bg-surface absolute right-0 z-popover mt-2 w-48 gap-1 rounded-2xl p-2">
|
|
||||||
<li>
|
|
||||||
<Button class="text-error" onclick={() => removeMember(pubkey)}>
|
|
||||||
<Icon icon={MinusCircle} />
|
|
||||||
Remove Member
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Popover>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {ManagementMethod} from "@welshman/util"
|
import {ManagementMethod} from "@welshman/util"
|
||||||
import {displayProfileByPubkey} from "@welshman/app"
|
import {displayProfileByPubkey} from "@welshman/app"
|
||||||
import {fly} from "@lib/transition"
|
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
|
||||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
|
||||||
import UserMinus from "@assets/icons/user-minus.svg?dataurl"
|
|
||||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Popover from "@lib/components/Popover.svelte"
|
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import ProfileAbout from "@app/components/ProfileAbout.svelte"
|
import ProfileAbout from "@app/components/ProfileAbout.svelte"
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import SpaceMemberRoles from "@app/components/SpaceMemberRoles.svelte"
|
import SpaceMemberMenu from "@app/components/SpaceMemberMenu.svelte"
|
||||||
import RoleBadge from "@app/components/RoleBadge.svelte"
|
import RoleBadge from "@app/components/RoleBadge.svelte"
|
||||||
import {removeSpaceMembers, banSpaceMembers, type SpaceRole} from "@app/members"
|
import {type SpaceRole} from "@app/members"
|
||||||
import {deriveSupportedMethods} from "@app/relays"
|
import {deriveSupportedMethods} from "@app/relays"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -34,56 +25,7 @@
|
||||||
const canAssign = $derived($supportedMethods.some(m => (m as string) === "assignrole"))
|
const canAssign = $derived($supportedMethods.some(m => (m as string) === "assignrole"))
|
||||||
const canUnassign = $derived($supportedMethods.some(m => (m as string) === "unassignrole"))
|
const canUnassign = $derived($supportedMethods.some(m => (m as string) === "unassignrole"))
|
||||||
|
|
||||||
let menuOpen = $state(false)
|
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
const closeMenu = () => (menuOpen = false)
|
|
||||||
|
|
||||||
const openProfile = () => {
|
|
||||||
menuOpen = false
|
|
||||||
pushModal(ProfileDetail, {pubkey, url})
|
|
||||||
}
|
|
||||||
|
|
||||||
const editRoles = () => {
|
|
||||||
menuOpen = false
|
|
||||||
pushModal(SpaceMemberRoles, {url, pubkey})
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeMember = () => {
|
|
||||||
menuOpen = false
|
|
||||||
pushModal(Confirm, {
|
|
||||||
title: "Remove Member",
|
|
||||||
message: `Remove @${displayProfileByPubkey(pubkey)} from the space?`,
|
|
||||||
confirm: async () => {
|
|
||||||
const error = await removeSpaceMembers(url, [pubkey])
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "Member has successfully been removed!"})
|
|
||||||
back()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const banMember = () => {
|
|
||||||
menuOpen = false
|
|
||||||
pushModal(Confirm, {
|
|
||||||
title: "Ban Member",
|
|
||||||
message: `Ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
|
||||||
confirm: async () => {
|
|
||||||
const error = await banSpaceMembers(url, [pubkey])
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "Member has successfully been banned!"})
|
|
||||||
back()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card card-interactive card-sm relative">
|
<div class="card card-interactive card-sm relative">
|
||||||
|
|
@ -108,44 +50,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if canAssign || canUnassign || canUnallow || canBan}
|
{#if canAssign || canUnassign || canUnallow || canBan}
|
||||||
<div class="pointer-events-auto relative shrink-0">
|
<div class="pointer-events-auto shrink-0">
|
||||||
<Button
|
<MenuButton
|
||||||
class="button button-ghost button-sm button-square"
|
class="button button-ghost button-sm button-square"
|
||||||
onclick={() => (menuOpen = !menuOpen)}>
|
component={SpaceMemberMenu}
|
||||||
<Icon icon={MenuDots} />
|
componentProps={{url, pubkey}} />
|
||||||
</Button>
|
|
||||||
{#if menuOpen}
|
|
||||||
<Popover hideOnClick onClose={closeMenu}>
|
|
||||||
<ul
|
|
||||||
transition:fly
|
|
||||||
class="menu bg-surface absolute right-0 z-popover mt-2 w-52 gap-1 rounded-2xl p-2">
|
|
||||||
{#if canAssign || canUnassign}
|
|
||||||
<li>
|
|
||||||
<Button onclick={editRoles}>
|
|
||||||
<Icon icon={Pen} />
|
|
||||||
Edit roles
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{#if canUnallow}
|
|
||||||
<li>
|
|
||||||
<Button onclick={removeMember}>
|
|
||||||
<Icon icon={UserMinus} />
|
|
||||||
Remove member
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{#if canBan}
|
|
||||||
<li>
|
|
||||||
<Button class="text-error" onclick={banMember}>
|
|
||||||
<Icon icon={MinusCircle} />
|
|
||||||
Ban member
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
||||||
</Popover>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
76
src/app/components/SpaceMemberBannedMenu.svelte
Normal file
76
src/app/components/SpaceMemberBannedMenu.svelte
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import {ManagementMethod} from "@welshman/util"
|
||||||
|
import {manageRelay} from "@welshman/app"
|
||||||
|
import Restart from "@assets/icons/restart.svg?dataurl"
|
||||||
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import {addSpaceMembers} from "@app/members"
|
||||||
|
import {deriveSupportedMethods} from "@app/relays"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
pubkey: string
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, pubkey, onClick}: Props = $props()
|
||||||
|
|
||||||
|
const supportedMethods = deriveSupportedMethods(url)
|
||||||
|
const canUnban = $derived($supportedMethods.includes(ManagementMethod.UnbanPubkey))
|
||||||
|
const canRestore = $derived($supportedMethods.includes(ManagementMethod.AllowPubkey))
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const unbanMember = async () => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.UnbanPubkey,
|
||||||
|
params: [pubkey],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "User has successfully been removed from the ban list!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreMember = async () => {
|
||||||
|
const error = await addSpaceMembers(url, [pubkey])
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "User has successfully been restored to membership!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ul: Element
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ul.addEventListener("click", onClick)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
|
||||||
|
{#if canUnban}
|
||||||
|
<li>
|
||||||
|
<Button onclick={unbanMember}>
|
||||||
|
<Icon icon={CloseCircle} />
|
||||||
|
Unban User
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#if canRestore}
|
||||||
|
<li>
|
||||||
|
<Button onclick={restoreMember}>
|
||||||
|
<Icon icon={Restart} />
|
||||||
|
Restore User
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
99
src/app/components/SpaceMemberMenu.svelte
Normal file
99
src/app/components/SpaceMemberMenu.svelte
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import {ManagementMethod} from "@welshman/util"
|
||||||
|
import {displayProfileByPubkey} from "@welshman/app"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
|
import UserMinus from "@assets/icons/user-minus.svg?dataurl"
|
||||||
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import SpaceMemberRoles from "@app/components/SpaceMemberRoles.svelte"
|
||||||
|
import {removeSpaceMembers, banSpaceMembers} from "@app/members"
|
||||||
|
import {deriveSupportedMethods} from "@app/relays"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
pubkey: string
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, pubkey, onClick}: Props = $props()
|
||||||
|
|
||||||
|
const supportedMethods = deriveSupportedMethods(url)
|
||||||
|
const canUnallow = $derived($supportedMethods.includes(ManagementMethod.UnallowPubkey))
|
||||||
|
const canBan = $derived($supportedMethods.includes(ManagementMethod.BanPubkey))
|
||||||
|
const canAssign = $derived($supportedMethods.some(m => (m as string) === "assignrole"))
|
||||||
|
const canUnassign = $derived($supportedMethods.some(m => (m as string) === "unassignrole"))
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const editRoles = () => pushModal(SpaceMemberRoles, {url, pubkey})
|
||||||
|
|
||||||
|
const removeMember = () =>
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Remove Member",
|
||||||
|
message: `Remove @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
|
confirm: async () => {
|
||||||
|
const error = await removeSpaceMembers(url, [pubkey])
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Member has successfully been removed!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const banMember = () =>
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Ban Member",
|
||||||
|
message: `Ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
|
confirm: async () => {
|
||||||
|
const error = await banSpaceMembers(url, [pubkey])
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Member has successfully been banned!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let ul: Element
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ul.addEventListener("click", onClick)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
|
||||||
|
{#if canAssign || canUnassign}
|
||||||
|
<li>
|
||||||
|
<Button onclick={editRoles}>
|
||||||
|
<Icon icon={Pen} />
|
||||||
|
Edit roles
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#if canUnallow}
|
||||||
|
<li>
|
||||||
|
<Button onclick={removeMember}>
|
||||||
|
<Icon icon={UserMinus} />
|
||||||
|
Remove member
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#if canBan}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={banMember}>
|
||||||
|
<Icon icon={MinusCircle} />
|
||||||
|
Ban member
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
||||||
import {manageRelay} from "@welshman/app"
|
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
|
||||||
import Restart from "@assets/icons/restart.svg?dataurl"
|
|
||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
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 Popover from "@lib/components/Popover.svelte"
|
import MenuButton from "@lib/components/MenuButton.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"
|
||||||
|
|
@ -16,9 +11,9 @@
|
||||||
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 Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import {deriveSpaceBannedPubkeyItems, addSpaceMembers} from "@app/members"
|
import SpaceMemberBannedMenu from "@app/components/SpaceMemberBannedMenu.svelte"
|
||||||
|
import {deriveSpaceBannedPubkeyItems} from "@app/members"
|
||||||
import {deriveSupportedMethods} from "@app/relays"
|
import {deriveSupportedMethods} from "@app/relays"
|
||||||
import {pushToast} from "@app/toast"
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -32,41 +27,6 @@
|
||||||
const canRestore = $derived($supportedMethods.includes(ManagementMethod.AllowPubkey))
|
const canRestore = $derived($supportedMethods.includes(ManagementMethod.AllowPubkey))
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const toggleMenu = (pubkey: string) => {
|
|
||||||
menuPubkey = menuPubkey === pubkey ? undefined : pubkey
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMenu = () => {
|
|
||||||
menuPubkey = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const unbanMember = async (pubkey: string) => {
|
|
||||||
const {error} = await manageRelay(url, {
|
|
||||||
method: ManagementMethod.UnbanPubkey,
|
|
||||||
params: [pubkey],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "User has successfully been removed from the ban list!"})
|
|
||||||
back()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const restoreMember = async (pubkey: string) => {
|
|
||||||
const error = await addSpaceMembers(url, [pubkey])
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "User has successfully been restored to membership!"})
|
|
||||||
back()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let menuPubkey = $state<string | undefined>()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
|
|
@ -86,37 +46,7 @@
|
||||||
<Profile {pubkey} {url} />
|
<Profile {pubkey} {url} />
|
||||||
</div>
|
</div>
|
||||||
{#if canUnban || canRestore}
|
{#if canUnban || canRestore}
|
||||||
<div class="relative">
|
<MenuButton component={SpaceMemberBannedMenu} componentProps={{url, pubkey}} />
|
||||||
<Button
|
|
||||||
class="button button-ghost button-sm button-circle"
|
|
||||||
onclick={() => toggleMenu(pubkey)}>
|
|
||||||
<Icon icon={MenuDots} />
|
|
||||||
</Button>
|
|
||||||
{#if menuPubkey === pubkey}
|
|
||||||
<Popover hideOnClick onClose={closeMenu}>
|
|
||||||
<ul
|
|
||||||
transition:fly
|
|
||||||
class="menu bg-surface absolute right-0 z-popover mt-2 w-48 gap-1 rounded-2xl p-2">
|
|
||||||
{#if canUnban}
|
|
||||||
<li>
|
|
||||||
<Button onclick={() => unbanMember(pubkey)}>
|
|
||||||
<Icon icon={CloseCircle} />
|
|
||||||
Unban User
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{#if canRestore}
|
|
||||||
<li>
|
|
||||||
<Button onclick={() => restoreMember(pubkey)}>
|
|
||||||
<Icon icon={Restart} />
|
|
||||||
Restore User
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
||||||
</Popover>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
71
src/app/components/SpaceRoleMenu.svelte
Normal file
71
src/app/components/SpaceRoleMenu.svelte
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
|
import TrashBin from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import RoleEdit from "@app/components/RoleEdit.svelte"
|
||||||
|
import RoleAddMembers from "@app/components/RoleAddMembers.svelte"
|
||||||
|
import {deleteRole, type SpaceRole} from "@app/members"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
role: SpaceRole
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, role, onClick}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const editRole = () => pushModal(RoleEdit, {url, role})
|
||||||
|
|
||||||
|
const addMembers = () => pushModal(RoleAddMembers, {url, role})
|
||||||
|
|
||||||
|
const confirmDelete = () =>
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Delete Role",
|
||||||
|
message: `Delete the "${role.label}" role? Members will keep their space membership.`,
|
||||||
|
confirm: async () => {
|
||||||
|
const error = await deleteRole(url, role.id)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Role deleted!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let ul: Element
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ul.addEventListener("click", onClick)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
|
||||||
|
<li>
|
||||||
|
<Button onclick={addMembers}>
|
||||||
|
<Icon icon={AddCircle} />
|
||||||
|
Add members
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Button onclick={editRole}>
|
||||||
|
<Icon icon={Pen} />
|
||||||
|
Edit role
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={confirmDelete}>
|
||||||
|
<Icon icon={TrashBin} />
|
||||||
|
Delete role
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
|
||||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
|
||||||
import TrashBin from "@assets/icons/trash-bin-2.svg?dataurl"
|
|
||||||
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 Popover from "@lib/components/Popover.svelte"
|
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||||
import Confirm from "@lib/components/Confirm.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"
|
||||||
|
|
@ -17,12 +12,10 @@
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import RoleCreate from "@app/components/RoleCreate.svelte"
|
import RoleCreate from "@app/components/RoleCreate.svelte"
|
||||||
import RoleEdit from "@app/components/RoleEdit.svelte"
|
|
||||||
import RoleAddMembers from "@app/components/RoleAddMembers.svelte"
|
|
||||||
import RoleItem from "@app/components/RoleItem.svelte"
|
import RoleItem from "@app/components/RoleItem.svelte"
|
||||||
import {deriveSpaceRoles, deleteRole, type SpaceRole} from "@app/members"
|
import SpaceRoleMenu from "@app/components/SpaceRoleMenu.svelte"
|
||||||
|
import {deriveSpaceRoles} from "@app/members"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -32,41 +25,9 @@
|
||||||
|
|
||||||
const roles = deriveSpaceRoles(url)
|
const roles = deriveSpaceRoles(url)
|
||||||
|
|
||||||
let menuRoleId = $state<string | undefined>()
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const closeMenu = () => (menuRoleId = undefined)
|
|
||||||
|
|
||||||
const createRole = () => pushModal(RoleCreate, {url})
|
const createRole = () => pushModal(RoleCreate, {url})
|
||||||
|
|
||||||
const editRole = (role: SpaceRole) => {
|
|
||||||
menuRoleId = undefined
|
|
||||||
pushModal(RoleEdit, {url, role})
|
|
||||||
}
|
|
||||||
|
|
||||||
const addMembers = (role: SpaceRole) => {
|
|
||||||
menuRoleId = undefined
|
|
||||||
pushModal(RoleAddMembers, {url, role})
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmDelete = (role: SpaceRole) => {
|
|
||||||
menuRoleId = undefined
|
|
||||||
pushModal(Confirm, {
|
|
||||||
title: "Delete Role",
|
|
||||||
message: `Delete the "${role.label}" role? Members will keep their space membership.`,
|
|
||||||
confirm: async () => {
|
|
||||||
const error = await deleteRole(url, role.id)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "Role deleted!"})
|
|
||||||
back()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
|
|
@ -84,38 +45,11 @@
|
||||||
{#each $roles as role (role.id)}
|
{#each $roles as role (role.id)}
|
||||||
<div class="card card-sm flex justify-between gap-2">
|
<div class="card card-sm flex justify-between gap-2">
|
||||||
<RoleItem {role} />
|
<RoleItem {role} />
|
||||||
<div class="relative shrink-0">
|
<div class="shrink-0">
|
||||||
<Button
|
<MenuButton
|
||||||
class="button button-ghost button-sm button-square"
|
class="button button-ghost button-sm button-square"
|
||||||
onclick={() => (menuRoleId = menuRoleId === role.id ? undefined : role.id)}>
|
component={SpaceRoleMenu}
|
||||||
<Icon icon={MenuDots} />
|
componentProps={{url, role}} />
|
||||||
</Button>
|
|
||||||
{#if menuRoleId === role.id}
|
|
||||||
<Popover hideOnClick onClose={closeMenu}>
|
|
||||||
<ul
|
|
||||||
transition:fly
|
|
||||||
class="menu bg-surface absolute right-0 z-popover mt-2 w-52 gap-1 rounded-2xl p-2">
|
|
||||||
<li>
|
|
||||||
<Button onclick={() => addMembers(role)}>
|
|
||||||
<Icon icon={AddCircle} />
|
|
||||||
Add members
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Button onclick={() => editRole(role)}>
|
|
||||||
<Icon icon={Pen} />
|
|
||||||
Edit role
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Button class="text-error" onclick={() => confirmDelete(role)}>
|
|
||||||
<Icon icon={TrashBin} />
|
|
||||||
Delete role
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Popover>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
40
src/lib/components/MenuButton.svelte
Normal file
40
src/lib/components/MenuButton.svelte
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type {Component} from "svelte"
|
||||||
|
import type {Instance} from "tippy.js"
|
||||||
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
component: Component<any>
|
||||||
|
componentProps?: Record<string, any>
|
||||||
|
class?: string
|
||||||
|
iconSize?: number
|
||||||
|
"aria-label"?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
component,
|
||||||
|
componentProps = {},
|
||||||
|
class: className = "button button-ghost button-sm button-circle",
|
||||||
|
iconSize,
|
||||||
|
"aria-label": ariaLabel,
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
|
const showPopover = () => popover?.show()
|
||||||
|
|
||||||
|
const hidePopover = () => popover?.hide()
|
||||||
|
|
||||||
|
let popover: Instance | undefined = $state()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button class={className} aria-label={ariaLabel} onclick={showPopover}>
|
||||||
|
<Tippy
|
||||||
|
bind:popover
|
||||||
|
{component}
|
||||||
|
props={{...componentProps, onClick: hidePopover}}
|
||||||
|
params={{trigger: "manual", interactive: true, placement: "bottom-end"}}>
|
||||||
|
<Icon icon={MenuDots} size={iconSize} />
|
||||||
|
</Tippy>
|
||||||
|
</Button>
|
||||||
Loading…
Reference in a new issue