From 49a65a1db1c8c5741566417ceab6998d11473f88 Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Sun, 25 Aug 2024 18:15:45 -0500 Subject: [PATCH] Zap subscription optimizations --- package-lock.json | 32 ++++++++--- package.json | 1 + .../carousels/templates/CourseTemplate.js | 25 +++++---- .../carousels/templates/ResourceTemplate.js | 23 ++++---- .../carousels/templates/WorkshopTemplate.js | 26 ++++----- .../content/courses/CourseDetails.js | 15 +++--- .../content/resources/ResourceDetails.js | 17 +++--- src/components/navbar/Navbar.js | 8 +-- src/components/navbar/user/UserAvatar.js | 15 +++++- src/context/NDKContext.js | 3 +- .../nostrQueries/zaps/useZapsSubscription.js | 53 ++++++++++--------- 11 files changed, 137 insertions(+), 81 deletions(-) diff --git a/package-lock.json b/package-lock.json index b34ea48..8eca486 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@getalby/bitcoin-connect-react": "^3.5.3", "@getalby/lightning-tools": "^5.0.3", "@nostr-dev-kit/ndk": "^2.10.0", + "@nostr-dev-kit/ndk-cache-dexie": "^2.5.1", "@prisma/client": "^5.17.0", "@tanstack/react-query": "^5.51.21", "@uiw/react-markdown-preview": "^5.1.2", @@ -1061,6 +1062,19 @@ "node": ">=16" } }, + "node_modules/@nostr-dev-kit/ndk-cache-dexie": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.1.tgz", + "integrity": "sha512-tUwEy68bd9GL5JVuZIjcpdwuDEBnaXen3WJ64/GRDtbyE1RB01Y6hHC7IQC9bcQ6SC7XBGyPd+2nuTyR7+Mffg==", + "license": "MIT", + "dependencies": { + "@nostr-dev-kit/ndk": "2.10.0", + "debug": "^4.3.4", + "dexie": "^4.0.2", + "nostr-tools": "^2.4.0", + "typescript-lru-cache": "^2.0.0" + } + }, "node_modules/@nostr-dev-kit/ndk/node_modules/@noble/curves": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", @@ -3517,9 +3531,9 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -4245,6 +4259,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dexie": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.8.tgz", + "integrity": "sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ==", + "license": "Apache-2.0" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -8279,9 +8299,9 @@ "license": "MIT" }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 6f1a5fd..ae1698a 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@getalby/bitcoin-connect-react": "^3.5.3", "@getalby/lightning-tools": "^5.0.3", "@nostr-dev-kit/ndk": "^2.10.0", + "@nostr-dev-kit/ndk-cache-dexie": "^2.5.1", "@prisma/client": "^5.17.0", "@tanstack/react-query": "^5.51.21", "@uiw/react-markdown-preview": "^5.1.2", diff --git a/src/components/content/carousels/templates/CourseTemplate.js b/src/components/content/carousels/templates/CourseTemplate.js index 4ef9d5e..114337d 100644 --- a/src/components/content/carousels/templates/CourseTemplate.js +++ b/src/components/content/carousels/templates/CourseTemplate.js @@ -9,18 +9,17 @@ import { Tag } from "primereact/tag"; import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription"; const CourseTemplate = ({ course }) => { - const [zapAmount, setZapAmount] = useState(null); + const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: course }); + const [zapAmount, setZapAmount] = useState(0); const router = useRouter(); const { returnImageProxy } = useImageProxy(); - const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: course }); useEffect(() => { - if (!zaps || zapsLoading || zapsError) return; - - const total = getTotalFromZaps(zaps, course); - - setZapAmount(total); - }, [course, zaps, zapsLoading, zapsError]); + if (zaps.length > 0) { + const total = getTotalFromZaps(zaps, course); + setZapAmount(total); + } + }, [zaps, course]); if (zapsError) return
Error: {zapsError}
; @@ -30,7 +29,7 @@ const CourseTemplate = ({ course }) => { > {/* Wrap the image in a div with a relative class with a padding-bottom of 56.25% representing the aspect ratio of 16:9 */}
router.push(`/course/${course.id}`)} + onClick={() => router.replace(`/course/${course.id}`)} className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer" style={{ paddingBottom: "56.25%" }} > @@ -61,7 +60,11 @@ const CourseTemplate = ({ course }) => { formatTimestampToHowLongAgo(course.created_at) )}

