Zap subscription optimizations

This commit is contained in:
austinkelsay 2024-08-25 18:15:45 -05:00
parent 0ab37a3f79
commit 49a65a1db1
11 changed files with 137 additions and 81 deletions

32
package-lock.json generated
View File

@ -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": {

View File

@ -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",

View File

@ -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 <div>Error: {zapsError}</div>;
@ -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 */}
<div
onClick={() => 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)
)}
</p>
<ZapDisplay zapAmount={zapAmount} event={course} zapsLoading={zapsLoading} />
<ZapDisplay
zapAmount={zapAmount}
event={course}
zapsLoading={zapsLoading && zapAmount === 0}
/>
</div>
{course?.topics && course?.topics.length > 0 && (
<div className="flex flex-row justify-start items-center mt-2">
@ -75,4 +78,4 @@ const CourseTemplate = ({ course }) => {
);
};
export default CourseTemplate;
export default CourseTemplate;

View File

@ -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 <div>Error: {zapsError}</div>;
@ -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 */}
<div
onClick={() => 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 }) => {
<p className="text-xs text-gray-400">
{formatTimestampToHowLongAgo(resource.published_at)}
</p>
<ZapDisplay zapAmount={zapAmount} event={resource} zapsLoading={zapsLoading} />
<ZapDisplay
zapAmount={zapAmount}
event={resource}
zapsLoading={zapsLoading && zapAmount === 0}
/>
</div>
{resource?.topics && resource?.topics.length > 0 && (
<div className="flex flex-row justify-start items-center mt-2">
@ -72,4 +75,4 @@ const ResourceTemplate = ({ resource }) => {
);
};
export default ResourceTemplate;
export default ResourceTemplate;

View File

@ -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 <div>Error: {zapsError}</div>;
return (
<div className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md">
{/* Wrap the image in a div with a relative class with a padding-bottom of 56.25% representing the aspect ratio of 16:9 */}
<div
onClick={() => 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 }) => {
<p className="text-xs text-gray-400">
{formatTimestampToHowLongAgo(workshop.published_at)}
</p>
<ZapDisplay zapAmount={zapAmount} event={workshop} zapsLoading={zapsLoading} />
<ZapDisplay
zapAmount={zapAmount}
event={workshop}
zapsLoading={zapsLoading && zapAmount === 0}
/>
</div>
{workshop?.topics && workshop?.topics.length > 0 && (
<div className="flex flex-row justify-start items-center mt-2">
@ -69,4 +71,4 @@ const WorkshopTemplate = ({ workshop }) => {
);
};
export default WorkshopTemplate;
export default WorkshopTemplate;

View File

@ -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 && (
<p className='text-green-500'>Price {processedEvent.price} sats</p>
)}
<ZapDisplay zapAmount={zapAmount} event={processedEvent} zapsLoading={zapsLoading} />
<ZapDisplay
zapAmount={zapAmount}
event={processedEvent}
zapsLoading={zapsLoading && zapAmount === 0}
/>
</div>
</div>
)}

View File

@ -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 && <p className='text-green-500'>Price {processedEvent.price} sats</p>}
<ZapDisplay zapAmount={zapAmount} event={processedEvent} zapsLoading={zapsLoading} />
<ZapDisplay
zapAmount={zapAmount}
event={processedEvent}
zapsLoading={zapsLoading && zapAmount === 0}
/>
</div>
</div>
)}

View File

@ -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)}></i>
<Menu model={menuItems} popup ref={menu} />
</div> */}
<div onClick={() => router.push('/').then(() => window.location.reload())} className="flex flex-row items-center justify-center cursor-pointer">
<div onClick={() => router.push('/')} className="flex flex-row items-center justify-center cursor-pointer">
<Image
alt="logo"
src="/plebdevs-guy.jpg"

View File

@ -6,6 +6,7 @@ import { Button } from 'primereact/button';
import { Menu } from 'primereact/menu';
import useWindowWidth from '@/hooks/useWindowWidth';
import {useSession, signOut} from 'next-auth/react';
import { Dialog } from 'primereact/dialog';
import 'primereact/resources/primereact.min.css';
import 'primeicons/primeicons.css';
import styles from '../navbar.module.css';
@ -14,6 +15,7 @@ const UserAvatar = () => {
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 = (
<div className='flex flex-row items-center justify-between'>
<Button severity='help' rounded label="About" className='text-[#f8f8ff] mr-4' onClick={() => setVisible(true)} />
<Dialog header="Header" visible={visible} style={{ width: '50vw' }} onHide={() => {if (!visible) return; setVisible(false); }}>
<p className="m-0">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
</Dialog>
<Button
label="Login"
icon="pi pi-user"
@ -91,7 +103,8 @@ const UserAvatar = () => {
rounded
onClick={() => router.push('/auth/signin')}
size={windowWidth < 768 ? 'small' : 'normal'}
/>
/>
</div>
);
}

View File

@ -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);
}, []);

View File

@ -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 };
}