diff --git a/src/components/app-body/nostr/feeds/feed-card.js b/src/components/app-body/nostr/feeds/feed-card.js index 33e339a..46fb8d2 100644 --- a/src/components/app-body/nostr/feeds/feed-card.js +++ b/src/components/app-body/nostr/feeds/feed-card.js @@ -1,6 +1,7 @@ /** * Component for displaying nostr posts */ + // Global npm libraries import React, { useCallback, useEffect, useState } from 'react' import { Card, Spinner } from 'react-bootstrap' @@ -8,12 +9,15 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faUser, faHeart as faHeartSolid } from '@fortawesome/free-solid-svg-icons' import { faHeart } from '@fortawesome/free-regular-svg-icons' import * as nip19 from 'nostr-tools/nip19' -import NostrFormat from '../nostr-format' import { finalizeEvent } from 'nostr-tools/pure' import { Relay } from 'nostr-tools/relay' import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency import { RelayPool } from 'nostr' +// Local libraries +import CopyOnClick from '../../bch-wallet/copy-on-click.js' +import NostrFormat from '../nostr-format' + function FeedCard (props) { const { post, appData, profiles } = props const { nostrKeyPair } = appData.bchWalletState @@ -21,7 +25,6 @@ function FeedCard (props) { const [profile, setProfile] = useState(profiles[post.pubkey]) const [npub, setNpub] = useState('') - const [isClicked, setIsClicked] = useState(false) const [isLiked, setIsLiked] = useState(false) const [likesCount, setLikesCount] = useState(null) const [likesFetched, setLikesFetched] = useState(false) @@ -104,13 +107,6 @@ function FeedCard (props) { } }, [profiles, post]) - // copy to clipboard - const copyToClipboard = useCallback((value) => { - setIsClicked(true) - appData.appUtil.copyToClipboard(value) - setTimeout(() => setIsClicked(false), 200) - }, [appData]) - // get short npub const getShortNpub = useCallback((npub) => { return npub.slice(0, 8) + '...' + npub.slice(-5) @@ -168,6 +164,11 @@ function FeedCard (props) { } } + const goToProfile = () => { + const profileUrl = `${window.location.origin}/profile/${npub}#single-view` + window.open(profileUrl, '_blank') + } + return ( @@ -186,20 +187,21 @@ function FeedCard (props) { src={profile.picture} alt='Profile' className='rounded-circle w-100 h-100' - style={{ objectFit: 'cover' }} + style={{ objectFit: 'cover', cursor: 'pointer' }} onError={handleProfilePictureError} onLoad={handleProfilePictureLoad} + onClick={goToProfile} /> ) : ( - + )}
- {profile && profile.name && {profile.name}} + {profile && profile.name && {profile.name}} {!profile?.name && ( - + {post.pubkey.slice(0, 8) + '...'} {!profile?.loaded && } @@ -211,14 +213,20 @@ function FeedCard (props) { title='Copy to clipboard' style={{ cursor: 'pointer', - transform: isClicked ? 'scale(0.95)' : 'scale(1)', transition: 'transform 0.1s ease', - display: 'inline-block' + display: 'inline-block', + marginRight: '5px' }} - onClick={() => copyToClipboard(npub)} + onClick={goToProfile} + > {getShortNpub(npub)} + )}
diff --git a/src/components/app-body/nostr/feeds/index.js b/src/components/app-body/nostr/feeds/index.js index 0c8e022..e3b22b5 100644 --- a/src/components/app-body/nostr/feeds/index.js +++ b/src/components/app-body/nostr/feeds/index.js @@ -1,16 +1,5 @@ /* Component for reading the nostr feeds. - - TODO: - - fetchProfile() should retrieve a profile from multiple relays. If the first relay returns a profile, - then that profile can be used and promise resolved. If the first relay returns no profile, the next - one should be tried until all relays are exhausted or one returns a profile. - - - useEffect() retrieves the feeds. This should cycle through each relay and posts from each one. - Once each relays has been tried, the posts should remove duplicate entries. Finally posts should - be sorted by date. - - - Clicking on a profile picture, name, or npub should open the profile for that user in a new tab. */ // Global npm libraries @@ -27,91 +16,121 @@ function Feeds (props) { const { appData } = props const [activeTab, setActiveTab] = useState(appData.lastFeedTab) - const { bchWalletState } = appData const [posts, setPosts] = useState([]) const [loaded, setLoaded] = useState(false) const [profiles, setProfiles] = useState({}) - // function to fetch profile and set it to profiles state - const fetchProfile = useCallback(async (pubkey) => { - // no fetch profile again if it exist - let hasProfileRequest = false - setProfiles(currentProfiles => { - if (currentProfiles[pubkey]) { - hasProfileRequest = true - return currentProfiles - } else { - const newProfiles = { ...currentProfiles } - newProfiles[pubkey] = { loaded: false } - return newProfiles - } - }) - if (hasProfileRequest) return + // 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) => { + // 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] }) + }) - // const pool = RelayPool(config.nostrRelays) - const pool = RelayPool([config.nostrRelay]) - 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('eose', relay => { - relay.close() - try { - // Mark unknown profiles. From this way we can know which profile was fetched and not found. - setProfiles(currentProfiles => { - if (!currentProfiles[pubkey]) { - const newProfiles = { ...currentProfiles } - newProfiles[pubkey] = { loaded: true } - return newProfiles + 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) } - return currentProfiles + relay.close() }) - } catch (error) { - console.warn(error) - // skip error + }) + // Stop looking for profile if found + if (profile) { + return profile } - }) - - pool.on('event', (relay, subId, ev) => { - try { - // update profiles data - const profile = JSON.parse(ev.content) - setProfiles(currentProfiles => { - const newProfiles = { ...currentProfiles } - newProfiles[pubkey] = profile - return newProfiles - }) - } catch (error) { - // skip error - } - }) + } }, []) - // Get global feed posts - useEffect(() => { - const start = () => { - // const pool = RelayPool(config.nostrRelays) - const pool = RelayPool([config.nostrRelay]) + // Get array of feeds from relays + const fetchFeeds = useCallback(async () => { + let feeds = await new Promise((resolve, reject) => { + let list = [] + let closedRelays = 0 + + const pool = RelayPool(config.nostrRelays) + // const pool = RelayPool([config.nostrRelay]) pool.on('open', relay => { relay.subscribe('REQ', { limit: 10, kinds: [1], '#t': ['slpdex-socialmedia'] }) }) pool.on('eose', relay => { - setLoaded(true) relay.close() + closedRelays++ + // Resolve list if all relays are closed + if (closedRelays === config.nostrRelays.length) { + resolve(list) + } }) - pool.on('event', async (relay, subId, ev) => { - setPosts(currentPosts => [...currentPosts, ev]) - // fetch post profile and set it to profiles state - fetchProfile(ev.pubkey) + pool.on('event', (relay, subId, ev) => { + // console.log('post retrieved from ', relay.url, ev.sig) + list = [...list, ev] }) + }) + + // 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) + + // console.log('feeds', feeds) + + setPosts(feeds) + return feeds + }, []) + + // Load data on component mount. + useEffect(() => { + const loadData = async () => { + // Get feeds + const feeds = await fetchFeeds() + setLoaded(true) + + const loadedProfiles = [] // fetched profiles ( this will be used for prevent load the same profile multiple times.) + // Map feeds and get feed owner profile. + for (let i = 0; i < feeds.length; i++) { + const pubKey = feeds[i].pubkey + + const exist = loadedProfiles.find((val) => { return val === pubKey }) + if (exist) { continue } + // Fech profile. + const profile = await fetchProfile(pubKey) + loadedProfiles.push(pubKey) // mark as loaded + + // Update profile state + setProfiles(currentProfiles => { + const newProfiles = { ...currentProfiles } + newProfiles[pubKey] = profile + return newProfiles + }) + } } - if (!loaded) { - start() + loadData() } - }, [bchWalletState, loaded, fetchProfile]) + }, [loaded, fetchFeeds, fetchProfile]) const onChangeTab = (tab) => { setActiveTab(tab)