From 20515139274d5123c830b9f2565816d3b58ca22d Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Mon, 2 Sep 2024 18:23:40 -0500 Subject: [PATCH] Basic nostr feed works, global feed is updated and works great --- src/components/BottomBar.js | 6 +- .../feeds/DiscordFeed.js} | 0 .../feeds/GlobalFeed.js} | 97 ++++++++++--- src/components/feeds/NostrFeed.js | 137 ++++++++++++++++++ .../feeds/StackerNewsFeed.js} | 0 src/hooks/nostr/useCommunityNotes.js | 52 +++++++ src/pages/{feed/index.js => feed.js} | 12 +- src/pages/feed/nostr.js | 79 ---------- 8 files changed, 278 insertions(+), 105 deletions(-) rename src/{pages/feed/discord.js => components/feeds/DiscordFeed.js} (100%) rename src/{pages/feed/global.js => components/feeds/GlobalFeed.js} (54%) create mode 100644 src/components/feeds/NostrFeed.js rename src/{pages/feed/stackernews.js => components/feeds/StackerNewsFeed.js} (100%) create mode 100644 src/hooks/nostr/useCommunityNotes.js rename src/pages/{feed/index.js => feed.js} (91%) delete mode 100644 src/pages/feed/nostr.js diff --git a/src/components/BottomBar.js b/src/components/BottomBar.js index 05c9d2a..4dbbebd 100644 --- a/src/components/BottomBar.js +++ b/src/components/BottomBar.js @@ -11,13 +11,13 @@ const BottomBar = () => { return (
-
router.push('/')} className={`cursor-pointer p-2 rounded-lg ${isActive('/') ? 'bg-gray-700' : ''}`}> +
router.push('/')} className={`cursor-pointer px-4 py-3 rounded-lg ${isActive('/') ? 'bg-gray-700' : ''}`}>
-
router.push('/content')} className={`cursor-pointer p-2 rounded-lg ${isActive('/content') ? 'bg-gray-700' : ''}`}> +
router.push('/content')} className={`cursor-pointer px-4 py-3 rounded-lg ${isActive('/content') ? 'bg-gray-700' : ''}`}>
-
router.push('/feed?channel=global')} className={`cursor-pointer p-2 rounded-lg ${isActive('/feed?channel=global') ? 'bg-gray-700' : ''}`}> +
router.push('/feed?channel=global')} className={`cursor-pointer px-4 py-3 rounded-lg ${isActive('/feed') ? 'bg-gray-700' : ''}`}>
diff --git a/src/pages/feed/discord.js b/src/components/feeds/DiscordFeed.js similarity index 100% rename from src/pages/feed/discord.js rename to src/components/feeds/DiscordFeed.js diff --git a/src/pages/feed/global.js b/src/components/feeds/GlobalFeed.js similarity index 54% rename from src/pages/feed/global.js rename to src/components/feeds/GlobalFeed.js index 8f046dc..8a140f7 100644 --- a/src/pages/feed/global.js +++ b/src/components/feeds/GlobalFeed.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Card } from 'primereact/card'; import { Avatar } from 'primereact/avatar'; import { Tag } from 'primereact/tag'; @@ -8,6 +8,13 @@ import { useDiscordQuery } from '@/hooks/communityQueries/useDiscordQuery'; import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; import { useRouter } from 'next/router'; +import { useCommunityNotes } from '@/hooks/nostr/useCommunityNotes'; +import { useNDKContext } from '@/context/NDKContext'; +import { findKind0Fields } from '@/utils/nostr'; +import NostrIcon from '../../../public/nostr.png'; +import Image from 'next/image'; +import { useImageProxy } from '@/hooks/useImageProxy'; +import { nip19 } from 'nostr-tools'; const StackerNewsIconComponent = () => ( @@ -25,8 +32,32 @@ const GlobalFeed = () => { const router = useRouter(); const { data: discordData, error: discordError, isLoading: discordLoading } = useDiscordQuery({page: router.query.page}); const { data: stackerNewsData, error: stackerNewsError, isLoading: stackerNewsLoading } = useQuery({queryKey: ['stackerNews'], queryFn: fetchStackerNews}); + const { communityNotes: nostrData, error: nostrError, isLoading: nostrLoading } = useCommunityNotes(); + const { ndk } = useNDKContext(); + const { returnImageProxy } = useImageProxy(); - if (discordLoading || stackerNewsLoading) { + const [authorData, setAuthorData] = useState({}); + + useEffect(() => { + const fetchAuthors = async () => { + const authorDataMap = {}; + for (const message of nostrData) { + const author = await fetchAuthor(message.pubkey); + authorDataMap[message.pubkey] = author; + } + setAuthorData(authorDataMap); + }; + + if (nostrData && nostrData.length > 0) { + fetchAuthors(); + } + }, [nostrData]); + + const fetchAuthor = async (pubkey) => { + // ... (keep the existing fetchAuthor function) + } + + if (discordLoading || stackerNewsLoading || nostrLoading) { return (
@@ -34,34 +65,57 @@ const GlobalFeed = () => { ); } - if (discordError || stackerNewsError) { + if (discordError || stackerNewsError || nostrError) { return
Failed to load feed. Please try again later.
; } const combinedFeed = [ ...(discordData || []).map(item => ({ ...item, type: 'discord' })), - ...(stackerNewsData || []).map(item => ({ ...item, type: 'stackernews' })) - ].sort((a, b) => new Date(b.timestamp || b.createdAt) - new Date(a.timestamp || a.createdAt)); + ...(stackerNewsData || []).map(item => ({ ...item, type: 'stackernews' })), + ...(nostrData || []).map(item => ({ ...item, type: 'nostr' })) + ].sort((a, b) => { + const dateA = a.type === 'nostr' ? a.created_at * 1000 : new Date(a.timestamp || a.createdAt); + const dateB = b.type === 'nostr' ? b.created_at * 1000 : new Date(b.timestamp || b.createdAt); + return dateB - dateA; + }); const header = (item) => (
- -

{item.type === 'discord' ? item.author : item.user.name}

+ +

+ {item.type === 'discord' ? item.author : + item.type === 'stackernews' ? item.user.name : + authorData[item.pubkey]?.username || item.pubkey.substring(0, 12) + '...'} +

- {item.type === 'discord' ? ( + {item.type === 'discord' && ( <> - ) : ( + )} + {item.type === 'stackernews' && ( <> } value="stackernews" className="w-fit bg-gray-600 text-[#f8f8ff] max-sidebar:mt-1" /> )} + {item.type === 'nostr' && ( + <> + + } value="nostr" className="w-fit text-[#f8f8ff] bg-blue-400 max-sidebar:mt-1" /> + + )}
@@ -70,18 +124,27 @@ const GlobalFeed = () => { const footer = (item) => (
- {new Date(item.timestamp || item.createdAt).toLocaleString()} + {item.type === 'nostr' + ? new Date(item.created_at * 1000).toLocaleString() + : new Date(item.timestamp || item.createdAt).toLocaleString()}
); @@ -97,8 +160,8 @@ const GlobalFeed = () => { footer={() => footer(item)} className="w-full bg-gray-700 shadow-lg hover:shadow-xl transition-shadow duration-300 mb-4" > - {item.type === 'discord' ? ( -

{item.content}

+ {item.type === 'discord' || item.type === 'nostr' ? ( +

{item.content}

) : ( <>

{item.title}

diff --git a/src/components/feeds/NostrFeed.js b/src/components/feeds/NostrFeed.js new file mode 100644 index 0000000..95ec6ae --- /dev/null +++ b/src/components/feeds/NostrFeed.js @@ -0,0 +1,137 @@ +import React, { useState, useEffect } from 'react'; +import { Card } from 'primereact/card'; +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/nostr.png'; +import Image from 'next/image'; +import { useImageProxy } from '@/hooks/useImageProxy'; +import { nip19 } from 'nostr-tools'; + +const NostrFeed = () => { + const router = useRouter(); + const { communityNotes, error, isLoading } = useCommunityNotes(); + const { ndk, addSigner } = 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; + } + setAuthorData(authorDataMap); + }; + + if (communityNotes && communityNotes.length > 0) { + fetchAuthors(); + } + }, [communityNotes]); + + 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; + } 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]; + + if (!author || Object.keys(author).length === 0 || !author.username || !author.avatar) { + return null; + } + + return ( +
+
+ +

{author?.username || author?.pubkey.substring(0, 12) + '...'}

+
+
+
+ + } value="nostr" className="w-fit text-[#f8f8ff] bg-blue-400 max-sidebar:mt-1" /> +
+
+
+ ); + } + + const footer = (message) => ( +
+ + {new Date(message.created_at * 1000).toLocaleString()} + +
+ ); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return
Failed to load messages. Please try again later.
; + } + + return ( +
+
+ {communityNotes && communityNotes.length > 0 ? ( + communityNotes.map(message => ( + footer(message)} + className="w-full bg-gray-700 shadow-lg hover:shadow-xl transition-shadow duration-300 mb-4" + > +

{message.content}

+
+ )) + ) : ( +
No messages available.
+ )} +
+
+ ); +}; + +export default NostrFeed; \ No newline at end of file diff --git a/src/pages/feed/stackernews.js b/src/components/feeds/StackerNewsFeed.js similarity index 100% rename from src/pages/feed/stackernews.js rename to src/components/feeds/StackerNewsFeed.js diff --git a/src/hooks/nostr/useCommunityNotes.js b/src/hooks/nostr/useCommunityNotes.js new file mode 100644 index 0000000..8062901 --- /dev/null +++ b/src/hooks/nostr/useCommunityNotes.js @@ -0,0 +1,52 @@ +import { useState, useEffect } from 'react'; +import { useNDKContext } from '@/context/NDKContext'; + +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 [error, setError] = useState(null); + const {ndk, addSigner} = useNDKContext(); + + useEffect(() => { + setIsClient(true); + }, []); + + const fetchCommunityNotesFromNDK = async () => { + setIsLoading(true); + setError(null); + try { + await ndk.connect(); + + 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; + } + 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]); + + return { communityNotes, isLoading, error }; +} \ No newline at end of file diff --git a/src/pages/feed/index.js b/src/pages/feed.js similarity index 91% rename from src/pages/feed/index.js rename to src/pages/feed.js index 32c4d88..8257c5d 100644 --- a/src/pages/feed/index.js +++ b/src/pages/feed.js @@ -2,15 +2,15 @@ import React, { useState, useEffect } from 'react'; import Image from 'next/image'; import { InputText } from 'primereact/inputtext'; import CommunityMenuTab from '@/components/menutab/CommunityMenuTab'; -import NostrFeed from './nostr'; -import DiscordFeed from './discord'; -import StackerNewsFeed from './stackernews'; -import GlobalFeed from './global'; +import NostrFeed from '@/components/feeds/NostrFeed'; +import DiscordFeed from '@/components/feeds/DiscordFeed'; +import StackerNewsFeed from '@/components/feeds/StackerNewsFeed'; +import GlobalFeed from '@/components/feeds/GlobalFeed'; import { useRouter } from 'next/router'; import { Message } from 'primereact/message'; import { Tag } from 'primereact/tag'; -import StackerNewsIcon from '../../../public/sn.svg'; -import NostrIcon from '../../../public/nostr.png'; +import StackerNewsIcon from '../../public/sn.svg'; +import NostrIcon from '../../public/nostr.png'; const Feed = () => { const [selectedTopic, setSelectedTopic] = useState('global'); diff --git a/src/pages/feed/nostr.js b/src/pages/feed/nostr.js deleted file mode 100644 index fbab8e4..0000000 --- a/src/pages/feed/nostr.js +++ /dev/null @@ -1,79 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Card } from 'primereact/card'; -import { Avatar } from 'primereact/avatar'; -import { Tag } from 'primereact/tag'; -import { Button } from 'primereact/button'; -import { ProgressSpinner } from 'primereact/progressspinner'; -import { useDiscordQuery } from '@/hooks/communityQueries/useDiscordQuery'; -import { useRouter } from 'next/router'; - -const NostrFeed = () => { - const router = useRouter(); - const { data, error, isLoading } = useDiscordQuery({page: router.query.page}); - - if (isLoading) { - return ( -
- -
- ); - } - - if (error) { - return
Failed to load messages. Please try again later.
; - } - - const header = (message) => ( -
-
- -

{message.author}

-
-
-
- - -
-
-
- ); - - const footer = (message) => ( -
- - {new Date(message.timestamp).toLocaleString()} - -
- ); - - return ( -
-
- {data && data.length > 0 ? ( - data.map(message => ( - header(message)} - footer={() => footer(message)} - className="w-full bg-gray-700 shadow-lg hover:shadow-xl transition-shadow duration-300 mb-4" - > -

{message.content}

-
- )) - ) : ( -
No messages available.
- )} -
-
- ); -}; - -export default NostrFeed; \ No newline at end of file