From 43013bd464a7573e3261e69a61b473a5be5edbc2 Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Fri, 15 Aug 2025 12:55:34 -0400 Subject: [PATCH] feat(nostr): Completed profile feed TODOs --- .../app-body/nostr/profile/index.js | 2 +- .../app-body/nostr/profile/profile-read.js | 100 ++++++++++-------- .../app-body/nostr/profile/public-read.js | 41 +++++-- 3 files changed, 92 insertions(+), 51 deletions(-) diff --git a/src/components/app-body/nostr/profile/index.js b/src/components/app-body/nostr/profile/index.js index fa684ee..a16bfe1 100644 --- a/src/components/app-body/nostr/profile/index.js +++ b/src/components/app-body/nostr/profile/index.js @@ -17,7 +17,7 @@ function Profile (props) { return ( <> - + diff --git a/src/components/app-body/nostr/profile/profile-read.js b/src/components/app-body/nostr/profile/profile-read.js index 4c5bd8a..29a031c 100644 --- a/src/components/app-body/nostr/profile/profile-read.js +++ b/src/components/app-body/nostr/profile/profile-read.js @@ -1,13 +1,10 @@ /** * Component to read nostr information kind 0 (normal posts) * - * TODO: - * - Currently feeds are retrieved from only one relay. It should retrieve Kind - * 0 posts from all relays. It should then remove any duplicate entries. */ // Global npm libraries -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useCallback } from 'react' import { Container, Button } from 'react-bootstrap' import CopyOnClick from '../../bch-wallet/copy-on-click.js' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -21,47 +18,66 @@ import { RelayPool } from 'nostr' function ProfileRead (props) { const { npub } = props - const { setProfile } = props - const [post, setPost] = useState({}) + const { onProfileRead } = props + const [profile, setProfile] = useState({}) const [loaded, setLoaded] = useState(false) const [imageError, setImageError] = useState({ picture: false, banner: false }) + // Load profile from nostr relays + // It uses multiple relays. It will exit after the first successful retrieval + // from any relay. If one relay fails, it will move on to the next one. + const fetchProfile = useCallback(async (pubKey) => { + console.log('fetching profile for pubkey ', pubKey) + // Looking for the profile in each relay sequentially + for (let i = 0; i < config.nostrRelays.length; i++) { + const profile = await new Promise((resolve) => { + const relay = config.nostrRelays[i] + // const pool = RelayPool(config.nostrRelays) + const pool = RelayPool([relay]) + pool.on('open', relay => { + relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubKey] }) + }) + + pool.on('eose', relay => { + console.log('Closing Relay') + relay.close() + resolve(false) + }) + + pool.on('event', (relay, subId, ev) => { + try { + const profile = JSON.parse(ev.content) + // console.log('profile', profile) + console.log(`Profile found for ${pubKey} at ${relay.url}`) + resolve(profile) + } catch (error) { + resolve(false) + } + relay.close() + }) + }) + // Stop looking for profile if found + if (profile) { + return profile + } + } + }, []) + useEffect(() => { - const start = () => { + const start = async () => { const pubHexData = nip19.decode(npub) const pubHex = pubHexData.data - // const pool = RelayPool(config.nostrRelays) - const pool = new RelayPool([config.nostrRelay]) - pool.on('open', relay => { - relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubHex] }) - setLoaded(true) - }) - - pool.on('eose', relay => { - console.log('Closing Relay') - relay.close() - }) - - pool.on('event', (relay, subId, ev) => { - console.log('Received event:', ev) - try { - const profile = JSON.parse(ev.content) - console.log('Profile:', profile) - setPost(profile) - setProfile(profile) - } catch (error) { - console.error('Error parsing profile:', error) - } - }) - + const profile = await fetchProfile(pubHex) + onProfileRead(profile) + setProfile(profile) setLoaded(true) } if (!loaded && npub) { start() } - }, [loaded, setProfile, npub]) + }, [loaded, onProfileRead, npub, fetchProfile]) const handleImageError = (type) => { setImageError(prev => ({ ...prev, [type]: true })) @@ -74,10 +90,10 @@ function ProfileRead (props) { return ( {/* Banner Section */} - {post.banner && !imageError.banner && ( + {profile.banner && !imageError.banner && (
Profile banner {/* Profile Picture */}
- {post.picture && !imageError.picture + {profile.picture && !imageError.picture ? ( Profile -

{post.name || 'username'}

+

{profile.name || 'username'}

@@ -132,24 +148,24 @@ function ProfileRead (props) {
{/* About Section */} - {post.about && ( + {profile.about && (
- +
)} {/* Website Link */} - {post.website && ( + {profile.website && ( )} diff --git a/src/components/app-body/nostr/profile/public-read.js b/src/components/app-body/nostr/profile/public-read.js index 4f71d72..299d1df 100644 --- a/src/components/app-body/nostr/profile/public-read.js +++ b/src/components/app-body/nostr/profile/public-read.js @@ -2,7 +2,7 @@ * Component for read nostr information kind 1 */ // Global npm libraries -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useCallback } from 'react' import { Container, Card, Spinner } from 'react-bootstrap' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faUser } from '@fortawesome/free-solid-svg-icons' @@ -20,35 +20,60 @@ function PublicRead (props) { const [loaded, setLoaded] = useState(false) const [profilePictureError, setProfilePictureError] = useState(false) - useEffect(() => { - // Get Last post from a author - const start = () => { + const getUserFeeds = useCallback(async () => { + let feeds = await new Promise((resolve) => { + let list = [] + let closedRelays = 0 const pubHexData = nip19.decode(npub) const pubHex = pubHexData.data console.log('pubhex', pubHex) const pool = RelayPool(config.nostrRelays) + pool.on('open', relay => { relay.subscribe('subid', { limit: 5, kinds: [1], authors: [pubHex] }) - setLoaded(true) }) pool.on('eose', relay => { console.log('Closing Relay') - setLoaded(true) relay.close() + closedRelays++ + // Resolve list if all relays are closed + if (closedRelays === config.nostrRelays.length) { + resolve(list) + } }) pool.on('event', (relay, subId, ev) => { console.log('Received event:', ev) - setPosts(currentPosts => [...currentPosts, ev]) + list = [...list, ev] }) + }) + console.log('feeds', feeds) + // Remove duplicated feeds + feeds = feeds.filter((val, i, list) => { + const existingIndex = list.findIndex(value => value.id === val.id) + return existingIndex === i + }) + + // Sort from newest to oldest + feeds.sort((a, b) => b.created_at - a.created_at) + + setPosts(feeds) + }, [npub]) + + useEffect(() => { + // Get Last post from a author + const start = async () => { + setLoaded(true) + await getUserFeeds() + setLoaded(false) } if (!loaded && npub) { start() } - }, [bchWalletState, loaded, npub]) + }, [bchWalletState, loaded, npub, getUserFeeds]) const handleProfilePictureError = () => { setProfilePictureError(true)