diff --git a/src/components/zap/ZapHistoryView.tsx b/src/components/zap/ZapHistoryView.tsx index 44cb07e..185b71e 100644 --- a/src/components/zap/ZapHistoryView.tsx +++ b/src/components/zap/ZapHistoryView.tsx @@ -1,8 +1,8 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { NDKEvent } from "@nostr-dev-kit/ndk"; import { useUserStore } from "../../stores/user"; import { useUIStore } from "../../stores/ui"; -import { fetchZapsReceived, fetchZapsSent } from "../../lib/nostr"; +import { fetchZapsReceived, fetchZapsSent, fetchNoteById } from "../../lib/nostr"; import { useProfile } from "../../hooks/useProfile"; import { timeAgo, shortenPubkey } from "../../lib/utils"; @@ -39,21 +39,53 @@ function ZapRow({ pubkey, amount, comment, + noteId, createdAt, direction, }: { pubkey: string | null; amount: number | null; comment: string; + noteId: string | null; createdAt: number; direction: "received" | "sent"; }) { - const { openProfile } = useUIStore(); + const { openProfile, openThread } = useUIStore(); const profile = useProfile(pubkey ?? ""); const name = pubkey ? profile?.displayName || profile?.name || shortenPubkey(pubkey) : "anonymous"; const avatar = profile?.picture; + const [notePreview, setNotePreview] = useState(null); + const [noteEvent, setNoteEvent] = useState(null); + const [loadingNote, setLoadingNote] = useState(false); + + useEffect(() => { + if (!noteId) return; + fetchNoteById(noteId).then((event) => { + if (event) { + setNoteEvent(event); + setNotePreview(event.content?.slice(0, 120) || null); + } + }).catch(() => {}); + }, [noteId]); + + const handleNoteClick = useCallback(async () => { + if (noteEvent) { + openThread(noteEvent); + return; + } + if (!noteId || loadingNote) return; + setLoadingNote(true); + try { + const event = await fetchNoteById(noteId); + if (event) { + setNoteEvent(event); + openThread(event); + } + } catch { /* ignore */ } + finally { setLoadingNote(false); } + }, [noteId, noteEvent, loadingNote, openThread]); return (
@@ -92,6 +124,22 @@ function ZapRow({ {comment && (

{comment}

)} + {noteId && ( + + )}
); @@ -202,13 +250,14 @@ export function ZapHistoryView() { {!loading && activeEvents.map((event) => { if (tab === "received") { - const { amount, senderPubkey, comment } = parseReceipt(event); + const { amount, senderPubkey, comment, noteId } = parseReceipt(event); return ( @@ -216,13 +265,14 @@ export function ZapHistoryView() { } else { // Sent zaps are also kind 9735 receipts; the recipient is in the lowercase "p" tag const recipientPubkey = event.tags.find((t) => t[0] === "p")?.[1] ?? null; - const { amount, comment } = parseReceipt(event); + const { amount, comment, noteId } = parseReceipt(event); return ( diff --git a/src/lib/lightning/nwc.ts b/src/lib/lightning/nwc.ts index 409ac90..95e53bc 100644 --- a/src/lib/lightning/nwc.ts +++ b/src/lib/lightning/nwc.ts @@ -32,7 +32,7 @@ export function isValidNwcUri(uri: string): boolean { export async function payInvoiceViaNWC(nwcUri: string, bolt11: string): Promise { const { walletPubkey, relayUrl, secret } = parseNwcUri(nwcUri); - const ndk = new NDK({ explicitRelayUrls: [relayUrl] }); + const ndk = new NDK({ explicitRelayUrls: [relayUrl], enableOutboxModel: false }); const signer = new NDKPrivateKeySigner(secret); ndk.signer = signer; await ndk.connect(); diff --git a/src/lib/nostr/core.ts b/src/lib/nostr/core.ts index efda371..7dfba5e 100644 --- a/src/lib/nostr/core.ts +++ b/src/lib/nostr/core.ts @@ -44,6 +44,13 @@ export const FALLBACK_RELAYS = [ "wss://relay.snort.social", ]; +// Override NDK's default outbox relays (purplepag.es can have DNS issues) +export const OUTBOX_RELAYS = [ + "wss://relay.damus.io/", + "wss://nos.lol/", + "wss://relay.nostr.band/", +]; + export function getStoredRelayUrls(): string[] { try { const stored = localStorage.getItem(RELAY_STORAGE_KEY); @@ -63,6 +70,7 @@ export function getNDK(): NDK { if (!ndk) { ndk = new NDK({ explicitRelayUrls: getStoredRelayUrls(), + outboxRelayUrls: OUTBOX_RELAYS, }); ndkCreatedAt = Date.now(); } @@ -92,6 +100,7 @@ export async function resetNDK(): Promise { // Create fresh instance ndk = new NDK({ explicitRelayUrls: getStoredRelayUrls(), + outboxRelayUrls: OUTBOX_RELAYS, }); ndkCreatedAt = Date.now(); diff --git a/src/lib/notificationPoller.ts b/src/lib/notificationPoller.ts index 884383e..582b449 100644 --- a/src/lib/notificationPoller.ts +++ b/src/lib/notificationPoller.ts @@ -89,6 +89,10 @@ async function pollOnce(pubkey: string) { const newFollowers = followers.filter((e) => e.pubkey !== pubkey && !existingFollowerPubkeys.has(e.pubkey)); if (newFollowers.length > 0) { dbSaveNotifications(newFollowers.map((e) => JSON.stringify(e.rawEvent())), pubkey, "follower"); + // Add to in-memory store so next poll cycle's pubkey dedup catches them + const store = useNotificationsStore.getState(); + const updated = [...store.notifications, ...newFollowers]; + useNotificationsStore.setState({ notifications: updated }); for (const e of newFollowers) { const name = await getProfileName(e.pubkey); notifyFollower(name).catch(() => {}); diff --git a/src/stores/lightning.ts b/src/stores/lightning.ts index 79dea6f..e7e9d67 100644 --- a/src/stores/lightning.ts +++ b/src/stores/lightning.ts @@ -78,7 +78,12 @@ export const useLightningStore = create(() => ({ }); return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Zap timed out after 45 seconds")); + }, 45000); + zapper.on("complete", (results) => { + clearTimeout(timeout); const errors = Array.from(results.values()).filter((r) => r instanceof Error); if (errors.length > 0) { reject(errors[0]); @@ -87,7 +92,12 @@ export const useLightningStore = create(() => ({ } }); - zapper.zap().catch(reject); + zapper.zap().then(() => { + // zap() resolved but "complete" event handles the result + }).catch((err) => { + clearTimeout(timeout); + reject(err); + }); }); }, }));