mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-06 18:31:00 +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 { InputTextarea } from 'primereact/inputtextarea';
|
||||||
import { Button } from 'primereact/button';
|
import { Button } from 'primereact/button';
|
||||||
import { Panel } from 'primereact/panel';
|
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 [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 (
|
return (
|
||||||
<Panel header={null} toggleable collapsed={collapsed} onToggle={onToggle} className="w-full" pt={{
|
<Panel header={null} toggleable collapsed={collapsed} onToggle={onToggle} className="w-full" pt={{
|
||||||
@ -36,6 +63,7 @@ const MessageInput = ({ collapsed, onToggle }) => {
|
|||||||
icon="pi pi-send"
|
icon="pi pi-send"
|
||||||
outlined
|
outlined
|
||||||
className='mt-2'
|
className='mt-2'
|
||||||
|
onClick={handleSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,62 +4,51 @@ import { Avatar } from 'primereact/avatar';
|
|||||||
import { Tag } from 'primereact/tag';
|
import { Tag } from 'primereact/tag';
|
||||||
import { Button } from 'primereact/button';
|
import { Button } from 'primereact/button';
|
||||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||||
import { useCommunityNotes } from '@/hooks/nostr/useCommunityNotes';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { useNDKContext } from '@/context/NDKContext';
|
import { useNDKContext } from '@/context/NDKContext';
|
||||||
import { findKind0Fields } from '@/utils/nostr';
|
import { findKind0Fields } from '@/utils/nostr';
|
||||||
import NostrIcon from '../../../public/images/nostr.png';
|
import NostrIcon from '../../../public/images/nostr.png';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useImageProxy } from '@/hooks/useImageProxy';
|
import { useImageProxy } from '@/hooks/useImageProxy';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
|
import { useCommunityNotes } from '@/hooks/nostr/useCommunityNotes';
|
||||||
|
|
||||||
const NostrFeed = () => {
|
const NostrFeed = () => {
|
||||||
const router = useRouter();
|
const { communityNotes, isLoading, error } = useCommunityNotes();
|
||||||
const { communityNotes, error, isLoading } = useCommunityNotes();
|
const { ndk } = useNDKContext();
|
||||||
const { ndk, addSigner } = useNDKContext();
|
|
||||||
const { returnImageProxy } = useImageProxy();
|
const { returnImageProxy } = useImageProxy();
|
||||||
|
|
||||||
const [authorData, setAuthorData] = useState({});
|
const [authorData, setAuthorData] = useState({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAuthors = async () => {
|
communityNotes.forEach(note => {
|
||||||
const authorDataMap = {};
|
if (!authorData[note.pubkey]) {
|
||||||
for (const message of communityNotes) {
|
fetchAuthor(note.pubkey);
|
||||||
const author = await fetchAuthor(message.pubkey);
|
|
||||||
authorDataMap[message.pubkey] = author;
|
|
||||||
}
|
}
|
||||||
setAuthorData(authorDataMap);
|
});
|
||||||
};
|
}, [communityNotes, authorData]);
|
||||||
|
|
||||||
if (communityNotes && communityNotes.length > 0) {
|
|
||||||
fetchAuthors();
|
|
||||||
}
|
|
||||||
}, [communityNotes]);
|
|
||||||
|
|
||||||
const fetchAuthor = async (pubkey) => {
|
const fetchAuthor = async (pubkey) => {
|
||||||
try {
|
try {
|
||||||
await ndk.connect();
|
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
kinds: [0],
|
kinds: [0],
|
||||||
authors: [pubkey]
|
authors: [pubkey]
|
||||||
}
|
};
|
||||||
|
|
||||||
const author = await ndk.fetchEvent(filter);
|
const author = await ndk.fetchEvent(filter);
|
||||||
if (author) {
|
if (author) {
|
||||||
try {
|
try {
|
||||||
const fields = await findKind0Fields(JSON.parse(author.content));
|
const fields = await findKind0Fields(JSON.parse(author.content));
|
||||||
return fields;
|
setAuthorData(prevData => ({
|
||||||
} catch (error) {
|
...prevData,
|
||||||
console.error('Error fetching author:', error);
|
[pubkey]: fields
|
||||||
}
|
}));
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching author:', error);
|
console.error('Error fetching author:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching author:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderHeader = (message) => {
|
const renderHeader = (message) => {
|
||||||
const author = authorData[message.pubkey];
|
const author = authorData[message.pubkey];
|
||||||
@ -115,7 +104,7 @@ const NostrFeed = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-gray-900 h-full w-full min-bottom-bar:w-[87vw]">
|
<div className="bg-gray-900 h-full w-full min-bottom-bar:w-[87vw]">
|
||||||
<div className="mx-4 mt-4">
|
<div className="mx-4 mt-4">
|
||||||
{communityNotes && communityNotes.length > 0 ? (
|
{communityNotes.length > 0 ? (
|
||||||
communityNotes.map(message => (
|
communityNotes.map(message => (
|
||||||
<Card
|
<Card
|
||||||
key={message.id}
|
key={message.id}
|
||||||
|
@ -154,7 +154,7 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
options={contentOptions}
|
options={contentOptions}
|
||||||
onChange={(e) => handleContentSelect(e.value, index)}
|
onChange={(e) => handleContentSelect(e.value, index)}
|
||||||
placeholder={lesson.id ? lesson.title : "Create New Lesson"}
|
placeholder={lesson.id ? lesson.title : "Select Lesson"}
|
||||||
optionLabel="label"
|
optionLabel="label"
|
||||||
optionGroupLabel="label"
|
optionGroupLabel="label"
|
||||||
optionGroupChildren="items"
|
optionGroupChildren="items"
|
||||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { Accordion, AccordionTab } from 'primereact/accordion';
|
import { Accordion, AccordionTab } from 'primereact/accordion';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import 'primeicons/primeicons.css';
|
import 'primeicons/primeicons.css';
|
||||||
|
import styles from "./sidebar.module.css";
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -23,11 +24,13 @@ 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' : ''}`}>
|
<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>
|
<i className="pi pi-plus pl-5" /> <p className="pl-2 rounded-md font-bold">Create</p>
|
||||||
</div>
|
</div>
|
||||||
<Accordion activeIndex={0}>
|
<Accordion activeIndex={0} className={styles['p-accordion']}>
|
||||||
<AccordionTab pt={{
|
<AccordionTab
|
||||||
|
pt={{
|
||||||
headerAction: ({ context }) => ({
|
headerAction: ({ context }) => ({
|
||||||
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"}>
|
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' : ''}`}>
|
<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' : ''}`}>
|
||||||
|
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 { useNDKContext } from '@/context/NDKContext';
|
||||||
|
import { NDKSubscriptionCacheUsage } from "@nostr-dev-kit/ndk";
|
||||||
|
|
||||||
export function useCommunityNotes() {
|
export function useCommunityNotes() {
|
||||||
const [isClient, setIsClient] = useState(false);
|
const [communityNotes, setCommunityNotes] = useState([]);
|
||||||
const [communityNotes, setCommunityNotes] = useState();
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
// Add new state variables for loading and error
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const {ndk, addSigner} = useNDKContext();
|
const { ndk } = useNDKContext();
|
||||||
|
|
||||||
useEffect(() => {
|
const addNote = useCallback((noteEvent) => {
|
||||||
setIsClient(true);
|
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(() => {
|
||||||
setIsLoading(true);
|
let subscription;
|
||||||
setError(null);
|
const noteIds = new Set();
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
|
async function subscribeToNotes() {
|
||||||
|
if (!ndk) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ndk.connect();
|
await ndk.connect();
|
||||||
|
|
||||||
const filter = { kinds: [1], "#t": ["plebdevs"] };
|
const filter = {
|
||||||
const events = await ndk.fetchEvents(filter);
|
kinds: [1],
|
||||||
|
'#t': ['plebdevs']
|
||||||
if (events && events.size > 0) {
|
|
||||||
const eventsArray = Array.from(events);
|
|
||||||
setCommunityNotes(eventsArray);
|
|
||||||
setIsLoading(false);
|
|
||||||
return eventsArray;
|
|
||||||
}
|
|
||||||
setIsLoading(false);
|
|
||||||
return [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching community notes from NDK:', error);
|
|
||||||
setError(error);
|
|
||||||
setIsLoading(false);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
subscription = ndk.subscribe(filter, {
|
||||||
if (isClient) {
|
closeOnEose: false,
|
||||||
fetchCommunityNotesFromNDK().then(fetchedCommunityNotes => {
|
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST
|
||||||
if (fetchedCommunityNotes && fetchedCommunityNotes.length > 0) {
|
});
|
||||||
setCommunityNotes(fetchedCommunityNotes);
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}, [isClient]);
|
}
|
||||||
|
|
||||||
|
setCommunityNotes([]);
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
subscribeToNotes();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.stop();
|
||||||
|
}
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [ndk, addNote]);
|
||||||
|
|
||||||
return { communityNotes, isLoading, error };
|
return { communityNotes, isLoading, error };
|
||||||
}
|
}
|
@ -11,6 +11,7 @@ import MessageInput from '@/components/feeds/MessageInput';
|
|||||||
import StackerNewsIcon from '../../public/images/sn.svg';
|
import StackerNewsIcon from '../../public/images/sn.svg';
|
||||||
import NostrIcon from '../../public/images/nostr.png';
|
import NostrIcon from '../../public/images/nostr.png';
|
||||||
import { Button } from 'primereact/button';
|
import { Button } from 'primereact/button';
|
||||||
|
import { Divider } from 'primereact/divider';
|
||||||
|
|
||||||
const Feed = () => {
|
const Feed = () => {
|
||||||
const [selectedTopic, setSelectedTopic] = useState('global');
|
const [selectedTopic, setSelectedTopic] = useState('global');
|
||||||
@ -54,6 +55,10 @@ const Feed = () => {
|
|||||||
setIsMessageInputCollapsed(e.value);
|
setIsMessageInputCollapsed(e.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMessageSent = () => {
|
||||||
|
setIsMessageInputCollapsed(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-900 h-[100vh] w-[100vw] min-bottom-bar:w-[87vw]">
|
<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">
|
<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)}
|
onClick={() => setIsMessageInputCollapsed(!isMessageInputCollapsed)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<MessageInput collapsed={isMessageInputCollapsed} onToggle={toggleMessageInput} />
|
<Divider />
|
||||||
|
<MessageInput
|
||||||
|
collapsed={isMessageInputCollapsed}
|
||||||
|
onToggle={toggleMessageInput}
|
||||||
|
onMessageSent={handleMessageSent}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-bottom-bar:hidden">
|
<div className="min-bottom-bar:hidden">
|
||||||
<CommunityMenuTab
|
<CommunityMenuTab
|
||||||
|
Loading…
x
Reference in New Issue
Block a user