fix(delete): single signature for admin takedown — stop Amber collision hang (1.2.12)

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 <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-04 22:56:38 +07:00
parent 955a823b02
commit 52fd6f7a81
2 changed files with 21 additions and 58 deletions

View file

@ -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": {

View file

@ -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<T>(promise: Promise<T>, ms: number): Promise<T> {
return new Promise<T>((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,
);
});
}
}
}
}