Nostr feed uses subscription hook now with ndk, can send nostr messages in community, some styling fixes

This commit is contained in:
austinkelsay 2024-09-03 17:40:22 -05:00
parent 9b31e6cf18
commit 6db4f4939c
7 changed files with 150 additions and 73 deletions

View File

@ -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 (
<Panel header={null} toggleable collapsed={collapsed} onToggle={onToggle} className="w-full" pt={{
@ -36,6 +63,7 @@ const MessageInput = ({ collapsed, onToggle }) => {
icon="pi pi-send"
outlined
className='mt-2'
onClick={handleSubmit}
/>
</div>
</div>

View File

@ -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 (
<div className="bg-gray-900 h-full w-full min-bottom-bar:w-[87vw]">
<div className="mx-4 mt-4">
{communityNotes && communityNotes.length > 0 ? (
{communityNotes.length > 0 ? (
communityNotes.map(message => (
<Card
key={message.id}

View File

@ -154,7 +154,7 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
<Dropdown
options={contentOptions}
onChange={(e) => 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"

View File

@ -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 = () => {
<div onClick={() => 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' : ''}`}>
<i className="pi pi-plus pl-5" /> <p className="pl-2 rounded-md font-bold">Create</p>
</div>
<Accordion activeIndex={0}>
<AccordionTab pt={{
headerAction: ({ context }) => ({
className: `hover:bg-gray-700 rounded-lg ${isActive('/feed') ? 'bg-gray-700' : ''}`
})
}}
<Accordion activeIndex={0} className={styles['p-accordion']}>
<AccordionTab
pt={{
headerAction: ({ context }) => ({
className: `hover:bg-gray-700 rounded-lg ${isActive('/feed') ? 'bg-gray-700' : ''} ${styles['p-accordion-header-link']}`
}),
content: styles['p-accordion-content']
}}
header={"Community"}>
<div onClick={() => 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' : ''}`}>
<p className="pl-3 rounded-md font-bold"><i className="pi pi-hashtag text-sm"></i> global</p>

View File

@ -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;
}

View File

@ -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 };
}

View File

@ -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 (
<div className="bg-gray-900 h-[100vh] w-[100vw] min-bottom-bar:w-[87vw]">
<div className="w-[100vw] min-bottom-bar:w-[87vw] px-4 pt-4 flex flex-col items-start">
@ -86,7 +91,12 @@ const Feed = () => {
onClick={() => setIsMessageInputCollapsed(!isMessageInputCollapsed)}
/>
</div>
<MessageInput collapsed={isMessageInputCollapsed} onToggle={toggleMessageInput} />
<Divider />
<MessageInput
collapsed={isMessageInputCollapsed}
onToggle={toggleMessageInput}
onMessageSent={handleMessageSent}
/>
</div>
<div className="min-bottom-bar:hidden">
<CommunityMenuTab