- +
{course?.topics && course?.topics.length > 0 && (
@@ -75,4 +78,4 @@ const CourseTemplate = ({ course }) => { ); }; -export default CourseTemplate; +export default CourseTemplate; \ No newline at end of file diff --git a/src/components/content/carousels/templates/ResourceTemplate.js b/src/components/content/carousels/templates/ResourceTemplate.js index 9babfbb..cda143c 100644 --- a/src/components/content/carousels/templates/ResourceTemplate.js +++ b/src/components/content/carousels/templates/ResourceTemplate.js @@ -9,19 +9,18 @@ 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 } = useZapsSubscription({ event: resource }); + const [zapAmount, setZapAmount] = useState(0); const router = useRouter(); const { returnImageProxy } = useImageProxy(); useEffect(() => { - if (!zaps || zapsLoading || zapsError) return; - - const total = getTotalFromZaps(zaps, resource); - - setZapAmount(total); - }, [resource, zaps, zapsLoading, zapsError]); + if (zaps.length > 0) { + const total = getTotalFromZaps(zaps, resource); + setZapAmount(total); + } + }, [zaps, resource]); if (zapsError) return
Error: {zapsError}
; @@ -31,7 +30,7 @@ const ResourceTemplate = ({ resource }) => { > {/* Wrap the image in a div with a relative class with a padding-bottom of 56.25% representing the aspect ratio of 16:9 */}
router.push(`/details/${resource.id}`)} + onClick={() => router.replace(`/details/${resource.id}`)} className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer" style={{ paddingBottom: "56.25%" }} > @@ -58,7 +57,11 @@ const ResourceTemplate = ({ resource }) => {

{formatTimestampToHowLongAgo(resource.published_at)}

- +
{resource?.topics && resource?.topics.length > 0 && (
@@ -72,4 +75,4 @@ const ResourceTemplate = ({ resource }) => { ); }; -export default ResourceTemplate; +export default ResourceTemplate; \ No newline at end of file diff --git a/src/components/content/carousels/templates/WorkshopTemplate.js b/src/components/content/carousels/templates/WorkshopTemplate.js index 5a133a1..f645a3b 100644 --- a/src/components/content/carousels/templates/WorkshopTemplate.js +++ b/src/components/content/carousels/templates/WorkshopTemplate.js @@ -9,26 +9,24 @@ import ZapDisplay from "@/components/zaps/ZapDisplay"; import { Tag } from "primereact/tag"; const WorkshopTemplate = ({ workshop }) => { - const [zapAmount, setZapAmount] = useState(null); + const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: workshop }); + const [zapAmount, setZapAmount] = useState(0); const router = useRouter(); const { returnImageProxy } = useImageProxy(); - const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: workshop }); useEffect(() => { - if (zapsLoading || !zaps) return; - - const total = getTotalFromZaps(zaps, workshop); - - setZapAmount(total); - }, [zaps, workshop, zapsLoading, zapsError]); + if (zaps.length > 0) { + const total = getTotalFromZaps(zaps, workshop); + setZapAmount(total); + } + }, [zaps, workshop]); if (zapsError) return
Error: {zapsError}
; return (
- {/* Wrap the image in a div with a relative class with a padding-bottom of 56.25% representing the aspect ratio of 16:9 */}
router.push(`/details/${workshop.id}`)} + onClick={() => router.replace(`/details/${workshop.id}`)} className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer" style={{ paddingBottom: "56.25%" }} > @@ -55,7 +53,11 @@ const WorkshopTemplate = ({ workshop }) => {

{formatTimestampToHowLongAgo(workshop.published_at)}

