Let a room admin delete a message with a NIP-29 delete-event op
This commit is contained in:
parent
3f57607780
commit
7cb3feeeb3
5 changed files with 103 additions and 49 deletions
49
src/app/components/EventAdminDeleteConfirm.svelte
Normal file
49
src/app/components/EventAdminDeleteConfirm.svelte
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<script lang="ts">
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {tagSpec, tagValue} from "@welshman/util"
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import {app, relayManagement, rooms} from "@app/core"
|
||||
import {AdminDelete, deriveUserAdminDelete} from "@app/rooms"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
noun: string
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
const {url, noun, event}: Props = $props()
|
||||
|
||||
const h = tagValue(tagSpec("h"), event.tags) ?? ""
|
||||
const adminDelete = deriveUserAdminDelete(url, event)
|
||||
const scope = $derived($adminDelete === AdminDelete.Room ? "room" : "space")
|
||||
|
||||
const remove = async () => {
|
||||
if ($adminDelete === AdminDelete.Room) {
|
||||
const command = await $rooms.deleteEvent(url, {h}, event.id)
|
||||
|
||||
return command.publish().waitForError()
|
||||
}
|
||||
|
||||
const {error} = await $relayManagement.forUrl(url).banEvent(event.id)
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
const confirm = async () => {
|
||||
const error = await remove()
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
pushToast({message: `${noun} has successfully been deleted!`})
|
||||
$app.repository.removeEvent(event.id)
|
||||
history.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Confirm
|
||||
{confirm}
|
||||
title="Delete {noun}"
|
||||
message="Are you sure you want to delete this {noun.toLowerCase()} from the {scope}?" />
|
||||
|
|
@ -9,17 +9,16 @@
|
|||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import Danger from "@assets/icons/danger.svg?dataurl"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import Report from "@app/components/Report.svelte"
|
||||
import EventAdminDeleteConfirm from "@app/components/EventAdminDeleteConfirm.svelte"
|
||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||
import PinboardSelect from "@app/components/PinboardSelect.svelte"
|
||||
import {shareEvent} from "@app/share"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveUserAdminDelete} from "@app/rooms"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {app, relayManagement, user} from "@app/core"
|
||||
import {user} from "@app/core"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
|
|
@ -32,8 +31,7 @@
|
|||
const {url, noun, event, onClick, customActions}: Props = $props()
|
||||
|
||||
const isRoot = event.kind !== COMMENT
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canBanEvent = $derived($supportedMethods.includes("banevent"))
|
||||
const adminDelete = deriveUserAdminDelete(url, event)
|
||||
|
||||
const report = () => pushModal(Report, {url, event})
|
||||
|
||||
|
|
@ -45,22 +43,7 @@
|
|||
|
||||
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
||||
|
||||
const showAdminDelete = () =>
|
||||
pushModal(Confirm, {
|
||||
title: `Delete ${noun}`,
|
||||
message: `Are you sure you want to delete this ${noun.toLowerCase()} from the space?`,
|
||||
confirm: async () => {
|
||||
const {error} = await $relayManagement.forUrl(url).banEvent(event.id)
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
} else {
|
||||
pushToast({message: "Event has successfully been deleted!"})
|
||||
$app.repository.removeEvent(event.id)
|
||||
history.back()
|
||||
}
|
||||
},
|
||||
})
|
||||
const showAdminDelete = () => pushModal(EventAdminDeleteConfirm, {url, noun, event})
|
||||
|
||||
let ul: Element
|
||||
|
||||
|
|
@ -105,7 +88,7 @@
|
|||
Report Content
|
||||
</Button>
|
||||
</li>
|
||||
{#if canBanEvent}
|
||||
{#if $adminDelete}
|
||||
<li>
|
||||
<Button class="text-error" onclick={showAdminDelete}>
|
||||
<Icon size={4} icon={TrashBin2} />
|
||||
|
|
|
|||
|
|
@ -12,15 +12,14 @@
|
|||
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import Report from "@app/components/Report.svelte"
|
||||
import PinboardSelect from "@app/components/PinboardSelect.svelte"
|
||||
import EventAdminDeleteConfirm from "@app/components/EventAdminDeleteConfirm.svelte"
|
||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
||||
import {app, relayManagement, roomPinLists, user} from "@app/core"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
import {deriveUserIsRoomAdmin} from "@app/rooms"
|
||||
import {roomPinLists, user} from "@app/core"
|
||||
import {deriveUserAdminDelete, deriveUserIsRoomAdmin} from "@app/rooms"
|
||||
import {shareEvent} from "@app/share"
|
||||
import {readAloud} from "@app/speech"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
|
@ -36,10 +35,9 @@
|
|||
|
||||
const h = tagValue(tagSpec("h"), event.tags) ?? ""
|
||||
const pinIds = $roomPinLists.pins(url, h).$
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const userIsRoomAdmin = deriveUserIsRoomAdmin(url, h)
|
||||
const adminDelete = deriveUserAdminDelete(url, event)
|
||||
const isPinned = $derived($pinIds.includes(event.id))
|
||||
const canBanEvent = $derived($supportedMethods.includes("banevent"))
|
||||
|
||||
const share = () => {
|
||||
onClick()
|
||||
|
|
@ -80,22 +78,10 @@
|
|||
pushModal(EventDeleteConfirm, {url, event})
|
||||
}
|
||||
|
||||
const showAdminDelete = () =>
|
||||
pushModal(Confirm, {
|
||||
title: `Delete Message`,
|
||||
message: `Are you sure you want to delete this message from the space?`,
|
||||
confirm: async () => {
|
||||
const {error} = await $relayManagement.forUrl(url).banEvent(event.id)
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
} else {
|
||||
pushToast({message: "Event has successfully been deleted!"})
|
||||
$app.repository.removeEvent(event.id)
|
||||
history.back()
|
||||
}
|
||||
},
|
||||
})
|
||||
const showAdminDelete = () => {
|
||||
onClick()
|
||||
pushModal(EventAdminDeleteConfirm, {url, noun: "Message", event})
|
||||
}
|
||||
|
||||
const togglePin = async () => {
|
||||
onClick()
|
||||
|
|
@ -174,7 +160,7 @@
|
|||
Report Content
|
||||
</Button>
|
||||
</li>
|
||||
{#if canBanEvent}
|
||||
{#if $adminDelete}
|
||||
<li>
|
||||
<Button class="text-error" onclick={showAdminDelete}>
|
||||
<Icon size={4} icon={TrashBin2} />
|
||||
|
|
|
|||
|
|
@ -25,10 +25,11 @@
|
|||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||
import ZapButton from "@app/components/ZapButton.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import EventAdminDeleteConfirm from "@app/components/EventAdminDeleteConfirm.svelte"
|
||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
||||
import {reactions, relays, roomPinLists, user} from "@app/core"
|
||||
import {deriveUserIsRoomAdmin} from "@app/rooms"
|
||||
import {deriveUserAdminDelete, deriveUserIsRoomAdmin} from "@app/rooms"
|
||||
import {ENABLE_ZAPS} from "@app/env"
|
||||
import {makeContentPath} from "@app/routes"
|
||||
import {shareEvent} from "@app/share"
|
||||
|
|
@ -49,6 +50,7 @@
|
|||
const path = makeContentPath(url, event.kind, getIdOrAddress(event))
|
||||
const pinIds = $roomPinLists.pins(url, h).$
|
||||
const userIsRoomAdmin = deriveUserIsRoomAdmin(url, h)
|
||||
const adminDelete = deriveUserAdminDelete(url, event)
|
||||
const isPinned = $derived($pinIds.includes(event.id))
|
||||
const tile = "button h-auto flex-col gap-1.5 py-4 text-xs"
|
||||
|
||||
|
|
@ -98,6 +100,8 @@
|
|||
|
||||
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
||||
|
||||
const showAdminDelete = () => pushModal(EventAdminDeleteConfirm, {url, noun: "Message", event})
|
||||
|
||||
const toggleMore = () => {
|
||||
showMore = !showMore
|
||||
}
|
||||
|
|
@ -192,6 +196,11 @@
|
|||
<Icon size={4} icon={TrashBin2} />
|
||||
Delete Message
|
||||
</Button>
|
||||
{:else if $adminDelete}
|
||||
<Button class="button button-neutral w-full text-error" onclick={showAdminDelete}>
|
||||
<Icon size={4} icon={TrashBin2} />
|
||||
Delete Message
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as nip19 from "nostr-tools/nip19"
|
|||
import {derived, get} from "svelte/store"
|
||||
import {formatTimestampAsDate, int, sortBy, uniq, MINUTE} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {outbox, relay, seen, toNostrURI} from "@welshman/util"
|
||||
import {outbox, relay, seen, tagSpec, tagValue, toNostrURI} from "@welshman/util"
|
||||
import type {EventContent, TrustedEvent} from "@welshman/util"
|
||||
import {Message} from "@welshman/domain"
|
||||
import {MembershipStatus, RoomLists, makeRoomKey, createSearch, publish} from "@welshman/app"
|
||||
|
|
@ -19,7 +19,7 @@ import {
|
|||
user,
|
||||
writer,
|
||||
} from "@app/core"
|
||||
import {deriveUserIsSpaceStaff} from "@app/management"
|
||||
import {deriveSpaceSupportedMethods, deriveUserIsSpaceStaff} from "@app/management"
|
||||
import {makeRoomPath} from "@app/routes"
|
||||
|
||||
export const PROTECTED = ["-"]
|
||||
|
|
@ -173,6 +173,33 @@ export const deriveUserIsRoomAdmin = (url: string, h: string) =>
|
|||
$isStaff || Boolean($room?.admins?.pubkeys().includes($user.pubkey)),
|
||||
)
|
||||
|
||||
// Deleting someone else's content goes one of two ways, and a space answers for either one.
|
||||
export enum AdminDelete {
|
||||
Room = "room",
|
||||
Space = "space",
|
||||
}
|
||||
|
||||
// A room admin deletes over NIP-29, which is scoped to the room the content is in. NIP-86 is the
|
||||
// wider grant and the only one that reaches content belonging to no room, so it comes second.
|
||||
export const deriveUserAdminDelete = (url: string, event: TrustedEvent) => {
|
||||
const h = tagValue(tagSpec("h"), event.tags) ?? ""
|
||||
|
||||
return derived(
|
||||
[deriveUserIsRoomAdmin(url, h), deriveSpaceSupportedMethods(url)],
|
||||
([$isRoomAdmin, $methods]): Maybe<AdminDelete> => {
|
||||
if (h && $isRoomAdmin) {
|
||||
return AdminDelete.Room
|
||||
}
|
||||
|
||||
if ($methods.includes("banevent")) {
|
||||
return AdminDelete.Space
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Room membership is the relay's business, but a space admin outranks it.
|
||||
export const deriveUserRoomMembershipStatus = (url: string, h: string) =>
|
||||
derived(
|
||||
|
|
|
|||
Loading…
Reference in a new issue