mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
Nostr feed uses subscription hook now with ndk, can send nostr messages in community, some styling fixes
This commit is contained in:
parent
9b31e6cf18
commit
6db4f4939c
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
12
src/components/sidebar/sidebar.module.css
Normal file
12
src/components/sidebar/sidebar.module.css
Normal 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;
|
||||
}
|
@ -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 };
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user