- +
{workshop?.topics && workshop?.topics.length > 0 && (
@@ -69,4 +71,4 @@ const WorkshopTemplate = ({ workshop }) => { ); }; -export default WorkshopTemplate; +export default WorkshopTemplate; \ No newline at end of file diff --git a/src/components/content/courses/CourseDetails.js b/src/components/content/courses/CourseDetails.js index 5867d33..2312a8f 100644 --- a/src/components/content/courses/CourseDetails.js +++ b/src/components/content/courses/CourseDetails.js @@ -88,11 +88,10 @@ export default function CourseDetails({ processedEvent, paidCourse, lessons, dec }, [processedEvent]); useEffect(() => { - if (!zaps || zaps.length === 0) return; - - const total = getTotalFromZaps(zaps, processedEvent); - - setZapAmount(total); + if (zaps.length > 0) { + const total = getTotalFromZaps(zaps, processedEvent); + setZapAmount(total); + } }, [zaps, processedEvent]); if (!processedEvent || !author) { @@ -160,7 +159,11 @@ export default function CourseDetails({ processedEvent, paidCourse, lessons, dec {paidCourse && author && processedEvent?.pubkey === session?.user?.pubkey && (

Price {processedEvent.price} sats

)} - +
)} diff --git a/src/components/content/resources/ResourceDetails.js b/src/components/content/resources/ResourceDetails.js index 54c680e..b3aa727 100644 --- a/src/components/content/resources/ResourceDetails.js +++ b/src/components/content/resources/ResourceDetails.js @@ -10,7 +10,7 @@ import { getTotalFromZaps } from "@/utils/lightning"; import { useSession } from "next-auth/react"; const ResourceDetails = ({processedEvent, topics, title, summary, image, price, author, paidResource, decryptedContent, handlePaymentSuccess, handlePaymentError}) => { - const [zapAmount, setZapAmount] = useState(null); + const [zapAmount, setZapAmount] = useState(0); const router = useRouter(); const { returnImageProxy } = useImageProxy(); @@ -18,11 +18,10 @@ const ResourceDetails = ({processedEvent, topics, title, summary, image, price, const { data: session, status } = useSession(); useEffect(() => { - if (!zaps) return; - - const total = getTotalFromZaps(zaps, processedEvent); - - setZapAmount(total); + if (zaps.length > 0) { + const total = getTotalFromZaps(zaps, processedEvent); + setZapAmount(total); + } }, [zaps, processedEvent]); return ( @@ -81,7 +80,11 @@ const ResourceDetails = ({processedEvent, topics, title, summary, image, price, {/* if this is the author of the resource show a zap button */} {paidResource && author && processedEvent?.pubkey === session?.user?.pubkey &&

Price {processedEvent.price} sats

} - +
)} diff --git a/src/components/navbar/Navbar.js b/src/components/navbar/Navbar.js index f4c2539..313b4d3 100644 --- a/src/components/navbar/Navbar.js +++ b/src/components/navbar/Navbar.js @@ -1,16 +1,18 @@ -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; import Image from 'next/image'; import UserAvatar from './user/UserAvatar'; import MenuTab from '../menutab/MenuTab'; import { Menubar } from 'primereact/menubar'; import { Menu } from 'primereact/menu'; import { useRouter } from 'next/router'; +import { Button } from 'primereact/button'; +import { Dialog } from 'primereact/dialog'; import 'primereact/resources/primereact.min.css'; import 'primeicons/primeicons.css'; const Navbar = () => { const router = useRouter(); - + const [visible, setVisible] = useState(false); const menu = useRef(null); const navbarHeight = '60px'; @@ -73,7 +75,7 @@ const Navbar = () => { onClick={(e) => menu.current.toggle(e)}> */} -
router.push('/').then(() => window.location.reload())} className="flex flex-row items-center justify-center cursor-pointer"> +
router.push('/')} className="flex flex-row items-center justify-center cursor-pointer"> logo { const router = useRouter(); const [isClient, setIsClient] = useState(false); const [user, setUser] = useState(null); + const [visible, setVisible] = useState(false); const { returnImageProxy } = useImageProxy(); const windowWidth = useWindowWidth(); @@ -84,6 +86,16 @@ const UserAvatar = () => { ); } else { userAvatar = ( +
+
); } diff --git a/src/context/NDKContext.js b/src/context/NDKContext.js index 4b83678..b2a2898 100644 --- a/src/context/NDKContext.js +++ b/src/context/NDKContext.js @@ -1,5 +1,6 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; import NDK, { NDKNip07Signer } from "@nostr-dev-kit/ndk"; +import NDKCacheAdapterDexie from "@nostr-dev-kit/ndk-cache-dexie"; const NDKContext = createContext(null); @@ -17,7 +18,7 @@ export const NDKProvider = ({ children }) => { const [ndk, setNdk] = useState(null); useEffect(() => { - const instance = new NDK({ explicitRelayUrls: relayUrls }); + const instance = new NDK({ explicitRelayUrls: relayUrls, enableOutboxModel: true, outboxRelayUrls: ["wss://nos.lol/"], cacheAdapter: new NDKCacheAdapterDexie({ dbName: 'ndk-cache' }) }); setNdk(instance); }, []); diff --git a/src/hooks/nostrQueries/zaps/useZapsSubscription.js b/src/hooks/nostrQueries/zaps/useZapsSubscription.js index 3cf5c06..b7a01c5 100644 --- a/src/hooks/nostrQueries/zaps/useZapsSubscription.js +++ b/src/hooks/nostrQueries/zaps/useZapsSubscription.js @@ -1,44 +1,48 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useNDKContext } from "@/context/NDKContext"; +import NDK, { NDKEvent, NDKSubscriptionCacheUsage } from "@nostr-dev-kit/ndk"; -export function useZapsSubscription({event}) { +export function useZapsSubscription({ event }) { const [zaps, setZaps] = useState([]); const [zapsLoading, setZapsLoading] = useState(true); const [zapsError, setZapsError] = useState(null); - const {ndk, addSigner} = useNDKContext(); + const { ndk } = useNDKContext(); + + const addZap = useCallback((zapEvent) => { + setZaps((prevZaps) => { + if (prevZaps.some(zap => zap.id === zapEvent.id)) return prevZaps; + return [...prevZaps, zapEvent]; + }); + }, []); useEffect(() => { let subscription; - let isFirstZap = true; - const zapIds = new Set(); // To keep track of zap IDs we've already seen + const zapIds = new Set(); async function subscribeToZaps() { + if (!event || !ndk) return; + try { const filters = [ { kinds: [9735], "#e": [event.id] }, - { kinds: [9735], "#a": [`${event.kind}:${event.id}:${event.d}`] } + { kinds: [9735], "#a": [`${event.kind}:${event.pubkey}:${event.id}`] } ]; - await ndk.connect(); - subscription = ndk.subscribe(filters); - subscription.on('event', (zapEvent) => { - // Check if we've already seen this zap + subscription = ndk.subscribe(filters, { + closeOnEose: false, + cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST + }); + + subscription.on('event', (zapEvent) => { if (!zapIds.has(zapEvent.id)) { zapIds.add(zapEvent.id); - setZaps((prevZaps) => [...prevZaps, zapEvent]); - - if (isFirstZap) { - setZapsLoading(false); - isFirstZap = false; - } + addZap(zapEvent); + setZapsLoading(false); } }); subscription.on('eose', () => { - // Only set loading to false if no zaps have been received yet - if (isFirstZap) { - setZapsLoading(false); - } + setZapsLoading(false); }); await subscription.start(); @@ -49,16 +53,17 @@ export function useZapsSubscription({event}) { } } - if (event && Object.keys(event).length > 0) { - subscribeToZaps(); - } + setZaps([]); + setZapsLoading(true); + setZapsError(null); + subscribeToZaps(); return () => { if (subscription) { subscription.stop(); } }; - }, [event, ndk]); + }, [event, ndk, addZap]); return { zaps, zapsLoading, zapsError }; } \ No newline at end of file