Implemented basic zap function, starting on listening for zaps

This commit is contained in:
austinkelsay 2024-03-27 18:12:17 -05:00
parent 6eb4edf617
commit 6ef8f2cb88
4 changed files with 111 additions and 39 deletions

View File

@ -1,7 +1,5 @@
import React, { useState, useEffect, use } from 'react'; import React, { useState, useEffect } from 'react';
import { Carousel } from 'primereact/carousel'; import { Carousel } from 'primereact/carousel';
import { useRouter } from 'next/router';
import { useImageProxy } from '@/hooks/useImageProxy';
import { parseEvent } from '@/utils/nostr'; import { parseEvent } from '@/utils/nostr';
import { useNostr } from '@/hooks/useNostr'; import { useNostr } from '@/hooks/useNostr';
import CourseTemplate from '@/components/content/carousels/templates/CourseTemplate'; import CourseTemplate from '@/components/content/carousels/templates/CourseTemplate';
@ -27,11 +25,8 @@ const responsiveOptions = [
export default function CoursesCarousel() { export default function CoursesCarousel() {
const [processedCourses, setProcessedCourses] = useState([]); const [processedCourses, setProcessedCourses] = useState([]);
const [screenWidth, setScreenWidth] = useState(null);
const [courses, setCourses] = useState([]); const [courses, setCourses] = useState([]);
const router = useRouter();
const { fetchCourses, events } = useNostr(); const { fetchCourses, events } = useNostr();
const { returnImageProxy } = useImageProxy();
useEffect(() => { useEffect(() => {
if (events && events.courses && events.courses.length > 0) { if (events && events.courses && events.courses.length > 0) {
@ -41,35 +36,6 @@ export default function CoursesCarousel() {
} }
}, [events]); }, [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(() => { useEffect(() => {
const processCourses = courses.map(course => { const processCourses = courses.map(course => {
const { id, content, title, summary, image, published_at } = parseEvent(course); const { id, content, title, summary, image, published_at } = parseEvent(course);

View File

@ -1,14 +1,27 @@
import React from "react"; import React, {useEffect, useState} from "react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useResponsiveImageDimensions from "@/hooks/useResponsiveImageDimensions"; import useResponsiveImageDimensions from "@/hooks/useResponsiveImageDimensions";
import { formatTimestampToHowLongAgo } from "@/utils/time"; import { formatTimestampToHowLongAgo } from "@/utils/time";
import { useImageProxy } from "@/hooks/useImageProxy"; import { useImageProxy } from "@/hooks/useImageProxy";
import { useNostr } from "@/hooks/useNostr";
const CourseTemplate = (course) => { const CourseTemplate = (course) => {
const [zaps, setZaps] = useState([]);
const router = useRouter(); const router = useRouter();
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const { width, height } = useResponsiveImageDimensions(); 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 ( return (
<div style={{width: width < 768 ? "auto" : width}} onClick={() => router.push(`/details/${course.id}`)} className="flex flex-col items-center mx-auto px-4 cursor-pointer mt-8 rounded-md shadow-lg"> <div style={{width: width < 768 ? "auto" : width}} onClick={() => router.push(`/details/${course.id}`)} className="flex flex-col items-center mx-auto px-4 cursor-pointer mt-8 rounded-md shadow-lg">
<div style={{maxWidth: width, minWidth: width}} className="max-tab:h-auto max-mob:h-auto"> <div style={{maxWidth: width, minWidth: width}} className="max-tab:h-auto max-mob:h-auto">

View File

@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from "react"; 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 axios from "axios";
import { useToast } from "./useToast"; import { useToast } from "./useToast";
@ -20,7 +20,8 @@ export const useNostr = () => {
resources: [], resources: [],
workshops: [], workshops: [],
courses: [], courses: [],
streams: [] streams: [],
zaps: []
}); });
const { showToast } = useToast(); const { showToast } = useToast();
@ -53,6 +54,9 @@ export const useNostr = () => {
try { try {
const sub = pool.current.subscribeMany(relays, filter, { const sub = pool.current.subscribeMany(relays, filter, {
onevent: async (event) => { onevent: async (event) => {
if (event.kind === 9735) {
console.log('event:', event);
}
const shouldInclude = await hasRequiredTags(event.tags); const shouldInclude = await hasRequiredTags(event.tags);
if (shouldInclude) { if (shouldInclude) {
setEvents(prevData => ({ 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 // Fetch resources, workshops, courses, and streams with appropriate filters and update functions
const fetchResources = async () => { const fetchResources = async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
@ -288,6 +370,8 @@ export const useNostr = () => {
fetchCourses, fetchCourses,
fetchWorkshops, fetchWorkshops,
// fetchStreams, // fetchStreams,
zapEvent,
fetchZapsForEvent,
getRelayStatuses, getRelayStatuses,
events events
}; };

View File

@ -27,10 +27,18 @@ export default function Details() {
const [author, setAuthor] = useState(null); const [author, setAuthor] = useState(null);
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const { fetchSingleEvent, fetchKind0 } = useNostr(); const { fetchSingleEvent, fetchKind0, zapEvent } = useNostr();
const router = useRouter(); const router = useRouter();
const handleZapEvent = async () => {
if (!event) return;
const response = await zapEvent(event);
console.log('zap response:', response);
}
useEffect(() => { useEffect(() => {
if (router.isReady) { if (router.isReady) {
const { slug } = router.query; const { slug } = router.query;
@ -112,6 +120,7 @@ export default function Details() {
label="Zap" label="Zap"
severity="success" severity="success"
outlined outlined
onClick={handleZapEvent}
pt={{ pt={{
button: { button: {
icon: ({ context }) => ({ icon: ({ context }) => ({