diff --git a/src/components/CourseDetails.js b/src/components/CourseDetails.js index 0173e79..c09a05b 100644 --- a/src/components/CourseDetails.js +++ b/src/components/CourseDetails.js @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; import { useNostr } from '@/hooks/useNostr'; -import { parseEvent, findKind0Fields, hexToNpub } from '@/utils/nostr'; +import { findKind0Fields } from '@/utils/nostr'; import { useImageProxy } from '@/hooks/useImageProxy'; import { Button } from 'primereact/button'; import { Tag } from 'primereact/tag'; @@ -31,9 +31,8 @@ export default function CourseDetails({processedEvent}) { const [bitcoinConnect, setBitcoinConnect] = useState(false); const [nAddress, setNAddress] = useState(null); const [user] = useLocalStorageWithEffect('user', {}); - console.log('user:', user); const { returnImageProxy } = useImageProxy(); - const { fetchSingleEvent, fetchKind0, zapEvent } = useNostr(); + const { fetchKind0, zapEvent } = useNostr(); const router = useRouter(); @@ -76,7 +75,6 @@ export default function CourseDetails({processedEvent}) { kind: processedEvent.kind, identifier: processedEvent.d, }); - console.log('naddr:', naddr); setNAddress(naddr); } }, [processedEvent]); diff --git a/src/components/content/carousels/CoursesCarousel.js b/src/components/content/carousels/CoursesCarousel.js index b5fd68f..6ce793f 100644 --- a/src/components/content/carousels/CoursesCarousel.js +++ b/src/components/content/carousels/CoursesCarousel.js @@ -69,7 +69,7 @@ export default function CoursesCarousel() {

Courses

0 ? [{}, {}, {}] : [...processedCourses, ...processedCourses]} + value={!processedCourses.length > 0 ? [{}, {}, {}] : [...processedCourses]} numVisible={2} itemTemplate={!processedCourses.length > 0 ? TemplateSkeleton : CourseTemplate} responsiveOptions={responsiveOptions} /> diff --git a/src/components/content/carousels/ResourcesCarousel.js b/src/components/content/carousels/ResourcesCarousel.js index 8d7f379..cb91624 100644 --- a/src/components/content/carousels/ResourcesCarousel.js +++ b/src/components/content/carousels/ResourcesCarousel.js @@ -64,7 +64,7 @@ export default function ResourcesCarousel() { return ( <>

Resources

- 0 ? [{}, {}, {}] : [...processedResources, ...processedResources]} + 0 ? [{}, {}, {}] : [...processedResources]} numVisible={2} itemTemplate={!processedResources.length > 0 ? TemplateSkeleton : ResourceTemplate} responsiveOptions={responsiveOptions} /> diff --git a/src/components/content/carousels/WorkshopsCarousel.js b/src/components/content/carousels/WorkshopsCarousel.js index 425bb40..651967f 100644 --- a/src/components/content/carousels/WorkshopsCarousel.js +++ b/src/components/content/carousels/WorkshopsCarousel.js @@ -66,7 +66,7 @@ export default function WorkshopsCarousel() { return ( <>

Workshops

- 0 ? [{}, {}, {}] : [...processedWorkshops, ...processedWorkshops]} + 0 ? [{}, {}, {}] : [...processedWorkshops]} numVisible={2} itemTemplate={!processedWorkshops.length > 0 ? TemplateSkeleton : WorkshopTemplate} responsiveOptions={responsiveOptions} /> diff --git a/src/components/zaps/ZapDisplay.js b/src/components/zaps/ZapDisplay.js index d740e18..b1a6dc2 100644 --- a/src/components/zaps/ZapDisplay.js +++ b/src/components/zaps/ZapDisplay.js @@ -2,17 +2,19 @@ import React, { useRef } from 'react'; import { OverlayPanel } from 'primereact/overlaypanel'; import ZapForm from './ZapForm'; import { ProgressSpinner } from 'primereact/progressspinner'; - const ZapDisplay = ({ zapAmount, event }) => { const op = useRef(null); return ( <> -

op.current.toggle(e)}> - - - {zapAmount || zapAmount === 0 ? zapAmount : } -

+ op.current.toggle(e)}> + + {zapAmount || zapAmount === 0 ? ( + zapAmount + ) : ( + + )} + @@ -20,4 +22,4 @@ const ZapDisplay = ({ zapAmount, event }) => { ) } -export default ZapDisplay; \ No newline at end of file +export default ZapDisplay; diff --git a/src/hooks/useNostr.js b/src/hooks/useNostr.js index ed9a336..bcc4b7a 100644 --- a/src/hooks/useNostr.js +++ b/src/hooks/useNostr.js @@ -24,6 +24,9 @@ export function useNostr() { const lastSubscriptionTime = useRef(0); const throttleDelay = 2000; + // ref to keep track of active subscriptions + const activeSubscriptions = useRef([]); + const processSubscriptionQueue = useCallback(() => { if (subscriptionQueue.current.length === 0) return; @@ -45,7 +48,23 @@ export function useNostr() { if (!pool) return; const subscriptionFn = () => { - return pool.subscribeMany(defaultRelays, filters, opts); + // Create the subscription + const sub = pool.subscribeMany(defaultRelays, filters, { + ...opts, + oneose: () => { + // Call the original oneose if it exists + opts.oneose?.(); + // Close the subscription after EOSE + sub.close(); + // Remove this subscription from activeSubscriptions + activeSubscriptions.current = activeSubscriptions.current.filter(s => s !== sub); + } + }); + + // Add this subscription to activeSubscriptions + activeSubscriptions.current.push(sub); + + return sub; }; subscriptionQueue.current.push(subscriptionFn); @@ -54,6 +73,19 @@ export function useNostr() { [pool, processSubscriptionQueue] ); + // Add this new function to close all active subscriptions + const closeAllSubscriptions = useCallback(() => { + activeSubscriptions.current.forEach(sub => sub.close()); + activeSubscriptions.current = []; + }, []); + + // Use an effect to close all subscriptions when the component unmounts + useEffect(() => { + return () => { + closeAllSubscriptions(); + }; + }, [closeAllSubscriptions]); + const publish = useCallback( async (event) => { if (!pool) return; diff --git a/src/hooks/useNostrOld.js b/src/hooks/useNostrOld.js deleted file mode 100644 index 77db210..0000000 --- a/src/hooks/useNostrOld.js +++ /dev/null @@ -1,386 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import { SimplePool, nip19, verifyEvent, nip57 } from "nostr-tools"; -import axios from "axios"; -import { useToast } from "./useToast"; - -const initialRelays = [ - "wss://nos.lol/", - "wss://relay.damus.io/", - "wss://relay.snort.social/", - "wss://relay.nostr.band/", - "wss://nostr.mutinywallet.com/", - "wss://relay.mutinywallet.com/", - "wss://relay.primal.net/" -]; - -const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY; - -export const useNostr = () => { - const [relays, setRelays] = useState(initialRelays); - const [relayStatuses, setRelayStatuses] = useState({}); - const [events, setEvents] = useState({ - resources: [], - workshops: [], - courses: [], - streams: [], - zaps: [] - }); - - const { showToast } = useToast(); - - const pool = useRef(new SimplePool({ seenOnEnabled: true })); - const subscriptions = useRef([]); - - const getRelayStatuses = () => { - if (pool.current && pool.current._conn) { - const statuses = {}; - - for (const url in pool.current._conn) { - const relay = pool.current._conn[url]; - statuses[url] = relay.status; // Assuming 'status' is an accessible field in Relay object - } - - setRelayStatuses(statuses); - } - }; - - const updateRelays = async (newRelays) => { - // Set new relays - setRelays(newRelays); - - // Ensure the relays are connected before using them - await Promise.all(newRelays.map(relay => pool.current.ensureRelay(relay))); - }; - - const fetchEvents = async (filter, updateDataField, hasRequiredTags) => { - try { - const sub = pool.current.subscribeMany(relays, filter, { - onevent: async (event) => { - const shouldInclude = await hasRequiredTags(event.tags); - if (shouldInclude) { - setEvents(prevData => ({ - ...prevData, - [updateDataField]: [...prevData[updateDataField], event] - })); - } - }, - onerror: (error) => { - console.error(`Error fetching ${updateDataField}:`, error); - }, - onclose: () => { - // Handle connection closure and retry if needed - console.log("Connection closed"); - // Implement retry logic here - }, - oneose: () => { - console.log("Subscription closed"); - sub.close(); - } - }); - - // Store the subscription in the ref for cleanup - subscriptions.current.push(sub); - } catch (error) { - console.error(`Error in fetchEvents for ${updateDataField}:`, error); - } - }; - - // zaps - // 1. get the author from the content - // 2. get the author's kind0 - // 3. get the author's lud16 if available - // 4. Make a get request to the lud16 endpoint and ensure that allowNostr is true - // 5. Create zap request event and sign it - // 6. Send to the callback url as a get req with the nostr event as a query param - // 7. get the invoice back and pay it with webln - // 8. listen for the zap receipt event and update the UI - - const zapEvent = async (event) => { - const kind0 = await fetchKind0([{ authors: [event.pubkey], kinds: [0] }], {}); - - if (Object.keys(kind0).length === 0) { - console.error('Error fetching kind0'); - return; - } - - if (kind0?.lud16) { - const lud16Username = kind0.lud16.split('@')[0]; - const lud16Domain = kind0.lud16.split('@')[1]; - - const lud16Url = `https://${lud16Domain}/.well-known/lnurlp/${lud16Username}`; - - const response = await axios.get(lud16Url); - - if (response.data.allowsNostr) { - const zapReq = nip57.makeZapRequest({ - profile: event.pubkey, - event: event.id, - amount: 1000, - relays: relays, - comment: 'Plebdevs Zap' - }); - - console.log('zapReq:', zapReq); - - const signedEvent = await window?.nostr.signEvent(zapReq); - - const callbackUrl = response.data.callback; - - const zapRequestAPICall = `${callbackUrl}?amount=${1000}&nostr=${encodeURI(JSON.stringify(signedEvent))}`; - - const invoiceResponse = await axios.get(zapRequestAPICall); - - if (invoiceResponse?.data?.pr) { - const invoice = invoiceResponse.data.pr; - - const enabled = await window?.webln?.enable(); - - console.log('webln enabled:', enabled); - - const payInvoiceResponse = await window?.webln?.sendPayment(invoice); - - console.log('payInvoiceResponse:', payInvoiceResponse); - } else { - console.error('Error fetching invoice'); - showToast('error', 'Error', 'Error fetching invoice'); - } - } - } else if (kind0?.lud06) { - // handle lnurlpay - } else { - showToast('error', 'Error', 'User has no Lightning Address or LNURL'); - return; - } - - } - - const fetchZapsForEvent = async (eventId) => { - const filter = [{ kinds: [9735], "#e": [eventId] }]; - fetchEvents(filter, 'zaps', () => true); - } - - // Fetch resources, workshops, courses, and streams with appropriate filters and update functions - const fetchResources = async () => { - const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }]; - const hasRequiredTags = async (eventData) => { - const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); - const hasResource = eventData.some(([tag, value]) => tag === "t" && value === "resource"); - if (hasPlebDevs && hasResource) { - const resourceId = eventData.find(([tag]) => tag === "d")?.[1]; - if (resourceId) { - try { - const response = await axios.get(`/api/resources/${resourceId}`); - return response.status === 200; - } catch (error) { - // Handle 404 or other errors gracefully - return false; - } - } - } - return false; - }; - fetchEvents(filter, 'resources', hasRequiredTags); - }; - - const fetchWorkshops = async () => { - const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }]; - const hasRequiredTags = async (eventData) => { - const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); - const hasWorkshop = eventData.some(([tag, value]) => tag === "t" && value === "workshop"); - if (hasPlebDevs && hasWorkshop) { - const workshopId = eventData.find(([tag]) => tag === "d")?.[1]; - if (workshopId) { - try { - const response = await axios.get(`/api/resources/${workshopId}`); - return response.status === 200; - } catch (error) { - // Handle 404 or other errors gracefully - return false; - } - } - } - return false; - }; - fetchEvents(filter, 'workshops', hasRequiredTags); - }; - - const fetchCourses = async () => { - const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }]; - const hasRequiredTags = async (eventData) => { - const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); - const hasCourse = eventData.some(([tag, value]) => tag === "t" && value === "course"); - if (hasPlebDevs && hasCourse) { - const courseId = eventData.find(([tag]) => tag === "d")?.[1]; - if (courseId) { - // try { - // const response = await axios.get(`/api/resources/${courseId}`); - // return response.status === 200; - // } catch (error) { - // // Handle 404 or other errors gracefully - // return false; - // } - return true; - } - } - return false; - }; - fetchEvents(filter, 'courses', hasRequiredTags); - }; - - // const fetchStreams = () => { - // const filter = [{kinds: [30311], authors: [AUTHOR_PUBKEY]}]; - // const hasRequiredTags = (eventData) => eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); - // fetchEvents(filter, 'streams', hasRequiredTags); - // } - - const fetchKind0 = async (criteria, params) => { - return new Promise((resolve, reject) => { - const events = []; - const timeoutDuration = 1000; - - const sub = pool.current.subscribeMany(relays, criteria, { - ...params, - onevent: (event) => { - events.push(event); - }, - onerror: (error) => { - reject(error); - } - }); - - // Set a timeout to sort and resolve with the most recent event - setTimeout(() => { - if (events.length === 0) { - resolve(null); // or reject based on your needs - } else { - events.sort((a, b) => b.created_at - a.created_at); // Sort in descending order - const mostRecentEventContent = JSON.parse(events[0].content); - resolve(mostRecentEventContent); - } - }, timeoutDuration); - }); - }; - - const fetchSingleEvent = async (id) => { - return new Promise((resolve, reject) => { - const sub = pool.current.subscribeMany(relays, [{ ids: [id] }], { - onevent: (event) => { - resolve(event); - }, - onerror: (error) => { - reject(error); - }, - onclose: () => { - // Handle connection closure and retry if needed - console.log("Connection closed"); - // Implement retry logic here - }, - oneose: () => { - console.log("Subscription closed"); - sub.close(); - } - }); - }); - } - - const publishEvent = async (relay, signedEvent) => { - console.log('publishing event to', relay); - return new Promise((resolve, reject) => { - const timeout = 3000 - const wsRelay = new window.WebSocket(relay) - let timer - let isMessageSentSuccessfully = false - - function timedout() { - clearTimeout(timer) - wsRelay.close() - reject(new Error(`relay timeout for ${relay}`)) - } - - timer = setTimeout(timedout, timeout) - - wsRelay.onopen = function () { - clearTimeout(timer) - timer = setTimeout(timedout, timeout) - wsRelay.send(JSON.stringify(['EVENT', signedEvent])) - } - - wsRelay.onmessage = function (msg) { - const m = JSON.parse(msg.data) - if (m[0] === 'OK') { - isMessageSentSuccessfully = true - clearTimeout(timer) - wsRelay.close() - console.log('Successfully sent event to', relay) - resolve() - } - } - - wsRelay.onerror = function (error) { - clearTimeout(timer) - console.log(error) - reject(new Error(`relay error: Failed to send to ${relay}`)) - } - - wsRelay.onclose = function () { - clearTimeout(timer) - if (!isMessageSentSuccessfully) { - reject(new Error(`relay error: Failed to send to ${relay}`)) - } - } - }) - }; - - - const publishAll = async (signedEvent) => { - try { - const promises = relays.map(relay => publishEvent(relay, signedEvent)); - const results = await Promise.allSettled(promises) - const successfulRelays = [] - const failedRelays = [] - - results.forEach((result, i) => { - if (result.status === 'fulfilled') { - successfulRelays.push(relays[i]) - showToast('success', `published to ${relays[i]}`) - } else { - failedRelays.push(relays[i]) - showToast('error', `failed to publish to ${relays[i]}`) - } - }) - - return { successfulRelays, failedRelays } - } catch (error) { - console.error('Error publishing event:', error); - } - }; - - - useEffect(() => { - getRelayStatuses(); - - const currentSubscriptions = subscriptions.current; - - return () => { - // Close all active subscriptions on cleanup - currentSubscriptions.forEach((sub) => { - sub.close(); - }); - }; - }, []); - - return { - updateRelays, - fetchSingleEvent, - publishAll, - fetchKind0, - fetchResources, - fetchCourses, - fetchWorkshops, - // fetchStreams, - zapEvent, - fetchZapsForEvent, - getRelayStatuses, - events - }; -}; \ No newline at end of file diff --git a/src/pages/draft/[slug]/index.js b/src/pages/draft/[slug]/index.js index d63643f..82fce22 100644 --- a/src/pages/draft/[slug]/index.js +++ b/src/pages/draft/[slug]/index.js @@ -196,7 +196,7 @@ export default function Details() { ...draft.topics.map(topic => ['t', topic]), ['published_at', Math.floor(Date.now() / 1000).toString()], // Include price and location tags only if price is present - ...(draft?.price ? [['price', draft.price], ['location', `https://plebdevs.com/resource/${draft.id}`]] : []), + ...(draft?.price ? [['price', draft.price], ['location', `https://plebdevs.com/details/${draft.id}`]] : []), ] }; type = 'resource'; diff --git a/src/pages/login.js b/src/pages/login.js index 970d4bb..096d8a5 100644 --- a/src/pages/login.js +++ b/src/pages/login.js @@ -2,7 +2,6 @@ import React from "react"; import { Button } from 'primereact/button'; import { useLogin } from "@/hooks/useLogin"; - const Login = () => { const { nostrLogin, anonymousLogin } = useLogin(); return ( diff --git a/src/pages/resource/[slug].js b/src/pages/resource/[slug].js deleted file mode 100644 index b434a39..0000000 --- a/src/pages/resource/[slug].js +++ /dev/null @@ -1,46 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import { useNostr } from "@/hooks/useNostr"; -import { parseEvent } from "@/utils/nostr"; - - -const Resource = () => { - const [resource, setResource] = useState(null); - - const router = useRouter(); - const { fetchSingleEvent } = useNostr(); - - const { slug } = router.query; - - console.log('slug:', slug); - - useEffect(() => { - const getResource = async () => { - if (slug) { - const fetchedResource = await fetchSingleEvent(slug); - console.log('fetchedResource:', fetchedResource); - const formattedResource = parseEvent(fetchedResource); - console.log('formattedResource:', formattedResource.summary); - setResource(formattedResource); - } - }; - - if (slug && !resource) { - getResource(); - } - }, [slug]); - - return ( -
-

{resource?.title}

-

{resource?.summary}

-
- { - resource?.content && - } -
-
- ); -} - -export default Resource; \ No newline at end of file