import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { isAdminPubkey } from '@/lib/admins'; import { addLocalHidden } from '@/lib/localModeration'; import { toast } from '@/hooks/useToast'; import type { NostrEvent } from '@nostrify/nostrify'; /** * DIAGNOSTIC DELETE * ================= * We could not identify WHERE the delete hangs (signing? relay publish?), so * this version narrates every step on-screen via toasts and splits the flow into * explicit, independently-timed phases: * * 1. SIGN — call the signer (Amber). Own timeout. If it hangs here, Amber * never returned (app↔Amber problem). * 2. PUBLISH — send the signed event to relays. Own timeout. If it hangs here, * the relay is the problem, not Amber. * * Whatever step is on screen when it freezes tells us EXACTLY what to fix. */ const SIGN_TIMEOUT_MS = 90_000; // matches the signer's own cap const PUBLISH_TIMEOUT_MS = 20_000; // relays should answer well within this function withTimeout(promise: Promise, ms: number, label: string): Promise { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error(`${label} timed out after ${Math.round(ms / 1000)}s.`)); }, ms); promise.then( (v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); }, ); }); } export function useDeleteEntity() { const { nostr } = useNostr(); const { user } = useCurrentUser(); const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ event, reason, }: { event: NostrEvent; reason?: string; }) => runDelete(event, reason), onSuccess: () => { queryClient.invalidateQueries(); }, }); async function runDelete(event: NostrEvent, reason?: string): Promise { if (!user) throw new Error('You must be logged in to delete.'); const isOwner = user.pubkey === event.pubkey; const isAdmin = isAdminPubkey(user.pubkey); if (!isOwner && !isAdmin) { throw new Error('You can only delete your own listing.'); } const dTag = event.tags.find(([name]) => name === 'd')?.[1] ?? ''; const coord = `${event.kind}:${event.pubkey}:${dTag}`; const deletionTags: string[][] = [ ['a', coord], ['e', event.id], ['k', String(event.kind)], ]; const template = { kind: 5, content: reason?.trim() || '', tags: deletionTags, created_at: Math.floor(Date.now() / 1000), }; // ── PHASE 1: SIGN ──────────────────────────────────────────────────────── // With the Amethyst-style foreground signer, Amber pops up its Approve/Reject // screen directly — no more silent ContentProvider stall. The signer has its // own graceful timeout, but we keep an outer guard as a belt-and-braces. toast({ title: 'Approve in Amber', description: 'Amber will open — tap Approve to confirm the deletion.', }); if (!user.signer) { throw new Error('No signer available. Please log in again.'); } let signed: NostrEvent; try { signed = await withTimeout( user.signer.signEvent(template), SIGN_TIMEOUT_MS, 'Signing (Amber)', ); } catch (e) { const msg = e instanceof Error ? e.message : String(e); // Distinguish a deliberate rejection from a real failure. if (/reject/i.test(msg)) { toast({ title: 'Cancelled', description: 'You rejected the request in Amber.' }); throw new Error('Signing cancelled.'); } toast({ title: "Couldn't sign", description: msg, variant: 'destructive', }); throw new Error(`SIGN step failed: ${msg}`); } // ── PHASE 2: PUBLISH ───────────────────────────────────────────────────── toast({ title: 'Step 2/2: Publishing…', description: 'Sending the deletion to relays.', }); try { await withTimeout( nostr.event(signed), PUBLISH_TIMEOUT_MS, 'Publishing (relays)', ); } catch (e) { const msg = e instanceof Error ? e.message : String(e); // The event was SIGNED fine — the relay is the slow/failing part. Hide it // locally so the user's action still takes visible effect on this device. if (isAdmin && !isOwner) addLocalHidden({ coord, id: event.id }); toast({ title: 'Stuck at PUBLISHING', description: `Relay step failed: ${msg}. It was signed OK; the relay didn't confirm.`, variant: 'destructive', }); throw new Error(`PUBLISH step failed: ${msg}`); } // Admin: hide locally too (no signature needed). if (isAdmin && !isOwner) { addLocalHidden({ coord, id: event.id }); } toast({ title: 'Deleted', description: 'The deletion was signed and published.' }); } }