mirror of
https://github.com/hoornet/vega.git
synced 2026-05-09 05:39:10 -07:00
Fix zap hang from dead outbox relay, add note preview to zap history
Override NDK default outbox relays (purplepag.es DNS failure was stalling getZapInfo), add 45s zap timeout, disable outbox on NWC instance, fix follower notification dedup in poller. Zap history rows now show a clickable preview of the original zapped note.
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||||
import { useUserStore } from "../../stores/user";
|
import { useUserStore } from "../../stores/user";
|
||||||
import { useUIStore } from "../../stores/ui";
|
import { useUIStore } from "../../stores/ui";
|
||||||
import { fetchZapsReceived, fetchZapsSent } from "../../lib/nostr";
|
import { fetchZapsReceived, fetchZapsSent, fetchNoteById } from "../../lib/nostr";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
import { timeAgo, shortenPubkey } from "../../lib/utils";
|
import { timeAgo, shortenPubkey } from "../../lib/utils";
|
||||||
|
|
||||||
@@ -39,21 +39,53 @@ function ZapRow({
|
|||||||
pubkey,
|
pubkey,
|
||||||
amount,
|
amount,
|
||||||
comment,
|
comment,
|
||||||
|
noteId,
|
||||||
createdAt,
|
createdAt,
|
||||||
direction,
|
direction,
|
||||||
}: {
|
}: {
|
||||||
pubkey: string | null;
|
pubkey: string | null;
|
||||||
amount: number | null;
|
amount: number | null;
|
||||||
comment: string;
|
comment: string;
|
||||||
|
noteId: string | null;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
direction: "received" | "sent";
|
direction: "received" | "sent";
|
||||||
}) {
|
}) {
|
||||||
const { openProfile } = useUIStore();
|
const { openProfile, openThread } = useUIStore();
|
||||||
const profile = useProfile(pubkey ?? "");
|
const profile = useProfile(pubkey ?? "");
|
||||||
const name = pubkey
|
const name = pubkey
|
||||||
? profile?.displayName || profile?.name || shortenPubkey(pubkey)
|
? profile?.displayName || profile?.name || shortenPubkey(pubkey)
|
||||||
: "anonymous";
|
: "anonymous";
|
||||||
const avatar = profile?.picture;
|
const avatar = profile?.picture;
|
||||||
|
const [notePreview, setNotePreview] = useState<string | null>(null);
|
||||||
|
const [noteEvent, setNoteEvent] = useState<NDKEvent | null>(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 (
|
return (
|
||||||
<div className="flex items-start gap-3 px-4 py-3 border-b border-border hover:bg-bg-hover transition-colors">
|
<div className="flex items-start gap-3 px-4 py-3 border-b border-border hover:bg-bg-hover transition-colors">
|
||||||
@@ -92,6 +124,22 @@ function ZapRow({
|
|||||||
{comment && (
|
{comment && (
|
||||||
<p className="text-text-muted text-[12px] leading-snug">{comment}</p>
|
<p className="text-text-muted text-[12px] leading-snug">{comment}</p>
|
||||||
)}
|
)}
|
||||||
|
{noteId && (
|
||||||
|
<button
|
||||||
|
onClick={handleNoteClick}
|
||||||
|
className="mt-1.5 w-full text-left bg-bg-raised border border-border px-3 py-2 rounded-sm hover:border-accent/40 transition-colors cursor-pointer group"
|
||||||
|
>
|
||||||
|
{notePreview ? (
|
||||||
|
<p className="text-text-muted text-[11px] leading-snug line-clamp-2 group-hover:text-text transition-colors">
|
||||||
|
{notePreview}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<span className="text-text-dim text-[11px]">
|
||||||
|
{loadingNote ? "loading note…" : "view original note →"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -202,13 +250,14 @@ export function ZapHistoryView() {
|
|||||||
{!loading &&
|
{!loading &&
|
||||||
activeEvents.map((event) => {
|
activeEvents.map((event) => {
|
||||||
if (tab === "received") {
|
if (tab === "received") {
|
||||||
const { amount, senderPubkey, comment } = parseReceipt(event);
|
const { amount, senderPubkey, comment, noteId } = parseReceipt(event);
|
||||||
return (
|
return (
|
||||||
<ZapRow
|
<ZapRow
|
||||||
key={event.id}
|
key={event.id}
|
||||||
pubkey={senderPubkey}
|
pubkey={senderPubkey}
|
||||||
amount={amount}
|
amount={amount}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
|
noteId={noteId}
|
||||||
createdAt={event.created_at ?? 0}
|
createdAt={event.created_at ?? 0}
|
||||||
direction="received"
|
direction="received"
|
||||||
/>
|
/>
|
||||||
@@ -216,13 +265,14 @@ export function ZapHistoryView() {
|
|||||||
} else {
|
} else {
|
||||||
// Sent zaps are also kind 9735 receipts; the recipient is in the lowercase "p" tag
|
// 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 recipientPubkey = event.tags.find((t) => t[0] === "p")?.[1] ?? null;
|
||||||
const { amount, comment } = parseReceipt(event);
|
const { amount, comment, noteId } = parseReceipt(event);
|
||||||
return (
|
return (
|
||||||
<ZapRow
|
<ZapRow
|
||||||
key={event.id}
|
key={event.id}
|
||||||
pubkey={recipientPubkey}
|
pubkey={recipientPubkey}
|
||||||
amount={amount}
|
amount={amount}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
|
noteId={noteId}
|
||||||
createdAt={event.created_at ?? 0}
|
createdAt={event.created_at ?? 0}
|
||||||
direction="sent"
|
direction="sent"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function isValidNwcUri(uri: string): boolean {
|
|||||||
export async function payInvoiceViaNWC(nwcUri: string, bolt11: string): Promise<string> {
|
export async function payInvoiceViaNWC(nwcUri: string, bolt11: string): Promise<string> {
|
||||||
const { walletPubkey, relayUrl, secret } = parseNwcUri(nwcUri);
|
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);
|
const signer = new NDKPrivateKeySigner(secret);
|
||||||
ndk.signer = signer;
|
ndk.signer = signer;
|
||||||
await ndk.connect();
|
await ndk.connect();
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ export const FALLBACK_RELAYS = [
|
|||||||
"wss://relay.snort.social",
|
"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[] {
|
export function getStoredRelayUrls(): string[] {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(RELAY_STORAGE_KEY);
|
const stored = localStorage.getItem(RELAY_STORAGE_KEY);
|
||||||
@@ -63,6 +70,7 @@ export function getNDK(): NDK {
|
|||||||
if (!ndk) {
|
if (!ndk) {
|
||||||
ndk = new NDK({
|
ndk = new NDK({
|
||||||
explicitRelayUrls: getStoredRelayUrls(),
|
explicitRelayUrls: getStoredRelayUrls(),
|
||||||
|
outboxRelayUrls: OUTBOX_RELAYS,
|
||||||
});
|
});
|
||||||
ndkCreatedAt = Date.now();
|
ndkCreatedAt = Date.now();
|
||||||
}
|
}
|
||||||
@@ -92,6 +100,7 @@ export async function resetNDK(): Promise<void> {
|
|||||||
// Create fresh instance
|
// Create fresh instance
|
||||||
ndk = new NDK({
|
ndk = new NDK({
|
||||||
explicitRelayUrls: getStoredRelayUrls(),
|
explicitRelayUrls: getStoredRelayUrls(),
|
||||||
|
outboxRelayUrls: OUTBOX_RELAYS,
|
||||||
});
|
});
|
||||||
ndkCreatedAt = Date.now();
|
ndkCreatedAt = Date.now();
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ async function pollOnce(pubkey: string) {
|
|||||||
const newFollowers = followers.filter((e) => e.pubkey !== pubkey && !existingFollowerPubkeys.has(e.pubkey));
|
const newFollowers = followers.filter((e) => e.pubkey !== pubkey && !existingFollowerPubkeys.has(e.pubkey));
|
||||||
if (newFollowers.length > 0) {
|
if (newFollowers.length > 0) {
|
||||||
dbSaveNotifications(newFollowers.map((e) => JSON.stringify(e.rawEvent())), pubkey, "follower");
|
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) {
|
for (const e of newFollowers) {
|
||||||
const name = await getProfileName(e.pubkey);
|
const name = await getProfileName(e.pubkey);
|
||||||
notifyFollower(name).catch(() => {});
|
notifyFollower(name).catch(() => {});
|
||||||
|
|||||||
@@ -78,7 +78,12 @@ export const useLightningStore = create<LightningState>(() => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reject(new Error("Zap timed out after 45 seconds"));
|
||||||
|
}, 45000);
|
||||||
|
|
||||||
zapper.on("complete", (results) => {
|
zapper.on("complete", (results) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
const errors = Array.from(results.values()).filter((r) => r instanceof Error);
|
const errors = Array.from(results.values()).filter((r) => r instanceof Error);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
reject(errors[0]);
|
reject(errors[0]);
|
||||||
@@ -87,7 +92,12 @@ export const useLightningStore = create<LightningState>(() => ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
zapper.zap().catch(reject);
|
zapper.zap().then(() => {
|
||||||
|
// zap() resolved but "complete" event handles the result
|
||||||
|
}).catch((err) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user