From 6db4f4939c8ebc3c31d2ef0c3354dbc08c4b6ef5 Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Tue, 3 Sep 2024 17:40:22 -0500 Subject: [PATCH] Nostr feed uses subscription hook now with ndk, can send nostr messages in community, some styling fixes --- src/components/feeds/MessageInput.js | 30 ++++- src/components/feeds/NostrFeed.js | 41 +++---- src/components/forms/course/LessonSelector.js | 2 +- src/components/sidebar/Sidebar.js | 15 ++- src/components/sidebar/sidebar.module.css | 12 ++ src/hooks/nostr/useCommunityNotes.js | 111 ++++++++++++------ src/pages/feed.js | 12 +- 7 files changed, 150 insertions(+), 73 deletions(-) create mode 100644 src/components/sidebar/sidebar.module.css diff --git a/src/components/feeds/MessageInput.js b/src/components/feeds/MessageInput.js index cc84e84..0b725f3 100644 --- a/src/components/feeds/MessageInput.js +++ b/src/components/feeds/MessageInput.js @@ -2,9 +2,36 @@ import React, { useState } from 'react'; import { InputTextarea } from 'primereact/inputtextarea'; import { Button } from 'primereact/button'; import { Panel } from 'primereact/panel'; +import { useNDKContext } from "@/context/NDKContext"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; +import { useToast } from '@/hooks/useToast'; -const MessageInput = ({ collapsed, onToggle }) => { +const MessageInput = ({ collapsed, onToggle, onMessageSent }) => { const [message, setMessage] = useState(''); + const { ndk, addSigner } = useNDKContext(); + const { showToast } = useToast(); + const handleSubmit = async () => { + if (!message.trim() || !ndk) return; + + try { + if (!ndk.signer) { + await addSigner(); + } + + const event = new NDKEvent(ndk); + event.kind = 1; + event.content = message; + event.tags = [['t', 'plebdevs']]; + + await event.publish(); + showToast('success', 'Message Sent', 'Your message has been sent to the PlebDevs community.'); + setMessage(''); // Clear the input after successful publish + onMessageSent(); // Call this function to close the accordion + } catch (error) { + console.error("Error publishing message:", error); + showToast('error', 'Error', 'There was an error sending your message. Please try again.'); + } + }; return ( { icon="pi pi-send" outlined className='mt-2' + onClick={handleSubmit} /> diff --git a/src/components/feeds/NostrFeed.js b/src/components/feeds/NostrFeed.js index 33980bf..d187909 100644 --- a/src/components/feeds/NostrFeed.js +++ b/src/components/feeds/NostrFeed.js @@ -4,62 +4,51 @@ import { Avatar } from 'primereact/avatar'; import { Tag } from 'primereact/tag'; import { Button } from 'primereact/button'; import { ProgressSpinner } from 'primereact/progressspinner'; -import { useCommunityNotes } from '@/hooks/nostr/useCommunityNotes'; -import { useRouter } from 'next/router'; import { useNDKContext } from '@/context/NDKContext'; import { findKind0Fields } from '@/utils/nostr'; import NostrIcon from '../../../public/images/nostr.png'; import Image from 'next/image'; import { useImageProxy } from '@/hooks/useImageProxy'; import { nip19 } from 'nostr-tools'; +import { useCommunityNotes } from '@/hooks/nostr/useCommunityNotes'; const NostrFeed = () => { - const router = useRouter(); - const { communityNotes, error, isLoading } = useCommunityNotes(); - const { ndk, addSigner } = useNDKContext(); + const { communityNotes, isLoading, error } = useCommunityNotes(); + const { ndk } = useNDKContext(); const { returnImageProxy } = useImageProxy(); - const [authorData, setAuthorData] = useState({}); useEffect(() => { - const fetchAuthors = async () => { - const authorDataMap = {}; - for (const message of communityNotes) { - const author = await fetchAuthor(message.pubkey); - authorDataMap[message.pubkey] = author; + communityNotes.forEach(note => { + if (!authorData[note.pubkey]) { + fetchAuthor(note.pubkey); } - setAuthorData(authorDataMap); - }; - - if (communityNotes && communityNotes.length > 0) { - fetchAuthors(); - } - }, [communityNotes]); + }); + }, [communityNotes, authorData]); const fetchAuthor = async (pubkey) => { try { - await ndk.connect(); - const filter = { kinds: [0], authors: [pubkey] - } + }; const author = await ndk.fetchEvent(filter); if (author) { try { const fields = await findKind0Fields(JSON.parse(author.content)); - return fields; + setAuthorData(prevData => ({ + ...prevData, + [pubkey]: fields + })); } catch (error) { console.error('Error fetching author:', error); } - } else { - return null; } } catch (error) { console.error('Error fetching author:', error); } - } + }; const renderHeader = (message) => { const author = authorData[message.pubkey]; @@ -115,7 +104,7 @@ const NostrFeed = () => { return (
- {communityNotes && communityNotes.length > 0 ? ( + {communityNotes.length > 0 ? ( communityNotes.map(message => ( handleContentSelect(e.value, index)} - placeholder={lesson.id ? lesson.title : "Create New Lesson"} + placeholder={lesson.id ? lesson.title : "Select Lesson"} optionLabel="label" optionGroupLabel="label" optionGroupChildren="items" diff --git a/src/components/sidebar/Sidebar.js b/src/components/sidebar/Sidebar.js index 468c1d4..69cffe0 100644 --- a/src/components/sidebar/Sidebar.js +++ b/src/components/sidebar/Sidebar.js @@ -2,6 +2,7 @@ import React from 'react'; import { Accordion, AccordionTab } from 'primereact/accordion'; import { useRouter } from 'next/router'; import 'primeicons/primeicons.css'; +import styles from "./sidebar.module.css"; const Sidebar = () => { const router = useRouter(); @@ -23,12 +24,14 @@ const Sidebar = () => {
router.push('/create')} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/create') ? 'bg-gray-700' : ''}`}>

Create

- - ({ - className: `hover:bg-gray-700 rounded-lg ${isActive('/feed') ? 'bg-gray-700' : ''}` - }) - }} + + ({ + className: `hover:bg-gray-700 rounded-lg ${isActive('/feed') ? 'bg-gray-700' : ''} ${styles['p-accordion-header-link']}` + }), + content: styles['p-accordion-content'] + }} header={"Community"}>
router.push('/feed?channel=global')} className={`w-full cursor-pointer py-2 hover:bg-gray-700 rounded-lg ${isActive('/feed?channel=global') ? 'bg-gray-700' : ''}`}>

global

diff --git a/src/components/sidebar/sidebar.module.css b/src/components/sidebar/sidebar.module.css new file mode 100644 index 0000000..af22992 --- /dev/null +++ b/src/components/sidebar/sidebar.module.css @@ -0,0 +1,12 @@ +.p-accordion .p-accordion-content { + border: none !important; + padding-top: 0px !important; +} +.p-accordion .p-accordion-header-link { + border: none !important; + padding-bottom: 12px !important; + padding-top: 12px !important; + margin-bottom: 8px !important; + border-bottom-left-radius: 7px !important; + border-bottom-right-radius: 7px !important; +} \ No newline at end of file diff --git a/src/hooks/nostr/useCommunityNotes.js b/src/hooks/nostr/useCommunityNotes.js index 8062901..cdb2df9 100644 --- a/src/hooks/nostr/useCommunityNotes.js +++ b/src/hooks/nostr/useCommunityNotes.js @@ -1,52 +1,87 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useNDKContext } from '@/context/NDKContext'; +import { NDKSubscriptionCacheUsage } from "@nostr-dev-kit/ndk"; export function useCommunityNotes() { - const [isClient, setIsClient] = useState(false); - const [communityNotes, setCommunityNotes] = useState(); - // Add new state variables for loading and error - const [isLoading, setIsLoading] = useState(false); + const [communityNotes, setCommunityNotes] = useState([]); + const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const {ndk, addSigner} = useNDKContext(); + const { ndk } = useNDKContext(); - useEffect(() => { - setIsClient(true); + const addNote = useCallback((noteEvent) => { + setCommunityNotes((prevNotes) => { + if (prevNotes.some(note => note.id === noteEvent.id)) return prevNotes; + const newNotes = [noteEvent, ...prevNotes]; + newNotes.sort((a, b) => b.created_at - a.created_at); + return newNotes; + }); }, []); - const fetchCommunityNotesFromNDK = async () => { + useEffect(() => { + let subscription; + const noteIds = new Set(); + let timeoutId; + + async function subscribeToNotes() { + if (!ndk) return; + + try { + await ndk.connect(); + + const filter = { + kinds: [1], + '#t': ['plebdevs'] + }; + + subscription = ndk.subscribe(filter, { + closeOnEose: false, + cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST + }); + + subscription.on('event', (noteEvent) => { + if (!noteIds.has(noteEvent.id)) { + noteIds.add(noteEvent.id); + addNote(noteEvent); + setIsLoading(false); + clearTimeout(timeoutId); + } + }); + + subscription.on('close', () => { + setIsLoading(false); + }); + + subscription.on('eose', () => { + console.log("eose in useCommunityNotes"); + setIsLoading(false); + }); + + await subscription.start(); + + // Set a 4-second timeout to stop loading state if no notes are received + timeoutId = setTimeout(() => { + setIsLoading(false); + }, 4000); + + } catch (err) { + console.error('Error subscribing to notes:', err); + setError(err.message); + setIsLoading(false); + } + } + + setCommunityNotes([]); setIsLoading(true); setError(null); - try { - await ndk.connect(); + subscribeToNotes(); - const filter = { kinds: [1], "#t": ["plebdevs"] }; - const events = await ndk.fetchEvents(filter); - - if (events && events.size > 0) { - const eventsArray = Array.from(events); - setCommunityNotes(eventsArray); - setIsLoading(false); - return eventsArray; + return () => { + if (subscription) { + subscription.stop(); } - setIsLoading(false); - return []; - } catch (error) { - console.error('Error fetching community notes from NDK:', error); - setError(error); - setIsLoading(false); - return []; - } - }; - - useEffect(() => { - if (isClient) { - fetchCommunityNotesFromNDK().then(fetchedCommunityNotes => { - if (fetchedCommunityNotes && fetchedCommunityNotes.length > 0) { - setCommunityNotes(fetchedCommunityNotes); - } - }); - } - }, [isClient]); + clearTimeout(timeoutId); + }; + }, [ndk, addNote]); return { communityNotes, isLoading, error }; } \ No newline at end of file diff --git a/src/pages/feed.js b/src/pages/feed.js index 33fe464..29da029 100644 --- a/src/pages/feed.js +++ b/src/pages/feed.js @@ -11,6 +11,7 @@ import MessageInput from '@/components/feeds/MessageInput'; import StackerNewsIcon from '../../public/images/sn.svg'; import NostrIcon from '../../public/images/nostr.png'; import { Button } from 'primereact/button'; +import { Divider } from 'primereact/divider'; const Feed = () => { const [selectedTopic, setSelectedTopic] = useState('global'); @@ -54,6 +55,10 @@ const Feed = () => { setIsMessageInputCollapsed(e.value); }; + const handleMessageSent = () => { + setIsMessageInputCollapsed(true); + }; + return (
@@ -86,7 +91,12 @@ const Feed = () => { onClick={() => setIsMessageInputCollapsed(!isMessageInputCollapsed)} />
- + +