From 6ef8f2cb887de6ec64f5bf21c82ccd168d5dc3e3 Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Wed, 27 Mar 2024 18:12:17 -0500 Subject: [PATCH] Implemented basic zap function, starting on listening for zaps --- .../content/carousels/CoursesCarousel.js | 36 +------- .../carousels/templates/CourseTemplate.js | 15 +++- src/hooks/useNostr.js | 88 ++++++++++++++++++- src/pages/details/[slug].js | 11 ++- 4 files changed, 111 insertions(+), 39 deletions(-) diff --git a/src/components/content/carousels/CoursesCarousel.js b/src/components/content/carousels/CoursesCarousel.js index e957f74..056eadb 100644 --- a/src/components/content/carousels/CoursesCarousel.js +++ b/src/components/content/carousels/CoursesCarousel.js @@ -1,7 +1,5 @@ -import React, { useState, useEffect, use } from 'react'; +import React, { useState, useEffect } from 'react'; import { Carousel } from 'primereact/carousel'; -import { useRouter } from 'next/router'; -import { useImageProxy } from '@/hooks/useImageProxy'; import { parseEvent } from '@/utils/nostr'; import { useNostr } from '@/hooks/useNostr'; import CourseTemplate from '@/components/content/carousels/templates/CourseTemplate'; @@ -27,11 +25,8 @@ const responsiveOptions = [ export default function CoursesCarousel() { const [processedCourses, setProcessedCourses] = useState([]); - const [screenWidth, setScreenWidth] = useState(null); const [courses, setCourses] = useState([]); - const router = useRouter(); const { fetchCourses, events } = useNostr(); - const { returnImageProxy } = useImageProxy(); useEffect(() => { if (events && events.courses && events.courses.length > 0) { @@ -41,35 +36,6 @@ export default function CoursesCarousel() { } }, [events]); - useEffect(() => { - // Update the state to the current window width - setScreenWidth(window.innerWidth); - - const handleResize = () => { - // Update the state to the new window width when it changes - setScreenWidth(window.innerWidth); - }; - - window.addEventListener('resize', handleResize); - - // Remove the event listener on cleanup - return () => window.removeEventListener('resize', handleResize); - }, []); // The empty array ensures this effect only runs once, similar to componentDidMount - - - const calculateImageDimensions = () => { - if (screenWidth >= 1200) { - // Large screens - return { width: 426, height: 240 }; - } else if (screenWidth >= 768 && screenWidth < 1200) { - // Medium screens - return { width: 344, height: 194 }; - } else { - // Small screens - return { width: screenWidth - 120, height: (screenWidth - 120) * (9 / 16) }; - } - }; - useEffect(() => { const processCourses = courses.map(course => { const { id, content, title, summary, image, published_at } = parseEvent(course); diff --git a/src/components/content/carousels/templates/CourseTemplate.js b/src/components/content/carousels/templates/CourseTemplate.js index 6fff862..3c92cd1 100644 --- a/src/components/content/carousels/templates/CourseTemplate.js +++ b/src/components/content/carousels/templates/CourseTemplate.js @@ -1,14 +1,27 @@ -import React from "react"; +import React, {useEffect, useState} from "react"; import Image from "next/image"; import { useRouter } from "next/router"; import useResponsiveImageDimensions from "@/hooks/useResponsiveImageDimensions"; import { formatTimestampToHowLongAgo } from "@/utils/time"; import { useImageProxy } from "@/hooks/useImageProxy"; +import { useNostr } from "@/hooks/useNostr"; const CourseTemplate = (course) => { + const [zaps, setZaps] = useState([]); const router = useRouter(); const { returnImageProxy } = useImageProxy(); const { width, height } = useResponsiveImageDimensions(); + const {events, fetchZapsForEvent} = useNostr(); + + useEffect(() => { + if (events && events.zaps) { + console.log('zaps:', events.zaps); + setZaps(events.zaps); + } else { + fetchZapsForEvent(course.id); + } + }, [events]); + return (
router.push(`/details/${course.id}`)} className="flex flex-col items-center mx-auto px-4 cursor-pointer mt-8 rounded-md shadow-lg">
diff --git a/src/hooks/useNostr.js b/src/hooks/useNostr.js index 7d6280e..05ac10a 100644 --- a/src/hooks/useNostr.js +++ b/src/hooks/useNostr.js @@ -1,5 +1,5 @@ import { useState, useEffect, useRef } from "react"; -import { SimplePool, nip19, verifyEvent } from "nostr-tools"; +import { SimplePool, nip19, verifyEvent, nip57 } from "nostr-tools"; import axios from "axios"; import { useToast } from "./useToast"; @@ -20,7 +20,8 @@ export const useNostr = () => { resources: [], workshops: [], courses: [], - streams: [] + streams: [], + zaps: [] }); const { showToast } = useToast(); @@ -53,6 +54,9 @@ export const useNostr = () => { try { const sub = pool.current.subscribeMany(relays, filter, { onevent: async (event) => { + if (event.kind === 9735) { + console.log('event:', event); + } const shouldInclude = await hasRequiredTags(event.tags); if (shouldInclude) { setEvents(prevData => ({ @@ -75,6 +79,84 @@ export const useNostr = () => { } }; + // 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] }]; + const hasRequiredTags = async (eventData) => { + const hasEtag = eventData.some(([tag, value]) => tag === "e" && value === eventId); + return hasEtag; + }; + fetchEvents(filter, 'zaps', hasRequiredTags); + } + // Fetch resources, workshops, courses, and streams with appropriate filters and update functions const fetchResources = async () => { const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; @@ -288,6 +370,8 @@ export const useNostr = () => { fetchCourses, fetchWorkshops, // fetchStreams, + zapEvent, + fetchZapsForEvent, getRelayStatuses, events }; diff --git a/src/pages/details/[slug].js b/src/pages/details/[slug].js index 26e2a33..aa31f7b 100644 --- a/src/pages/details/[slug].js +++ b/src/pages/details/[slug].js @@ -27,10 +27,18 @@ export default function Details() { const [author, setAuthor] = useState(null); const { returnImageProxy } = useImageProxy(); - const { fetchSingleEvent, fetchKind0 } = useNostr(); + const { fetchSingleEvent, fetchKind0, zapEvent } = useNostr(); const router = useRouter(); + const handleZapEvent = async () => { + if (!event) return; + + const response = await zapEvent(event); + + console.log('zap response:', response); + } + useEffect(() => { if (router.isReady) { const { slug } = router.query; @@ -112,6 +120,7 @@ export default function Details() { label="Zap" severity="success" outlined + onClick={handleZapEvent} pt={{ button: { icon: ({ context }) => ({