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">
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {Address} from "@welshman/util"
|
||||
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 {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import MenuButton from "@lib/components/MenuButton.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 PinContentEvent from "@app/components/PinContentEvent.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"
|
||||
import PinMenu from "@app/components/PinMenu.svelte"
|
||||
import {type Pin} from "@app/pinboards"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
|
|
@ -42,61 +33,17 @@
|
|||
|
||||
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>
|
||||
|
||||
<div class="{props.class} relative flex h-full flex-col gap-2">
|
||||
{#if showMenu}
|
||||
<div class="absolute right-2 top-2 z-feature">
|
||||
<Button
|
||||
<MenuButton
|
||||
class="button button-neutral button-sm button-square"
|
||||
aria-label="More options"
|
||||
onclick={() => (menuOpen = !menuOpen)}>
|
||||
<Icon size={4} icon={MenuDots} />
|
||||
</Button>
|
||||
{#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}
|
||||
iconSize={4}
|
||||
component={PinMenu}
|
||||
componentProps={{url, pin}} />
|
||||
</div>
|
||||
{#if pin.title}
|
||||
<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 {Client} from "@pomade/core"
|
||||
import {session, isPomadeSession} from "@welshman/app"
|
||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||
import Badge from "@lib/components/Badge.svelte"
|
||||
import Popover from "@lib/components/Popover.svelte"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import PomadeSessionMenu from "@app/components/PomadeSessionMenu.svelte"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {loadOtherPomadeSessions} 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[]>([])
|
||||
|
||||
const deleteSession = async (sessionItem: PomadeSessionWithPeers) => {
|
||||
|
|
@ -83,27 +70,9 @@
|
|||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<Button
|
||||
class="button button-ghost button-sm button-circle"
|
||||
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>
|
||||
<MenuButton
|
||||
component={PomadeSessionMenu}
|
||||
componentProps={{onDelete: () => deleteSession(sessionItem)}} />
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<Badge variant="neutral">
|
||||
|
|
|
|||
|
|
@ -1,22 +1,7 @@
|
|||
<script lang="ts">
|
||||
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 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"
|
||||
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||
import ReportMenuList from "@app/components/ReportMenuList.svelte"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
|
|
@ -25,131 +10,6 @@
|
|||
}
|
||||
|
||||
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>
|
||||
|
||||
<div class="relative">
|
||||
<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>
|
||||
<MenuButton component={ReportMenuList} componentProps={{url, event, onResolved}} />
|
||||
|
|
|
|||
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">
|
||||
import {goto} from "$app/navigation"
|
||||
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 {displayRelayUrl} from "@welshman/util"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import EyeClosed from "@assets/icons/eye-closed.svg?dataurl"
|
||||
import Eye from "@assets/icons/eye.svg?dataurl"
|
||||
|
|
@ -17,32 +8,22 @@
|
|||
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Popover from "@lib/components/Popover.svelte"
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||
import Tooltip from "@lib/components/Tooltip.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ProfileCircles from "@app/components/ProfileCircles.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 RoomImage from "@app/components/RoomImage.svelte"
|
||||
import {deriveRoom, deriveUserRooms, addRoom, removeRoom} from "@app/groups"
|
||||
import {
|
||||
deriveRoomMembers,
|
||||
deriveUserIsRoomAdmin,
|
||||
deriveUserRoomMembershipStatus,
|
||||
MembershipStatus,
|
||||
} from "@app/members"
|
||||
import {deriveRoomMembers} from "@app/members"
|
||||
import {deriveShouldNotify, toggleRoomNotifications} from "@app/settings"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
|
|
@ -53,8 +34,6 @@
|
|||
|
||||
const room = deriveRoom(url, h)
|
||||
const members = deriveRoomMembers(url, h)
|
||||
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
|
||||
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
|
||||
const userRooms = deriveUserRooms(url)
|
||||
|
||||
const isFavorite = $derived($userRooms.includes(h))
|
||||
|
|
@ -62,34 +41,6 @@
|
|||
|
||||
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 toggleFavorite = () => {
|
||||
|
|
@ -103,28 +54,6 @@
|
|||
const toggleShouldNotify = () => {
|
||||
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>
|
||||
|
||||
<Modal>
|
||||
|
|
@ -139,62 +68,7 @@
|
|||
<span class="text-primary">{displayRelayUrl(url)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<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>
|
||||
<MenuButton component={RoomDetailMenu} componentProps={{url, h}} />
|
||||
</div>
|
||||
{#if $room?.about}
|
||||
<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">
|
||||
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 AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import {fly} from "@lib/transition"
|
||||
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 MenuButton from "@lib/components/MenuButton.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
|
|
@ -18,10 +13,9 @@
|
|||
import Profile from "@app/components/Profile.svelte"
|
||||
import RoomName from "@app/components/RoomName.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 {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
|
|
@ -30,39 +24,12 @@
|
|||
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const room = deriveRoom(url, h)
|
||||
const members = deriveRoomMembers(url, h)
|
||||
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const toggleMenu = (pubkey: string) => {
|
||||
menuPubkey = menuPubkey === pubkey ? undefined : pubkey
|
||||
}
|
||||
|
||||
const closeMenu = () => {
|
||||
menuPubkey = undefined
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<Modal>
|
||||
|
|
@ -89,27 +56,7 @@
|
|||
<div class="min-w-0 flex-1">
|
||||
<Profile {pubkey} {url} />
|
||||
</div>
|
||||
<div class="relative">
|
||||
<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>
|
||||
<MenuButton component={RoomMemberMenu} componentProps={{url, h, pubkey}} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,15 @@
|
|||
<script lang="ts">
|
||||
import {ManagementMethod} from "@welshman/util"
|
||||
import {displayProfileByPubkey} from "@welshman/app"
|
||||
import {fly} from "@lib/transition"
|
||||
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 MenuButton from "@lib/components/MenuButton.svelte"
|
||||
import Profile from "@app/components/Profile.svelte"
|
||||
import ProfileAbout from "@app/components/ProfileAbout.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 {removeSpaceMembers, banSpaceMembers, type SpaceRole} from "@app/members"
|
||||
import {type SpaceRole} from "@app/members"
|
||||
import {deriveSupportedMethods} from "@app/relays"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
|
|
@ -34,56 +25,7 @@
|
|||
const canAssign = $derived($supportedMethods.some(m => (m as string) === "assignrole"))
|
||||
const canUnassign = $derived($supportedMethods.some(m => (m as string) === "unassignrole"))
|
||||
|
||||
let menuOpen = $state(false)
|
||||
|
||||
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()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
||||
</script>
|
||||
|
||||
<div class="card card-interactive card-sm relative">
|
||||
|
|
@ -108,44 +50,11 @@
|
|||
</div>
|
||||
</div>
|
||||
{#if canAssign || canUnassign || canUnallow || canBan}
|
||||
<div class="pointer-events-auto relative shrink-0">
|
||||
<Button
|
||||
<div class="pointer-events-auto shrink-0">
|
||||
<MenuButton
|
||||
class="button button-ghost button-sm button-square"
|
||||
onclick={() => (menuOpen = !menuOpen)}>
|
||||
<Icon icon={MenuDots} />
|
||||
</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}
|
||||
component={SpaceMemberMenu}
|
||||
componentProps={{url, pubkey}} />
|
||||
</div>
|
||||
{/if}
|
||||
</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">
|
||||
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 {fly} from "@lib/transition"
|
||||
import Button from "@lib/components/Button.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 ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
|
|
@ -16,9 +11,9 @@
|
|||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.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 {pushToast} from "@app/toast"
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
|
|
@ -32,41 +27,6 @@
|
|||
const canRestore = $derived($supportedMethods.includes(ManagementMethod.AllowPubkey))
|
||||
|
||||
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>
|
||||
|
||||
<Modal>
|
||||
|
|
@ -86,37 +46,7 @@
|
|||
<Profile {pubkey} {url} />
|
||||
</div>
|
||||
{#if canUnban || canRestore}
|
||||
<div class="relative">
|
||||
<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>
|
||||
<MenuButton component={SpaceMemberBannedMenu} componentProps={{url, pubkey}} />
|
||||
{/if}
|
||||
</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">
|
||||
import {fly} from "@lib/transition"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.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 Icon from "@lib/components/Icon.svelte"
|
||||
import Popover from "@lib/components/Popover.svelte"
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
|
|
@ -17,12 +12,10 @@
|
|||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import RelayName from "@app/components/RelayName.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 {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 {pushToast} from "@app/toast"
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
|
|
@ -32,41 +25,9 @@
|
|||
|
||||
const roles = deriveSpaceRoles(url)
|
||||
|
||||
let menuRoleId = $state<string | undefined>()
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const closeMenu = () => (menuRoleId = undefined)
|
||||
|
||||
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>
|
||||
|
||||
<Modal>
|
||||
|
|
@ -84,38 +45,11 @@
|
|||
{#each $roles as role (role.id)}
|
||||
<div class="card card-sm flex justify-between gap-2">
|
||||
<RoleItem {role} />
|
||||
<div class="relative shrink-0">
|
||||
<Button
|
||||
<div class="shrink-0">
|
||||
<MenuButton
|
||||
class="button button-ghost button-sm button-square"
|
||||
onclick={() => (menuRoleId = menuRoleId === role.id ? undefined : role.id)}>
|
||||
<Icon icon={MenuDots} />
|
||||
</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}
|
||||
component={SpaceRoleMenu}
|
||||
componentProps={{url, role}} />
|
||||
</div>
|
||||
</div>
|
||||
{/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