diff --git a/src/components/app-body/configuration/index.js b/src/components/app-body/configuration/index.js index 47fcaaf..9275420 100644 --- a/src/components/app-body/configuration/index.js +++ b/src/components/app-body/configuration/index.js @@ -6,6 +6,7 @@ // Global npm libraries import React from 'react' import ServerSelectView from './select-server-view' +import RelaySelectionView from './relay-selection-view' function ConfigurationView (props) { const { appData } = props @@ -13,6 +14,7 @@ function ConfigurationView (props) { return ( <> + ) } diff --git a/src/components/app-body/configuration/relay-selection-view.js b/src/components/app-body/configuration/relay-selection-view.js new file mode 100644 index 0000000..65a0f86 --- /dev/null +++ b/src/components/app-body/configuration/relay-selection-view.js @@ -0,0 +1,333 @@ +/* + This component is a View that allows the user to manage Nostr relay settings. +*/ + +// Global npm libraries +import React, { useState } from 'react' +import { Row, Col, Form, Card, Button } from 'react-bootstrap' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faShareAlt, faTrashAlt } from '@fortawesome/free-solid-svg-icons' +function RelaySelectionView (props) { + const { appData } = props + const { relaysData, updateRelaysData, restoreRelaysData } = appData + console.log('relaysData', relaysData) + + const [newRelayAddress, setNewRelayAddress] = useState('') + + const addRelay = (newRelay) => { + const exist = relaysData.find(relay => relay.address === newRelay) + if (!exist) { + relaysData.push({ address: newRelay, read: true, write: true }) + updateRelaysData(relaysData) + } + } + // Delete relay from relaysData array + const deleteRelay = (relayToDelete) => { + const index = relaysData.findIndex(relay => relay.address === relayToDelete.address) + if (index !== -1) { + relaysData.splice(index, 1) + updateRelaysData(relaysData) + } + } + const handleReadToggle = (relayToToggleRead) => { + const index = relaysData.findIndex(relay => relay.address === relayToToggleRead.address) + if (index !== -1) { + relaysData[index].read = !relaysData[index].read + updateRelaysData(relaysData) + } + } + const handleWriteToggle = (relayToToggleWrite) => { + const index = relaysData.findIndex(relay => relay.address === relayToToggleWrite.address) + if (index !== -1) { + relaysData[index].write = !relaysData[index].write + updateRelaysData(relaysData) + } + } + return ( + <> + + + + +

Nostr Relay Configuration

+

+ Manage your Nostr relay connections. Configure read and write permissions for each relay. +

+ +
+
+ + + + + + + + + +
+
+
Relay Connections
+ + {relaysData.length} relays + +
+ +
+ {relaysData.map((relay, index) => ( +
+
+ {/* Relay Address */} +
+
+ {relay.address} +
+
+ + {/* Controls */} +
+ {/* Read/Write Toggles */} +
+
+ + Read + + handleReadToggle(relay)} + checked={relay.read} + className='mb-0' + style={{ margin: 0 }} + /> +
+
+ + Write + + handleWriteToggle(relay)} + checked={relay.write} + className='mb-0' + style={{ margin: 0 }} + /> +
+
+ + {/* Action Buttons */} +
+ + +
+
+
+
+ ))} +
+
+ +
+ + + +
+
+
+ Add New Relay +
+

+ Enter the URL of a Nostr relay to add it to your configuration +

