@@ -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) => (
-
- );
-
- 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