From b937dd25072bf60be25e69e4e9df1c110dfe2cbc Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Tue, 13 Aug 2024 14:42:36 -0500 Subject: [PATCH] Better zap subscriptions --- .../carousels/templates/CourseTemplate.js | 20 +-- .../carousels/templates/ResourceTemplate.js | 20 +-- .../carousels/templates/WorkshopTemplate.js | 19 +-- .../content/courses/CourseDetails.js | 15 +-- .../content/courses/CourseLesson.js | 17 +-- .../content/resources/ResourceDetails.js | 14 ++- .../nostrQueries/zaps/useZapsSubscription.js | 115 +++++++++--------- src/pages/details/[slug]/index.js | 39 ------ src/utils/lightning.js | 18 +++ 9 files changed, 111 insertions(+), 166 deletions(-) diff --git a/src/components/content/carousels/templates/CourseTemplate.js b/src/components/content/carousels/templates/CourseTemplate.js index 374a3a8..20345bf 100644 --- a/src/components/content/carousels/templates/CourseTemplate.js +++ b/src/components/content/carousels/templates/CourseTemplate.js @@ -3,31 +3,21 @@ import Image from "next/image"; import { useRouter } from "next/router"; import { formatTimestampToHowLongAgo } from "@/utils/time"; import { useImageProxy } from "@/hooks/useImageProxy"; -import { getSatAmountFromInvoice } from "@/utils/lightning"; +import { getTotalFromZaps } from "@/utils/lightning"; import ZapDisplay from "@/components/zaps/ZapDisplay"; -import { useZapsQuery } from "@/hooks/nostrQueries/zaps/useZapsQuery"; +import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription"; const CourseTemplate = ({ course }) => { const [zapAmount, setZapAmount] = useState(null); const router = useRouter(); const { returnImageProxy } = useImageProxy(); - const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: course, type: "course" }); + const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: course }); useEffect(() => { if (!zaps || zapsLoading || zapsError) return; - let total = 0; - zaps.forEach((zap) => { - // If the zap matches the event or the parameterized event, then add the zap to the total - if (zap.tags.find(tag => tag[0] === "e" && tag[1] === course.id) || zap.tags.find(tag => tag[0] === "a" && tag[1] === `${course.kind}:${course.id}:${course.d}`)) { - const bolt11Tag = zap.tags.find(tag => tag[0] === "bolt11"); - const invoice = bolt11Tag ? bolt11Tag[1] : null; - if (invoice) { - const amount = getSatAmountFromInvoice(invoice); - total += amount; - } - } - }); + const total = getTotalFromZaps(zaps, course); + setZapAmount(total); }, [course, zaps, zapsLoading, zapsError]); diff --git a/src/components/content/carousels/templates/ResourceTemplate.js b/src/components/content/carousels/templates/ResourceTemplate.js index 92e3859..8d8a668 100644 --- a/src/components/content/carousels/templates/ResourceTemplate.js +++ b/src/components/content/carousels/templates/ResourceTemplate.js @@ -3,13 +3,13 @@ import Image from "next/image"; import { useRouter } from "next/router"; import { formatTimestampToHowLongAgo } from "@/utils/time"; import { useImageProxy } from "@/hooks/useImageProxy"; -import { useZapsQuery } from "@/hooks/nostrQueries/zaps/useZapsQuery"; -import { getSatAmountFromInvoice } from "@/utils/lightning"; +import { getTotalFromZaps } from "@/utils/lightning"; import ZapDisplay from "@/components/zaps/ZapDisplay"; +import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription"; const ResourceTemplate = ({ resource }) => { const [zapAmount, setZapAmount] = useState(null); - const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: resource, type: "resource" }); + const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: resource }); const router = useRouter(); const { returnImageProxy } = useImageProxy(); @@ -17,18 +17,8 @@ const ResourceTemplate = ({ resource }) => { useEffect(() => { if (!zaps || zapsLoading || zapsError) return; - let total = 0; - zaps.forEach((zap) => { - // If the zap matches the event or the parameterized event, then add the zap to the total - if (zap.tags.find(tag => tag[0] === "e" && tag[1] === resource.id) || zap.tags.find(tag => tag[0] === "a" && tag[1] === `${resource.kind}:${resource.id}:${resource.d}`)) { - const bolt11Tag = zap.tags.find(tag => tag[0] === "bolt11"); - const invoice = bolt11Tag ? bolt11Tag[1] : null; - if (invoice) { - const amount = getSatAmountFromInvoice(invoice); - total += amount; - } - } - }); + const total = getTotalFromZaps(zaps, resource); + setZapAmount(total); }, [resource, zaps, zapsLoading, zapsError]); diff --git a/src/components/content/carousels/templates/WorkshopTemplate.js b/src/components/content/carousels/templates/WorkshopTemplate.js index 4b85239..12ee82b 100644 --- a/src/components/content/carousels/templates/WorkshopTemplate.js +++ b/src/components/content/carousels/templates/WorkshopTemplate.js @@ -3,31 +3,20 @@ import Image from "next/image"; import { useRouter } from "next/router"; import { formatTimestampToHowLongAgo } from "@/utils/time"; import { useImageProxy } from "@/hooks/useImageProxy"; -import { useZapsQuery } from "@/hooks/nostrQueries/zaps/useZapsQuery"; -import { getSatAmountFromInvoice } from "@/utils/lightning"; +import { getTotalFromZaps } from "@/utils/lightning"; +import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription"; import ZapDisplay from "@/components/zaps/ZapDisplay"; const WorkshopTemplate = ({ workshop }) => { const [zapAmount, setZapAmount] = useState(null); const router = useRouter(); const { returnImageProxy } = useImageProxy(); - const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: workshop, type: "workshop" }); + const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: workshop }); useEffect(() => { if (zapsLoading || !zaps) return; - let total = 0; - zaps.forEach((zap) => { - // If the zap matches the event or the parameterized event, then add the zap to the total - if (zap.tags.find(tag => tag[0] === "e" && tag[1] === workshop.id) || zap.tags.find(tag => tag[0] === "a" && tag[1] === `${workshop.kind}:${workshop.id}:${workshop.d}`)) { - const bolt11Tag = zap.tags.find(tag => tag[0] === "bolt11"); - const invoice = bolt11Tag ? bolt11Tag[1] : null; - if (invoice) { - const amount = getSatAmountFromInvoice(invoice); - total += amount; - } - } - }); + const total = getTotalFromZaps(zaps, workshop); setZapAmount(total); }, [zaps, workshop, zapsLoading, zapsError]); diff --git a/src/components/content/courses/CourseDetails.js b/src/components/content/courses/CourseDetails.js index 203cefb..be4b154 100644 --- a/src/components/content/courses/CourseDetails.js +++ b/src/components/content/courses/CourseDetails.js @@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback } from 'react'; import { useRouter } from 'next/router'; import { useImageProxy } from '@/hooks/useImageProxy'; import ZapDisplay from '@/components/zaps/ZapDisplay'; -import { getSatAmountFromInvoice } from '@/utils/lightning'; +import { getTotalFromZaps } from '@/utils/lightning'; import { Tag } from 'primereact/tag'; import { nip19 } from 'nostr-tools'; import { useSession } from 'next-auth/react'; @@ -95,17 +95,10 @@ export default function CourseDetails({ processedEvent }) { useEffect(() => { if (!zaps || zaps.length === 0) return; - let total = 0; - zaps.forEach((zap) => { - const bolt11Tag = zap.tags.find(tag => tag[0] === "bolt11"); - const invoice = bolt11Tag ? bolt11Tag[1] : null; - if (invoice) { - const amount = getSatAmountFromInvoice(invoice); - total += amount; - } - }); + const total = getTotalFromZaps(zaps, processedEvent); + setZapAmount(total); - }, [zaps]); + }, [zaps, processedEvent]); return (
diff --git a/src/components/content/courses/CourseLesson.js b/src/components/content/courses/CourseLesson.js index d1d045d..6a5b328 100644 --- a/src/components/content/courses/CourseLesson.js +++ b/src/components/content/courses/CourseLesson.js @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { Tag } from "primereact/tag"; import Image from "next/image"; import { useImageProxy } from "@/hooks/useImageProxy"; -import { getSatAmountFromInvoice } from "@/utils/lightning"; +import { getTotalFromZaps } from "@/utils/lightning"; import ZapDisplay from "@/components/zaps/ZapDisplay"; import dynamic from "next/dynamic"; import { useZapsQuery } from "@/hooks/nostrQueries/zaps/useZapsQuery"; @@ -45,19 +45,10 @@ const CourseLesson = ({ lesson, course }) => { useEffect(() => { if (!zaps || zapsLoading || zapsError) return; - let total = 0; - zaps.forEach((zap) => { - if (zap.tags.find(tag => tag[0] === "e" && tag[1] === lesson.id) || zap.tags.find(tag => tag[0] === "a" && tag[1] === `${lesson.kind}:${lesson.id}:${lesson.d}`)) { - const bolt11Tag = zap.tags.find(tag => tag[0] === "bolt11"); - const invoice = bolt11Tag ? bolt11Tag[1] : null; - if (invoice) { - const amount = getSatAmountFromInvoice(invoice); - total += amount; - } - } - }); + const total = getTotalFromZaps(zaps, lesson); + setZapAmount(total); - }, [zaps, zapsLoading, zapsError]); + }, [zaps, zapsLoading, zapsError, lesson]); return (
diff --git a/src/components/content/resources/ResourceDetails.js b/src/components/content/resources/ResourceDetails.js index a364b20..d102eb8 100644 --- a/src/components/content/resources/ResourceDetails.js +++ b/src/components/content/resources/ResourceDetails.js @@ -5,12 +5,24 @@ import { useRouter } from "next/router"; import ResourcePaymentButton from "@/components/bitcoinConnect/ResourcePaymentButton"; import ZapDisplay from "@/components/zaps/ZapDisplay"; import { useImageProxy } from "@/hooks/useImageProxy"; +import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription"; +import { getTotalFromZaps } from "@/utils/lightning"; + +const ResourceDetails = ({processedEvent, topics, title, summary, image, price, author, paidResource, decryptedContent, handlePaymentSuccess, handlePaymentError}) => { + const [zapAmount, setZapAmount] = useState(null); -const ResourceDetails = ({processedEvent,topics, title, summary, image, price, author, paidResource, decryptedContent, zapAmount, zapsLoading, handlePaymentSuccess, handlePaymentError}) => { const router = useRouter(); const { slug } = router.query; const { returnImageProxy } = useImageProxy(); + const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: processedEvent }); + useEffect(() => { + if (!zaps) return; + + const total = getTotalFromZaps(zaps, processedEvent); + + setZapAmount(total); + }, [zaps, processedEvent]); return (
diff --git a/src/hooks/nostrQueries/zaps/useZapsSubscription.js b/src/hooks/nostrQueries/zaps/useZapsSubscription.js index 06198d2..93c73b3 100644 --- a/src/hooks/nostrQueries/zaps/useZapsSubscription.js +++ b/src/hooks/nostrQueries/zaps/useZapsSubscription.js @@ -1,67 +1,68 @@ import { useState, useEffect } from 'react'; -import { useNDKContext } from '@/context/NDKContext'; +import { useNDKContext } from "@/context/NDKContext"; -export function useZapsSubscription({ event }) { - const [isClient, setIsClient] = useState(false); - const [zaps, setZaps] = useState([]); - const [zapsLoading, setZapsLoading] = useState(true); - const [zapsError, setZapsError] = useState(null); - const ndk = useNDKContext(); +export function useZapsSubscription({event}) { + const [zaps, setZaps] = useState([]); + const [zapsLoading, setZapsLoading] = useState(true); + const [zapsError, setZapsError] = useState(null); + const ndk = useNDKContext(); - useEffect(() => { - setIsClient(true); - }, []); + useEffect(() => { + let subscription; + let isFirstZap = true; + const zapIds = new Set(); // To keep track of zap IDs we've already seen - useEffect(() => { - if (!isClient || !ndk || !event) return; + async function subscribeToZaps() { + try { + const filters = [ + { kinds: [9735], "#e": [event.id] }, + { kinds: [9735], "#a": [`${event.kind}:${event.id}:${event.d}`] } + ]; + await ndk.connect(); + console.log("filters", filters); + subscription = ndk.subscribe(filters); - let subscription = null; + subscription.on('event', (zapEvent) => { + console.log("event", zapEvent); + + // Check if we've already seen this zap + if (!zapIds.has(zapEvent.id)) { + zapIds.add(zapEvent.id); + setZaps((prevZaps) => [...prevZaps, zapEvent]); - const fetchZapsFromNDK = async () => { - try { - await ndk.connect(); - const uniqueEvents = new Set(); - - const filters = [ - { kinds: [9735], "#e": [event.id] }, - { kinds: [9735], "#a": [`${event.kind}:${event.id}:${event.d}`] } - ]; - - subscription = ndk.subscribe(filters); - - subscription.on('event', (zap) => { - uniqueEvents.add(zap); - setZaps(Array.from(uniqueEvents)); - setZapsLoading(false); - }); - - subscription.on('eose', () => { - setZaps(Array.from(uniqueEvents)); - setZapsLoading(false); - }); - - // if there are no zaps for 15 seconds and no eose to stop loading - setTimeout(() => { - if (uniqueEvents.size === 0) { - setZapsLoading(false); - setZaps(Array.from(uniqueEvents)); - } - }, 15000); - - } catch (error) { - setZapsError('Error fetching zaps from NDK: ' + error); - setZapsLoading(false); + if (isFirstZap) { + setZapsLoading(false); + isFirstZap = false; } - }; + } + }); - fetchZapsFromNDK(); + subscription.on('eose', () => { + console.log("eose"); + // Only set loading to false if no zaps have been received yet + if (isFirstZap) { + setZapsLoading(false); + } + }); - return () => { - if (subscription) { - subscription.stop(); - } - }; - }, [isClient, ndk, event]); + await subscription.start(); + } catch (error) { + console.error("Error subscribing to zaps:", error); + setZapsError(error.message); + setZapsLoading(false); + } + } - return { zaps, zapsLoading, zapsError }; -} + if (event && Object.keys(event).length > 0) { + subscribeToZaps(); + } + + return () => { + if (subscription) { + subscription.stop(); + } + }; + }, [event, ndk]); + + return { zaps, zapsLoading, zapsError }; +} \ No newline at end of file diff --git a/src/pages/details/[slug]/index.js b/src/pages/details/[slug]/index.js index 21f69d0..a9dae9e 100644 --- a/src/pages/details/[slug]/index.js +++ b/src/pages/details/[slug]/index.js @@ -2,21 +2,14 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; import { useRouter } from 'next/router'; import { parseEvent, findKind0Fields } from '@/utils/nostr'; -import { useImageProxy } from '@/hooks/useImageProxy'; -import { getSatAmountFromInvoice } from '@/utils/lightning'; -import ZapDisplay from '@/components/zaps/ZapDisplay'; -import { Tag } from 'primereact/tag'; import { Button } from 'primereact/button'; import { nip19, nip04 } from 'nostr-tools'; import { useSession } from 'next-auth/react'; -import Image from 'next/image'; import dynamic from 'next/dynamic'; import ZapThreadsWrapper from '@/components/ZapThreadsWrapper'; import { useToast } from '@/hooks/useToast'; import { useNDKContext } from '@/context/NDKContext'; import ResourceDetails from '@/components/content/resources/ResourceDetails'; -import { useZapsSubscription } from '@/hooks/nostrQueries/zaps/useZapsSubscription'; -import ResourcePaymentButton from '@/components/bitcoinConnect/ResourcePaymentButton'; import 'primeicons/primeicons.css'; const MDDisplay = dynamic( @@ -33,9 +26,7 @@ export default function Details() { const [event, setEvent] = useState(null); const [processedEvent, setProcessedEvent] = useState({}); const [author, setAuthor] = useState(null); - const [bitcoinConnect, setBitcoinConnect] = useState(false); const [nAddress, setNAddress] = useState(null); - const [zapAmount, setZapAmount] = useState(null); const [paidResource, setPaidResource] = useState(false); const [decryptedContent, setDecryptedContent] = useState(null); const [authorView, setAuthorView] = useState(false); @@ -43,9 +34,7 @@ export default function Details() { const ndk = useNDKContext(); const { data: session, update } = useSession(); const [user, setUser] = useState(null); - const { returnImageProxy } = useImageProxy(); const { showToast } = useToast(); - const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: processedEvent }); const router = useRouter(); @@ -61,16 +50,6 @@ export default function Details() { } }, [processedEvent]); - useEffect(() => { - if (typeof window === 'undefined') return; - - const bitcoinConnectConfig = window.localStorage.getItem('bc:config'); - - if (bitcoinConnectConfig) { - setBitcoinConnect(true); - } - }, []); - useEffect(() => { const decryptContent = async () => { if (user && paidResource) { @@ -165,22 +144,6 @@ export default function Details() { } }, [processedEvent]); - useEffect(() => { - if (!zaps) return; - - let total = 0; - zaps.forEach((zap) => { - const bolt11Tag = zap.tags.find(tag => tag[0] === "bolt11"); - const invoice = bolt11Tag ? bolt11Tag[1] : null; - if (invoice) { - const amount = getSatAmountFromInvoice(invoice); - total += amount; - } - }); - - setZapAmount(total); - }, [zaps]); - const handleDelete = async () => { try { const response = await axios.delete(`/api/resources/${processedEvent.d}`); @@ -239,8 +202,6 @@ export default function Details() { author={author} paidResource={paidResource} decryptedContent={decryptedContent} - zapAmount={zapAmount} - zapsLoading={zapsLoading} handlePaymentSuccess={handlePaymentSuccess} handlePaymentError={handlePaymentError} /> diff --git a/src/utils/lightning.js b/src/utils/lightning.js index 39ca868..6202069 100644 --- a/src/utils/lightning.js +++ b/src/utils/lightning.js @@ -3,4 +3,22 @@ import Bolt11Decoder from "light-bolt11-decoder" export const getSatAmountFromInvoice = (invoice) => { const decoded = Bolt11Decoder.decode(invoice) return decoded.sections[2].value / 1000; +} + +export const getTotalFromZaps = (zaps, event) => { + let total = 0; + let uniqueZaps = new Set(); + zaps.forEach((zap) => { + // If the zap matches the event or the parameterized event, then add the zap to the total + if ((zap.tags.find(tag => tag[0] === "e" && tag[1] === event.id) || zap.tags.find(tag => tag[0] === "a" && tag[1] === `${event.kind}:${event.id}:${event.d}`)) &&!uniqueZaps.has(zap.id)) { + uniqueZaps.add(zap.id); + const bolt11Tag = zap.tags.find(tag => tag[0] === "bolt11"); + const invoice = bolt11Tag ? bolt11Tag[1] : null; + if (invoice) { + const amount = getSatAmountFromInvoice(invoice); + total += amount; + } + } + }); + return total; } \ No newline at end of file