Fix bitcoin connect init bug, fix resource fetching and publishing flows in useNostr, added delete for drafts

This commit is contained in:
austinkelsay 2024-07-21 11:11:24 -05:00
parent a9a443fed1
commit ff0a0facaf
3 changed files with 189 additions and 151 deletions

View File

@ -9,21 +9,24 @@ const Button = dynamic(
} }
); );
let initialized = false;
export async function initializeBitcoinConnect() {
if (!initialized) {
const { init } = await import('@getalby/bitcoin-connect-react');
init({
appName: "PlebDevs",
filters: ["nwc"],
showBalance: false
});
initialized = true;
}
}
const BitcoinConnectButton = () => { const BitcoinConnectButton = () => {
useEffect(() => { useEffect(() => {
const initializeBitcoinConnect = async () => {
// Initialize Bitcoin Connect
const { init } = await import('@getalby/bitcoin-connect-react');
init({
appName: "PlebDevs",
filters: ["nwc"],
showBalance: false
});
};
initializeBitcoinConnect(); initializeBitcoinConnect();
}, []); // Empty dependency array to run only once on component mount }, []);
return ( return (
<Button onConnect={(provider) => { <Button onConnect={(provider) => {
@ -32,4 +35,4 @@ const BitcoinConnectButton = () => {
); );
} }
export default BitcoinConnectButton; export default BitcoinConnectButton;

View File

@ -3,6 +3,8 @@ import axios from 'axios';
import { nip57, nip19 } from 'nostr-tools'; import { nip57, nip19 } from 'nostr-tools';
import { NostrContext } from '@/context/NostrContext'; import { NostrContext } from '@/context/NostrContext';
import { lnurlEncode } from '@/utils/lnurl'; import { lnurlEncode } from '@/utils/lnurl';
import { parseEvent } from '@/utils/nostr';
import { v4 as uuidv4 } from 'uuid';
const defaultRelays = [ const defaultRelays = [
"wss://nos.lol/", "wss://nos.lol/",
@ -228,29 +230,29 @@ export function useNostr() {
const fetchKind0 = useCallback( const fetchKind0 = useCallback(
async (publicKey) => { async (publicKey) => {
return new Promise((resolve) => { return new Promise((resolve) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
resolve(null); // Resolve with null if no event is received within the timeout resolve(null); // Resolve with null if no event is received within the timeout
}, 10000); // 10 seconds timeout }, 10000); // 10 seconds timeout
subscribe( subscribe(
[{ authors: [publicKey], kinds: [0] }], [{ authors: [publicKey], kinds: [0] }],
{ {
onevent: (event) => { onevent: (event) => {
clearTimeout(timeout); clearTimeout(timeout);
resolve(JSON.parse(event.content)); resolve(JSON.parse(event.content));
}, },
onerror: (error) => { onerror: (error) => {
clearTimeout(timeout); clearTimeout(timeout);
console.error('Error fetching kind 0:', error); console.error('Error fetching kind 0:', error);
resolve(null); resolve(null);
}, },
} }
); );
}); });
}, },
[subscribe] [subscribe]
); );
const zapEvent = useCallback( const zapEvent = useCallback(
async (event, amount, comment) => { async (event, amount, comment) => {
@ -340,37 +342,36 @@ export function useNostr() {
const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }]; const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = (tags) => { const hasRequiredTags = (tags) => {
const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
// Check if 'resource' tag exists
const hasResource = tags.some(([tag, value]) => tag === "t" && value === "resource"); const hasResource = tags.some(([tag, value]) => tag === "t" && value === "resource");
// Return true if both tags exist
return hasPlebDevs && hasResource; return hasPlebDevs && hasResource;
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let resources = []; let resources = [];
const subscription = subscribe( const subscription = subscribe(
filter, filter,
{ {
onevent: (event) => { onevent: (event) => {
if (hasRequiredTags(event.tags)) { if (hasRequiredTags(event.tags)) {
console.log('event:', event);
resources.push(event); resources.push(event);
} }
}, },
onerror: (error) => { onerror: (error) => {
console.error('Error fetching resources:', error); console.error('Error fetching resources:', error);
subscription?.close(); // Don't resolve here, just log the error
resolve(resources);
}, },
onclose: () => { onclose: () => {
resolve(resources); // Don't resolve here either
}, },
}, },
2000 // Adjust the timeout value as needed 2000 // Adjust the timeout value as needed
); );
// Set a timeout to resolve the promise after collecting events
setTimeout(() => { setTimeout(() => {
subscription?.close(); subscription?.close();
console.log('Resolving with resources:', resources);
resolve(resources); resolve(resources);
}, 2000); // Adjust the timeout value as needed }, 2000); // Adjust the timeout value as needed
}); });
@ -378,32 +379,33 @@ export function useNostr() {
const fetchWorkshops = useCallback(async () => { const fetchWorkshops = useCallback(async () => {
const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }]; const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }];
console.log('filter:', filter);
const hasRequiredTags = (tags) => { const hasRequiredTags = (tags) => {
const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
const hasWorkshop = tags.some(([tag, value]) => tag === "t" && value === "workshop"); const hasWorkshop = tags.some(([tag, value]) => tag === "t" && value === "workshop");
return hasPlebDevs && hasWorkshop; return hasPlebDevs && hasWorkshop;
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let workshops = []; let workshops = [];
const subscription = subscribe( const subscription = subscribe(
filter, filter,
{ {
onevent: (event) => { onevent: (event) => {
console.log('Received workshop event:', event);
if (hasRequiredTags(event.tags)) { if (hasRequiredTags(event.tags)) {
console.log('Workshop event passed tag check, adding to workshops');
workshops.push(event); workshops.push(event);
} else {
console.log('Workshop event did not pass tag check');
} }
}, },
onerror: (error) => { onerror: (error) => {
console.error('Error fetching workshops:', error); console.error('Error fetching workshops:', error);
subscription?.close(); // Don't resolve here, just log the error
resolve(workshops);
}, },
onclose: () => { onclose: () => {
resolve(workshops); // Don't resolve here either
}, },
}, },
2000 // Adjust the timeout value as needed 2000 // Adjust the timeout value as needed
@ -411,6 +413,7 @@ export function useNostr() {
setTimeout(() => { setTimeout(() => {
subscription?.close(); subscription?.close();
console.log('Resolving with workshops:', workshops);
resolve(workshops); resolve(workshops);
}, 2000); // Adjust the timeout value as needed }, 2000); // Adjust the timeout value as needed
}); });
@ -420,30 +423,30 @@ export function useNostr() {
const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }]; const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = (tags) => { const hasRequiredTags = (tags) => {
const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
const hasCourse = tags.some(([tag, value]) => tag === "t" && value === "course"); const hasCourse = tags.some(([tag, value]) => tag === "t" && value === "course");
return hasPlebDevs && hasCourse; return hasPlebDevs && hasCourse;
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let courses = []; let courses = [];
const subscription = subscribe( const subscription = subscribe(
filter, filter,
{ {
onevent: (event) => { onevent: (event) => {
console.log('Received course event:', event);
if (hasRequiredTags(event.tags)) { if (hasRequiredTags(event.tags)) {
console.log('Course event passed tag check, adding to courses');
courses.push(event); courses.push(event);
} else {
console.log('Course event did not pass tag check');
} }
}, },
onerror: (error) => { onerror: (error) => {
console.error('Error fetching courses:', error); console.error('Error fetching courses:', error);
subscription?.close(); // Don't resolve here, just log the error
resolve(courses);
}, },
onclose: () => { onclose: () => {
resolve(courses); // Don't resolve here either
}, },
}, },
2000 // Adjust the timeout value as needed 2000 // Adjust the timeout value as needed
@ -451,6 +454,7 @@ export function useNostr() {
setTimeout(() => { setTimeout(() => {
subscription?.close(); subscription?.close();
console.log('Resolving with courses:', courses);
resolve(courses); resolve(courses);
}, 2000); // Adjust the timeout value as needed }, 2000); // Adjust the timeout value as needed
}); });
@ -458,92 +462,87 @@ export function useNostr() {
const publishResource = useCallback( const publishResource = useCallback(
async (resourceEvent) => { async (resourceEvent) => {
const published = await publish(resourceEvent); const published = await publish(resourceEvent);
if (published) {
const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(resourceEvent);
const user = window.localStorage.getItem('user'); if (published) {
const userId = JSON.parse(user).id; const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(resourceEvent);
const payload = { const user = window.localStorage.getItem('user');
const userId = JSON.parse(user).id;
};
const payload = {
if (payload && payload.user) { id: uuidv4(),
try { user: {
const response = await axios.post('/api/resources', payload); connect: { id: userId } // This is the correct way to connect to an existing user
},
if (response.status === 201) { noteId: id
try { };
const deleteResponse = await axios.delete(`/api/drafts/${resourceEvent.id}`);
if (payload && payload.user) {
if (deleteResponse.status === 204) { try {
return true; const response = await axios.post('/api/resources', payload);
if (response.status === 201) {
return true;
}
} catch (error) {
console.error('Error creating resource:', error);
return false;
} }
} catch (error) {
console.error('Error deleting draft:', error);
return false;
}
} }
} catch (error) {
console.error('Error creating resource:', error);
return false;
}
} }
}
return false;
return false;
}, },
[publish] [publish]
); );
const publishCourse = useCallback( const publishCourse = useCallback(
async (courseEvent) => { async (courseEvent) => {
const published = await publish(courseEvent); const published = await publish(courseEvent);
if (published) { if (published) {
const user = window.localStorage.getItem('user'); const user = window.localStorage.getItem('user');
const pubkey = JSON.parse(user).pubkey; const pubkey = JSON.parse(user).pubkey;
const payload = { const payload = {
title: courseEvent.title, title: courseEvent.title,
summary: courseEvent.summary, summary: courseEvent.summary,
type: 'course', type: 'course',
content: courseEvent.content, content: courseEvent.content,
image: courseEvent.image, image: courseEvent.image,
user: pubkey, user: pubkey,
topics: [...courseEvent.topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'course'] topics: [...courseEvent.topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'course']
}; };
if (payload && payload.user) { if (payload && payload.user) {
try { try {
const response = await axios.post('/api/courses', payload); const response = await axios.post('/api/courses', payload);
if (response.status === 201) { if (response.status === 201) {
try { try {
const deleteResponse = await axios.delete(`/api/drafts/${courseEvent.id}`); const deleteResponse = await axios.delete(`/api/drafts/${courseEvent.id}`);
if (deleteResponse.status === 204) { if (deleteResponse.status === 204) {
return true; return true;
}
} catch (error) {
console.error('Error deleting draft:', error);
return false;
}
}
} catch (error) {
console.error('Error creating course:', error);
return false;
} }
} catch (error) {
console.error('Error deleting draft:', error);
return false;
}
} }
} catch (error) {
console.error('Error creating course:', error);
return false;
}
} }
}
return false;
return false;
}, },
[publish] [publish]
); );
return { subscribe, publish, fetchSingleEvent, fetchZapsForEvent, fetchKind0, fetchResources, fetchWorkshops, fetchCourses, zapEvent, fetchZapsForEvents, publishResource, publishCourse }; return { subscribe, publish, fetchSingleEvent, fetchZapsForEvent, fetchKind0, fetchResources, fetchWorkshops, fetchCourses, zapEvent, fetchZapsForEvents, publishResource, publishCourse };
} }

View File

@ -72,10 +72,42 @@ export default function Details() {
const { unsignedEvent, type } = await buildEvent(draft); const { unsignedEvent, type } = await buildEvent(draft);
if (unsignedEvent) { if (unsignedEvent) {
await publishEvent(unsignedEvent, type); const published = await publishEvent(unsignedEvent, type);
// if successful, delete the draft, redirect to profile
if (published) {
axios.delete(`/api/drafts/${draft.id}`)
.then(res => {
if (res.status === 204) {
showToast('success', 'Success', 'Draft deleted successfully.');
router.push(`/profile`);
} else {
showToast('error', 'Error', 'Failed to delete draft.');
}
})
.catch(err => {
console.error(err);
});
}
} else {
showToast('error', 'Error', 'Failed to broadcast resource. Please try again.');
} }
} else { }
showToast('error', 'Error', 'Failed to broadcast resource. Please try again.'); }
const handleDelete = async () => {
if (draft) {
await axios.delete(`/api/drafts/${draft.id}`)
.then(res => {
if (res.status === 204) {
showToast('success', 'Success', 'Draft deleted successfully.');
router.push(`/profile`);
} else {
showToast('error', 'Error', 'Failed to delete draft.');
}
})
.catch(err => {
console.error(err);
});
} }
} }
@ -127,28 +159,31 @@ export default function Details() {
published = await publishCourse(signedEvent); published = await publishCourse(signedEvent);
} }
console.log('published:', published);
if (published) { if (published) {
// check if the event is published // check if the event is published
const publishedEvent = await fetchSingleEvent(signedEvent.id); const publishedEvent = await fetchSingleEvent(signedEvent.id);
console.log('publishedEvent:', publishedEvent); console.log('publishedEvent:', publishedEvent);
if (publishedEvent) { if (publishedEvent) {
// show success message // show success message
showToast('success', 'Success', `${type} published successfully.`); showToast('success', 'Success', `${type} published successfully.`);
// delete the draft // delete the draft
console.log('draft:', draft);
await axios.delete(`/api/drafts/${draft.id}`) await axios.delete(`/api/drafts/${draft.id}`)
.then(res => { .then(res => {
if (res.status === 204) { if (res.status === 204) {
showToast('success', 'Success', 'Draft deleted successfully.'); showToast('success', 'Success', 'Draft deleted successfully.');
router.push(`/profile`); router.push(`/profile`);
} else { } else {
showToast('error', 'Error', 'Failed to delete draft.'); showToast('error', 'Error', 'Failed to delete draft.');
} }
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
}); });
} }
} }
} }
@ -163,7 +198,7 @@ export default function Details() {
case 'resource': case 'resource':
if (draft?.price) { if (draft?.price) {
// encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY // encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY
encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY ,process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content); encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content);
} }
event = { event = {
kind: draft?.price ? 30402 : 30023, // Determine kind based on if price is present kind: draft?.price ? 30402 : 30023, // Determine kind based on if price is present
@ -185,7 +220,7 @@ export default function Details() {
case 'workshop': case 'workshop':
if (draft?.price) { if (draft?.price) {
// encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY // encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY
encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY ,process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content); encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content);
} }
event = { event = {
kind: draft?.price ? 30402 : 30023, kind: draft?.price ? 30402 : 30023,
@ -254,12 +289,12 @@ export default function Details() {
/> />
{user && user?.pubkey && ( {user && user?.pubkey && (
<p className='text-lg'> <p className='text-lg'>
Created by{' '} Created by{' '}
<a href={`https://nostr.com/${hexToNpub(user?.pubkey)}`} rel='noreferrer noopener' target='_blank' className='text-blue-500 hover:underline'> <a href={`https://nostr.com/${hexToNpub(user?.pubkey)}`} rel='noreferrer noopener' target='_blank' className='text-blue-500 hover:underline'>
{user?.username || user?.pubkey.slice(0, 10)}{'... '} {user?.username || user?.pubkey.slice(0, 10)}{'... '}
</a> </a>
</p> </p>
)} )}
</div> </div>
</div> </div>
<div className='flex flex-col max-tab:mt-12 max-mob:mt-12'> <div className='flex flex-col max-tab:mt-12 max-mob:mt-12'>
@ -281,9 +316,10 @@ export default function Details() {
</div> </div>
</div> </div>
<div className='w-[75vw] mx-auto flex flex-row justify-end mt-12'> <div className='w-[75vw] mx-auto flex flex-row justify-end mt-12'>
<div className='w-[15vw] flex flex-row justify-between'> <div className='w-fit flex flex-row justify-between'>
<Button onClick={() => router.push(`/draft/${draft?.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto my-2" /> <Button onClick={handleSubmit} label="Publish" severity='success' outlined className="w-auto m-2" />
<Button onClick={handleSubmit} label="Publish" severity='success' outlined className="w-auto my-2" /> <Button onClick={() => router.push(`/draft/${draft?.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
<Button onClick={handleDelete} label="Delete" severity='danger' outlined className="w-auto m-2 mr-0" />
</div> </div>
</div> </div>
<div className='w-[75vw] mx-auto mt-12 p-12 border-t-2 border-gray-300 max-tab:p-0 max-mob:p-0 max-tab:max-w-[100vw] max-mob:max-w-[100vw]'> <div className='w-[75vw] mx-auto mt-12 p-12 border-t-2 border-gray-300 max-tab:p-0 max-mob:p-0 max-tab:max-w-[100vw] max-mob:max-w-[100vw]'>