From 7cb3feeeb3246988435685d09d2f0eb4eacaad5e Mon Sep 17 00:00:00 2001 From: Coracle-Bot Date: Sat, 19 Sep 2026 18:43:01 +0000 Subject: [PATCH] Let a room admin delete a message with a NIP-29 delete-event op --- .../components/EventAdminDeleteConfirm.svelte | 49 +++++++++++++++++++ src/app/components/EventMenu.svelte | 29 +++-------- src/app/components/RoomItemMenu.svelte | 32 ++++-------- src/app/components/RoomItemMenuMobile.svelte | 11 ++++- src/app/rooms.ts | 31 +++++++++++- 5 files changed, 103 insertions(+), 49 deletions(-) create mode 100644 src/app/components/EventAdminDeleteConfirm.svelte diff --git a/src/app/components/EventAdminDeleteConfirm.svelte b/src/app/components/EventAdminDeleteConfirm.svelte new file mode 100644 index 00000000..0a6ab179 --- /dev/null +++ b/src/app/components/EventAdminDeleteConfirm.svelte @@ -0,0 +1,49 @@ + + + diff --git a/src/app/components/EventMenu.svelte b/src/app/components/EventMenu.svelte index 40ab7529..b15f4e42 100644 --- a/src/app/components/EventMenu.svelte +++ b/src/app/components/EventMenu.svelte @@ -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 - {#if canBanEvent} + {#if $adminDelete}
  • - {#if canBanEvent} + {#if $adminDelete}
  • + {:else if $adminDelete} + {/if} {/if} diff --git a/src/app/rooms.ts b/src/app/rooms.ts index 7ee1d199..b2f4427b 100644 --- a/src/app/rooms.ts +++ b/src/app/rooms.ts @@ -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 => { + 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(