+
+ +
+
+ + Relay Address + + setNewRelayAddress(e.target.value)} + style={{ + fontFamily: 'monospace', + fontSize: '0.9em', + border: '1px solid #dee2e6', + borderRadius: '6px', + padding: '10px 12px' + }} + /> +
+ +
+
+ +
+
+
+ + ) +} + +export default RelaySelectionView diff --git a/src/components/app-body/index.js b/src/components/app-body/index.js index 303fe2a..bf4d6bb 100644 --- a/src/components/app-body/index.js +++ b/src/components/app-body/index.js @@ -15,14 +15,14 @@ import GetBalance from './balance' import Wallet from './bch-wallet' import Placeholder2 from './placeholder2' import Placeholder3 from './placeholder3' -// import ServerSelectView from './servers/select-server-view' +import ServerSelectView from './configuration/select-server-view.js' // import SelectServerButton from './servers/select-server-button' import NftsForSale from './nfts-for-sale' import BchSend from './bch-send' import SlpTokens from './slp-tokens' import SweepWif from './sweep/index.js' import SignMessage from './sign/index.js' -import ServerSelectView from './configuration/select-server-view' +import ConfigurationView from './configuration/index.js' import NostrPost from './nostr/nostr-post/index.js' import Profile from './nostr/profile/index.js' import Feeds from './nostr/feeds/index.js' @@ -47,7 +47,7 @@ function AppBody (props) { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/components/app-body/nostr/content-creators/content-creators-list.js b/src/components/app-body/nostr/content-creators/content-creators-list.js index cfe9cfd..057935e 100644 --- a/src/components/app-body/nostr/content-creators/content-creators-list.js +++ b/src/components/app-body/nostr/content-creators/content-creators-list.js @@ -43,6 +43,7 @@ function ContentCreators (props) { useEffect(() => { const loadCreators = async () => { try { + console.log('loadCreators()') const creatorsRes = await axios.get(`${SERVER}/sm/list/all/0`) const creators = creatorsRes.data // console.log('creators', creators) @@ -52,8 +53,9 @@ function ContentCreators (props) { for (let i = 0; i < creators.length; i++) { try { const creator = creators[i] - const profile = await appData.nostrQueries.getProfile(creator.pubkey) - + const profileRes = await appData.nostrQueries.getProfile(creator.pubkey) + let profile = profileRes + if (!profileRes) profile = {} // the folowing lines should re-render the ContentCard setCreators(prevCreators => { const updatedCreators = [...prevCreators] @@ -73,7 +75,7 @@ function ContentCreators (props) { loadCreators() getFollowList() } - }, [loaded, followList, getFollowList, appData]) + }, [loaded, getFollowList, appData]) const filteredCreators = useCallback(() => { if (!creators || creators.length === 0) { diff --git a/src/components/app-body/nostr/feeds/feed-card.js b/src/components/app-body/nostr/feeds/feed-card.js index d490806..438d274 100644 --- a/src/components/app-body/nostr/feeds/feed-card.js +++ b/src/components/app-body/nostr/feeds/feed-card.js @@ -12,7 +12,6 @@ import * as nip19 from 'nostr-tools/nip19' import { finalizeEvent } from 'nostr-tools/pure' import { Relay } from 'nostr-tools/relay' import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency -import config from '../../../../config/index.js' // Local libraries import CopyOnClick from '../../bch-wallet/copy-on-click.js' @@ -20,7 +19,7 @@ import NostrFormat from '../nostr-format' function FeedCard (props) { const { post, appData, profiles } = props - const { nostrKeyPair } = appData.bchWalletState + const { nostrKeyPair, writeRelays } = appData.bchWalletState const [profile, setProfile] = useState(profiles[post.pubkey]) @@ -104,7 +103,7 @@ function FeedCard (props) { } console.log(`eventTemplate: ${JSON.stringify(eventTemplate, null, 2)}`) - config.nostrRelays.map(async (relayUrl) => { + writeRelays.map(async (relayUrl) => { try { // Sign the post const signedEvent = finalizeEvent(eventTemplate, privateKeyBin) diff --git a/src/components/app-body/nostr/feeds/following.js b/src/components/app-body/nostr/feeds/following.js index 621e65a..00f0f32 100644 --- a/src/components/app-body/nostr/feeds/following.js +++ b/src/components/app-body/nostr/feeds/following.js @@ -4,41 +4,21 @@ // Global npm libraries import React, { useEffect, useState } from 'react' import { Container, Spinner } from 'react-bootstrap' -import { RelayPool } from 'nostr' // Local libraries -import config from '../../../../config' import FeedCard from './feed-card' function Following (props) { const { appData, posts, profiles } = props + const { nostrQueries } = appData const [followingPosts, setFollowingPosts] = useState([]) const [loaded, setLoaded] = useState(false) useEffect(() => { const getFollowingPosts = async () => { - const followingList = await new Promise((resolve, reject) => { - let list = [] - const { nostrKeyPair } = appData.bchWalletState - - const pool = RelayPool(config.nostrRelays) - pool.on('open', relay => { - relay.subscribe('subid', { limit: 1, kinds: [3], authors: [nostrKeyPair.pubHex] }) - }) - - pool.on('eose', relay => { - relay.close() - /** This ensures to return an empty array if no records are found! - * Applies for new users that don't have a follow list - */ - resolve(list) - }) - - pool.on('event', (relay, subId, ev) => { - list = ev.tags - resolve(list) - }) - }) + console.log('getFollowingPosts') + const { nostrKeyPair } = appData.bchWalletState + const followingList = await nostrQueries.getFollowList(nostrKeyPair.pubHex) // Filter posts by following list let filteredPosts = [] @@ -55,7 +35,7 @@ function Following (props) { setLoaded(true) } getFollowingPosts() - }, [appData, posts]) + }, [appData, posts, nostrQueries]) return ( diff --git a/src/components/app-body/nostr/nostr-post/profile-post.js b/src/components/app-body/nostr/nostr-post/profile-post.js index 85749a8..4bf4e62 100644 --- a/src/components/app-body/nostr/nostr-post/profile-post.js +++ b/src/components/app-body/nostr/nostr-post/profile-post.js @@ -11,11 +11,10 @@ import { Relay } from 'nostr-tools/relay' import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency // Local libraries -import config from '../../../../config' function ProfilePost (props) { const { appData } = props - const { bchWalletState } = appData + const { bchWalletState, writeRelays } = appData const [accordionKey, setAccordionKey] = useState(null) const [onFetch, setOnFetch] = useState(false) const [formLoaded, setFormLoaded] = useState(false) @@ -90,7 +89,7 @@ function ProfilePost (props) { console.log('signedEvent: ', signedEvent) // Publish the post to each relay. - config.nostrRelays.map(async (relayUrl) => { + writeRelays.map(async (relayUrl) => { try { // Connect to a relay. const relay = await Relay.connect(relayUrl) diff --git a/src/components/app-body/nostr/nostr-post/public-post.js b/src/components/app-body/nostr/nostr-post/public-post.js index 58f10f7..f345021 100644 --- a/src/components/app-body/nostr/nostr-post/public-post.js +++ b/src/components/app-body/nostr/nostr-post/public-post.js @@ -11,12 +11,11 @@ import { Relay } from 'nostr-tools/relay' import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency // Local libraries -import config from '../../../../config' function PublicPost (props) { const [accordionKey, setAccordionKey] = useState('0') const [onFetch, setOnFetch] = useState(false) - const { bchWalletState } = props.appData + const { bchWalletState, writeRelays } = props.appData const [formData, setFormData] = useState({ content: '' }) @@ -64,7 +63,7 @@ function PublicPost (props) { console.log('signedEvent: ', signedEvent) // Publish the post to each relay. - config.nostrRelays.map(async (relayUrl) => { + writeRelays.map(async (relayUrl) => { try { // Connect to a relay. const relay = await Relay.connect(relayUrl) diff --git a/src/hooks/state.js b/src/hooks/state.js index 1ef34a2..8792360 100644 --- a/src/hooks/state.js +++ b/src/hooks/state.js @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useRef, useState } from 'react' // import { useQueryParam, StringParam } from 'use-query-params' import useLocalStorageState from 'use-local-storage-state' import AppUtil from '../util' @@ -8,19 +8,24 @@ import { useLocation } from 'react-router-dom' function useAppState () { const location = useLocation() - + // Default local storage object + const localStorageDefault = { + serverUrl: 'https://free-bch.fullstack.cash', // Default server + relays: [ + { address: 'wss://nostr-relay.psfoundation.info', read: true, write: true }, + { address: 'wss://relay.damus.io', read: true, write: true } + ], + nftData: {}, + lastFeedTab: 'feed' + } // Load Local storage Data const [lsState, setLSState, { removeItem }] = useLocalStorageState('bchWalletState-template', { ssr: true, - defaultValue: { - serverUrl: 'https://free-bch.fullstack.cash' // Default server - }, - nftData: {}, - lastFeedTab: 'feed' + defaultValue: localStorageDefault + }) console.log('lsState: ', lsState) - // Initialize data states const [serverUrl, setServerUrl] = useState(lsState.serverUrl) // Default server url const [menuState, setMenuState] = useState(0) @@ -28,7 +33,7 @@ function useAppState () { const [servers, setServers] = useState([]) const [dexLib, setDexLib] = useState(false) const [nostr, setNostr] = useState(false) - const [lastFeedTab, setLastFeedTab] = useState(lsState.lastFeedTab || 'feed') + const [lastFeedTab, setLastFeedTab] = useState(lsState.lastFeedTab || localStorageDefault.lastFeedTab) // Startup state management const [asyncInitStarted, setAsyncInitStarted] = useState(false) @@ -45,6 +50,14 @@ function useAppState () { // NFTs for sale stored data to improve performance const [nftForSaleCacheData, setNftForSaleCacheData] = useState(lsState.nftData || {}) + // Relays + const [relaysData, setRelaysData] = useState(lsState.relays || localStorageDefault.relays) // All relays data + const [readRelays, setReadRelays] = useState(relaysData.filter(relay => relay.read).map(relay => relay.address)) // Read relays + const [writeRelays, setWriteRelays] = useState(relaysData.filter(relay => relay.write).map(relay => relay.address)) // Write relays + + // Nostr queries service + const nostrQueriesRef = useRef(new NostrQueries({ relays: readRelays })) + // The wallet state makes this a true progressive web app (PWA). As // balances, UTXOs, and tokens are retrieved, this state is updated. // properties are enumerated here for the purpose of documentation. @@ -97,7 +110,7 @@ function useAppState () { return newBchWalletState }) - // console.log(`New wallet state: ${JSON.stringify(bchWalletState, null, 2)}`) + // console.log(`New wallet state: ${JSON.stringify(bchWalletState, null, 2)}`) } catch (err) { console.error('Error in App.js updateBchWalletState()') throw err @@ -114,6 +127,35 @@ function useAppState () { setNftForSaleCacheData(allCacheData) // Update the state updateLocalStorage({ nftData: allCacheData }) // Update the local storage } + // Update relays data + function updateRelaysData (relaysData) { + setRelaysData(relaysData) + updateLocalStorage({ relays: relaysData }) // Update the local storage + // get the relay addresses with read set to true + const readRelays = relaysData.filter(relay => relay.read).map(relay => relay.address) + setReadRelays(readRelays) + console.log('readRelays: ', readRelays) + // get the relay addresses with write set to true + const writeRelays = relaysData.filter(relay => relay.write).map(relay => relay.address) + setWriteRelays(writeRelays) + console.log('writeRelays: ', writeRelays) + nostrQueriesRef.current = new NostrQueries({ relays: readRelays }) + } + // Restore relays data + function restoreRelaysData () { + const relaysData = [...localStorageDefault.relays] // Create a new array in order to detect changes + setRelaysData(relaysData) + updateLocalStorage({ relays: relaysData }) + // get the relay addresses with read set to true + const readRelays = relaysData.filter(relay => relay.read).map(relay => relay.address) + setReadRelays(readRelays) + console.log('readRelays: ', readRelays) + // get the relay addresses with write set to true + const writeRelays = relaysData.filter(relay => relay.write).map(relay => relay.address) + setWriteRelays(writeRelays) + console.log('writeRelays: ', writeRelays) + nostrQueriesRef.current = new NostrQueries({ relays: readRelays }) + } return { serverUrl, @@ -158,7 +200,13 @@ function useAppState () { setLastFeedTab, isSingleView, setIsSingleView, - nostrQueries: new NostrQueries() + nostrQueries: nostrQueriesRef.current, + relaysData, + updateRelaysData, + restoreRelaysData, + readRelays, + writeRelays + } } diff --git a/src/services/nostr-queries.js b/src/services/nostr-queries.js index 128b4a9..69c76e4 100644 --- a/src/services/nostr-queries.js +++ b/src/services/nostr-queries.js @@ -2,13 +2,16 @@ * Nostr class for query into relay pools * */ -import config from '../config' import { RelayPool } from 'nostr' import * as nip19 from 'nostr-tools/nip19' export default class NostrQueries { - constructor () { - this.relays = config.nostrRelays + constructor ({ relays }) { + this.relays = relays || [] + } + + setRelays (relays) { + this.relays = relays } npubToHex (npub) { @@ -22,6 +25,9 @@ export default class NostrQueries { // from any relay. If one relay fails, it will move on to the next one. async getProfile (pubHex) { try { + if (this.relays.length === 0) { + return false + } for (let i = 0; i < this.relays.length; i++) { const profile = await new Promise((resolve) => { const relay = this.relays[i] @@ -47,6 +53,10 @@ export default class NostrQueries { } relay.close() }) + pool.on('error', (relay) => { + relay.close() + resolve(false) + }) }) // Stop looking for profile if found if (profile) { @@ -61,9 +71,12 @@ export default class NostrQueries { // Get Feeds by user pubkey async getUserFeeds (pubHex) { try { + if (this.relays.length === 0) { + return [] + } let feeds = await new Promise((resolve) => { let list = [] - let closedRelays = 0 + const closedRelays = [] const pool = RelayPool(this.relays) @@ -73,9 +86,11 @@ export default class NostrQueries { pool.on('eose', relay => { relay.close() - closedRelays++ + if (!closedRelays.includes(relay)) { + closedRelays.push(relay) + } // Resolve list if all relays are closed - if (closedRelays === config.nostrRelays.length) { + if (closedRelays.length === this.relays.length) { resolve(list) } }) @@ -83,6 +98,15 @@ export default class NostrQueries { pool.on('event', (relay, subId, ev) => { list = [...list, ev] }) + pool.on('error', (relay) => { + relay.close() + if (!closedRelays.includes(relay)) { + closedRelays.push(relay) + } // Resolve list if all relays are closed + if (closedRelays.length === this.relays.length) { + resolve(list) + } + }) }) // Remove duplicated feeds feeds = feeds.filter((val, i, list) => { @@ -102,11 +126,15 @@ export default class NostrQueries { // Get global feeds async getGlobalFeeds () { try { + if (this.relays.length === 0) { + return [] + } + let feeds = await new Promise((resolve, reject) => { let list = [] - let closedRelays = 0 + const closedRelays = [] - const pool = RelayPool(config.nostrRelays) + const pool = RelayPool(this.relays) // const pool = RelayPool([config.nostrRelay]) pool.on('open', relay => { relay.subscribe('REQ', { limit: 10, kinds: [1], '#t': ['slpdex-socialmedia'] }) @@ -114,17 +142,29 @@ export default class NostrQueries { pool.on('eose', relay => { relay.close() - closedRelays++ + if (!closedRelays.includes(relay)) { + closedRelays.push(relay) + } // Resolve list if all relays are closed - if (closedRelays === config.nostrRelays.length) { + if (closedRelays.length === this.relays.length) { resolve(list) } }) pool.on('event', (relay, subId, ev) => { - // console.log('post retrieved from ', relay.url, ev.sig) + console.log('post retrieved from ', relay.url, ev.sig) list = [...list, ev] }) + pool.on('error', (relay) => { + relay.close() + if (!closedRelays.includes(relay)) { + closedRelays.push(relay) + } + // Resolve list if all relays are closed + if (closedRelays.length === this.relays.length) { + resolve(list) + } + }) }) // Remove duplicated feeds @@ -146,11 +186,14 @@ export default class NostrQueries { // Get follow list by pubkey async getFollowList (pubHex) { + if (this.relays.length === 0) { + return [] + } return new Promise((resolve, reject) => { let list = [] - let closedRelays = 0 + const closedRelays = [] - const pool = RelayPool(config.nostrRelays) + const pool = RelayPool(this.relays) // const pool = RelayPool([config.nostrRelay]) pool.on('open', relay => { relay.subscribe('subid', { limit: 1, kinds: [3], authors: [pubHex] }) @@ -158,9 +201,11 @@ export default class NostrQueries { pool.on('eose', relay => { relay.close() - closedRelays++ + if (!closedRelays.includes(relay)) { + closedRelays.push(relay) + } // Resolve list if all relays are closed - if (closedRelays === this.relays.length) { + if (closedRelays.length === this.relays.length) { resolve(list) } }) @@ -170,15 +215,28 @@ export default class NostrQueries { // Merge list received from all relays list = [...list, ...ev.tags] }) + pool.on('error', (relay) => { + relay.close() + if (!closedRelays.includes(relay)) { + closedRelays.push(relay) + } + // Resolve list if all relays are closed + if (closedRelays.length === this.relays.length) { + resolve(list) + } + }) }) } // Get event likes async getPostLikes (postId) { try { + if (this.relays.length === 0) { + return [] + } let likesRes = await new Promise((resolve) => { const likes = [] - let closedRelays = 0 + const closedRelays = [] const pool = RelayPool(this.relays) pool.on('open', relay => { @@ -187,9 +245,11 @@ export default class NostrQueries { pool.on('eose', relay => { relay.close() - closedRelays++ + if (!closedRelays.includes(relay)) { + closedRelays.push(relay) + } // Resolve list if all relays are closed - if (closedRelays === this.relays.length) { + if (closedRelays.length === this.relays.length) { resolve(likes) } }) @@ -204,6 +264,16 @@ export default class NostrQueries { // skip error } }) + pool.on('error', (relay) => { + relay.close() + if (!closedRelays.includes(relay)) { + closedRelays.push(relay) + } + // Resolve list if all relays are closed + if (closedRelays.length === this.relays.length) { + resolve(likes) + } + }) }) // Remove duplicated events