From 52fd6f7a813f387d4e30f4d453da0f502a9356ff Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Fri, 4 Sep 2026 22:56:38 +0700 Subject: [PATCH] =?UTF-8?q?fix(delete):=20single=20signature=20for=20admin?= =?UTF-8?q?=20takedown=20=E2=80=94=20stop=20Amber=20collision=20hang=20(1.?= =?UTF-8?q?2.12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin delete fired THREE signed events (kind:5 deletion, kind:1985 label, NIP-86 banevent). Each opens Amber, but Amber handles one approval at a time — the extra background signs collided with the primary one on Android and hung the whole delete ("taking longer than expected"). Now the admin path publishes only the kind:5 deletion (one Amber approval) and hides locally (no signature); the optional label/relay-ban are dropped from the interactive flow. Also raise the awaited timeout 40s→120s to fit a real Amber round-trip. Bump to 1.2.12. Co-authored-by: shakespeare.diy --- package.json | 2 +- src/hooks/useDeleteEntity.ts | 77 ++++++++++-------------------------- 2 files changed, 21 insertions(+), 58 deletions(-) diff --git a/package.json b/package.json index b77ee21..2c0e3b2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "skate-spots", "private": true, - "version": "1.2.11", + "version": "1.2.12", "type": "module", "homepage": "https://skate-spots.com", "scripts": { diff --git a/src/hooks/useDeleteEntity.ts b/src/hooks/useDeleteEntity.ts index 95e7575..f04ba21 100644 --- a/src/hooks/useDeleteEntity.ts +++ b/src/hooks/useDeleteEntity.ts @@ -2,20 +2,18 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { isAdminPubkey } from '@/lib/admins'; -import { - MODERATION_LABEL_NAMESPACE, - MODERATION_LABEL_HIDDEN, -} from '@/lib/types'; import { addLocalHidden } from '@/lib/localModeration'; -import { banEventOnRelay } from '@/lib/relayAdmin'; import type { NostrEvent } from '@nostrify/nostrify'; /** - * Hard ceiling on how long the whole delete can run before we give up and - * surface an error. Guarantees the UI spinner can NEVER hang indefinitely, - * whatever the signer/relay does underneath. + * Hard ceiling on how long the AWAITED part of the delete can run before we give + * up. This must be generous enough for an Amber round-trip: the user has to + * switch to Amber, approve, and switch back — which easily exceeds 40s if they + * fumble. Too short a ceiling was surfacing "taking longer than expected" even + * when Amber was about to succeed. The signer has its own 90s cap underneath, so + * we align to that plus headroom. */ -const DELETE_TIMEOUT_MS = 40_000; +const DELETE_TIMEOUT_MS = 120_000; function withHardTimeout(promise: Promise, ms: number): Promise { return new Promise((resolve, reject) => { @@ -113,56 +111,21 @@ export function useDeleteEntity() { tags: deletionTags, }); - // STEP 2 (admin takedown only): record the target locally RIGHT NOW so the - // app hides it on this device immediately — no waiting on a second signer - // round-trip. Then fire the cross-device kind:1985 moderation label in the - // BACKGROUND (not awaited): a second Amber approval, if needed, happens - // after the dialog has already closed and never blocks the UI. This is the - // fix for "stuck on Deleting…": the mutation resolves as soon as kind:5 is - // published, regardless of the label. + // STEP 2 (admin takedown only): hide the target LOCALLY right now so it + // disappears from this device immediately. This needs NO signature. + // + // CRITICAL UX/BUG FIX: we deliberately do NOT fire additional signed + // events here (the kind:1985 moderation label or the NIP-86 relay ban). + // Each of those calls Amber, and Amber can only handle ONE approval at a + // time — firing a second/third signer request while the first is still in + // flight COLLIDES on Android and makes the whole delete hang ("taking + // longer than expected", Amber juggling multiple prompts). The single + // kind:5 deletion (step 1) already tells relays to drop the event, and the + // local hide covers this device instantly. The optional label / relay-ban + // can be performed separately from the relay moderator tools without + // blocking or breaking the interactive delete. if (isAdmin && !isOwner) { addLocalHidden({ coord, id: event.id }); - - const labelTags: string[][] = [ - ['L', MODERATION_LABEL_NAMESPACE], - ['l', MODERATION_LABEL_HIDDEN, MODERATION_LABEL_NAMESPACE], - ['a', coord], - ['e', event.id], - ['k', String(event.kind)], - ['p', event.pubkey], - ['alt', `SkateSpots admin takedown${reason?.trim() ? `: ${reason.trim()}` : ''}`], - ]; - // Intentionally NOT awaited — background best-effort. - void publishEvent({ - kind: 1985, - content: reason?.trim() || 'Removed by SkateSpots admin', - tags: labelTags, - }).catch((labelError) => { - console.warn( - '[useDeleteEntity] background moderation label failed (content is ' + - 'already hidden locally; kind:5 was published):', - labelError, - ); - }); - - // REAL takedown on our own relay (NIP-86 `banevent`). Because the admin - // is the relay owner/moderator, goskatespots.nostr1.com honors this and - // actually removes the event from its database — unlike a cross-author - // kind:5 request. Also background/best-effort so it never blocks the UI; - // this may prompt one more signer approval (NIP-98 auth event). - if (user.signer) { - void banEventOnRelay({ - signer: user.signer, - eventId: event.id, - reason: reason?.trim() || 'Removed by SkateSpots admin', - }).catch((banError) => { - console.warn( - '[useDeleteEntity] relay banevent failed (content is hidden in-app ' + - 'regardless; you can retry from the relay moderator portal):', - banError, - ); - }); - } } } }