import { useNostr } from "@nostrify/react"; import { useMutation, type UseMutationResult } from "@tanstack/react-query"; import { useCurrentUser } from "./useCurrentUser"; import { isOffline, queueSignedEvent } from "@/lib/offlineCache"; import { minePow } from "@/lib/pow"; import { toast } from "@/hooks/useToast"; import type { NostrEvent, NPool } from "@nostrify/nostrify"; /** * Keep a signed event for delivery when we're back online. Signing already * happened (above), so an interactive signer is NOT prompted again later — we * just hand the signature over. */ async function queueForLater(event: NostrEvent): Promise { try { await queueSignedEvent(event); toast({ title: 'Saved offline', description: "We'll publish it automatically when you're back online.", }); return true; } catch { return false; } } /** * Publishes a Nostr event with retry logic and improved error handling. * * If the relays can't be reached, the SIGNED event is queued (see * `lib/offlineCache`) and delivered by `useSyncPendingPosts()` on reconnect — * so an offline action completes instead of being lost. */ export function useNostrPublish(): UseMutationResult { const { nostr } = useNostr(); const { user } = useCurrentUser(); return useMutation({ mutationFn: async (t: Omit & { pow?: number }) => { if (!user) { throw new Error("You must be logged in to publish events"); } const tags = [...(t.tags ?? [])]; // Add the client tag if it doesn't exist if (location.protocol === "https:" && !tags.some(([name]) => name === "client")) { tags.push(["client", location.hostname]); } const createdAt = t.created_at ?? Math.floor(Date.now() / 1000); // Optional NIP-13 proof of work. Mined on the UNSIGNED event — the id does // not depend on the signature — so the signer is still prompted only once. let finalTags = tags; if (t.pow && t.pow > 0) { const { nonce } = await minePow( { pubkey: user.pubkey, kind: t.kind, created_at: createdAt, tags, content: t.content ?? "" }, t.pow, ); finalTags = [...tags.filter(([n]) => n !== "nonce"), ["nonce", nonce, String(t.pow)]]; } // Sign the event const event = await user.signer.signEvent({ kind: t.kind, content: t.content ?? "", tags: finalTags, created_at: createdAt, }); // Offline: don't sit through a 15s publish timeout before giving up. if (isOffline()) { await queueForLater(event); return event; } try { await publishEventWithRetry(nostr, event); } catch (err) { // Relays unreachable — keep it and deliver later, unless queuing failed // (then the real error is the honest thing to surface). const queued = await queueForLater(event); if (!queued) throw err; } return event; }, onError: (error) => { console.error("Failed to publish event:", error); }, }); } /** * Publishes event with retry logic and partial success handling */ async function publishEventWithRetry( nostr: NPool, event: NostrEvent, maxRetries = 1, timeoutMs = 15000 ): Promise { let lastError: Error | null = null; // Try publishing with retries for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { // Attempt to publish await nostr.event(event, { signal: controller.signal }); return; // Success! } finally { clearTimeout(timeoutId); } } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // If this isn't the last attempt, wait before retrying if (attempt < maxRetries) { const backoffMs = 1000 * Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, backoffMs)); } } } // All retries failed if (lastError) { // Provide user-friendly error messages if (lastError.name === 'AbortError' || lastError.message.includes('timeout')) { throw new Error( 'Publishing timed out. Your relays may be slow or unreachable. ' + 'Try checking your relay connections in Settings > Relays.' ); } else if (lastError.message.includes('network') || lastError.message.includes('fetch')) { throw new Error( 'Network error while publishing. Please check your internet connection and try again.' ); } else { throw new Error( `Failed to publish after ${maxRetries + 1} attempts: ${lastError.message}` ); } } throw new Error('Failed to publish event to any relay'); }