Compare commits
6 Commits
338a965f82
...
5d17ae8d4a
Author | SHA1 | Date | |
---|---|---|---|
|
5d17ae8d4a | ||
|
8790a943c3 | ||
|
d3d87be2c3 | ||
|
f4ecbe5f39 | ||
|
424e06a52b | ||
|
e3db895086 |
@ -10,7 +10,11 @@ import {
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { setAuthState, setMetadataEvent } from '../../store/actions'
|
import {
|
||||||
|
setAuthState,
|
||||||
|
setMetadataEvent,
|
||||||
|
userLogOutAction
|
||||||
|
} from '../../store/actions'
|
||||||
import { State } from '../../store/rootReducer'
|
import { State } from '../../store/rootReducer'
|
||||||
import { Dispatch } from '../../store/store'
|
import { Dispatch } from '../../store/store'
|
||||||
import Username from '../username'
|
import Username from '../username'
|
||||||
@ -24,7 +28,6 @@ import {
|
|||||||
} from '../../routes'
|
} from '../../routes'
|
||||||
import {
|
import {
|
||||||
clearAuthToken,
|
clearAuthToken,
|
||||||
clearState,
|
|
||||||
saveNsecBunkerDelegatedKey,
|
saveNsecBunkerDelegatedKey,
|
||||||
shorten
|
shorten
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@ -96,7 +99,8 @@ export const AppBar = () => {
|
|||||||
|
|
||||||
// clear authToken saved in local storage
|
// clear authToken saved in local storage
|
||||||
clearAuthToken()
|
clearAuthToken()
|
||||||
clearState()
|
|
||||||
|
dispatch(userLogOutAction())
|
||||||
|
|
||||||
// update nsecBunker delegated key after logout
|
// update nsecBunker delegated key after logout
|
||||||
const nostrController = NostrController.getInstance()
|
const nostrController = NostrController.getInstance()
|
||||||
|
@ -74,24 +74,15 @@ export class AuthController {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const relaysState = store.getState().relays
|
|
||||||
|
|
||||||
if (relaysState) {
|
|
||||||
// Relays state is defined and there is no need to await for the latest relay map
|
|
||||||
this.nostrController.getRelayMap(pubkey).then((relayMap) => {
|
|
||||||
if (!compareObjects(relaysState?.map, relayMap)) {
|
|
||||||
store.dispatch(setRelayMapAction(relayMap.map))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Relays state is not defined, await for the latest relay map
|
|
||||||
const relayMap = await this.nostrController.getRelayMap(pubkey)
|
const relayMap = await this.nostrController.getRelayMap(pubkey)
|
||||||
|
|
||||||
if (Object.keys(relayMap).length < 1) {
|
if (Object.keys(relayMap).length < 1) {
|
||||||
// Navigate user to relays page
|
// Navigate user to relays page if relay map is empty
|
||||||
return Promise.resolve(appPrivateRoutes.relays)
|
return Promise.resolve(appPrivateRoutes.relays)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (store.getState().auth?.loggedIn) {
|
||||||
|
if (!compareObjects(store.getState().relays?.map, relayMap.map))
|
||||||
store.dispatch(setRelayMapAction(relayMap.map))
|
store.dispatch(setRelayMapAction(relayMap.map))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,8 @@ import NDK, {
|
|||||||
NDKNip46Signer,
|
NDKNip46Signer,
|
||||||
NDKPrivateKeySigner,
|
NDKPrivateKeySigner,
|
||||||
NDKUser,
|
NDKUser,
|
||||||
NostrEvent
|
NostrEvent,
|
||||||
|
NDKSubscription
|
||||||
} from '@nostr-dev-kit/ndk'
|
} from '@nostr-dev-kit/ndk'
|
||||||
import {
|
import {
|
||||||
Event,
|
Event,
|
||||||
@ -11,25 +12,45 @@ import {
|
|||||||
SimplePool,
|
SimplePool,
|
||||||
UnsignedEvent,
|
UnsignedEvent,
|
||||||
Filter,
|
Filter,
|
||||||
|
Relay,
|
||||||
finalizeEvent,
|
finalizeEvent,
|
||||||
nip04,
|
nip04,
|
||||||
nip19,
|
nip19,
|
||||||
kinds
|
kinds
|
||||||
} from 'nostr-tools'
|
} from 'nostr-tools'
|
||||||
import { EventEmitter } from 'tseep'
|
import { EventEmitter } from 'tseep'
|
||||||
import { updateNsecbunkerPubkey } from '../store/actions'
|
import {
|
||||||
|
updateNsecbunkerPubkey,
|
||||||
|
setMostPopularRelaysAction,
|
||||||
|
setRelayInfoAction,
|
||||||
|
setRelayConnectionStatusAction
|
||||||
|
} from '../store/actions'
|
||||||
import { AuthState, LoginMethods } from '../store/auth/types'
|
import { AuthState, LoginMethods } from '../store/auth/types'
|
||||||
import store from '../store/store'
|
import store from '../store/store'
|
||||||
import { SignedEvent, RelayMap } from '../types'
|
import {
|
||||||
import { getNsecBunkerDelegatedKey, verifySignedEvent } from '../utils'
|
SignedEvent,
|
||||||
|
RelayMap,
|
||||||
|
RelayStats,
|
||||||
|
ReadRelay,
|
||||||
|
RelayInfoObject,
|
||||||
|
RelayConnectionStatus,
|
||||||
|
RelayConnectionState
|
||||||
|
} from '../types'
|
||||||
|
import {
|
||||||
|
compareObjects,
|
||||||
|
getNsecBunkerDelegatedKey,
|
||||||
|
verifySignedEvent
|
||||||
|
} from '../utils'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
export class NostrController extends EventEmitter {
|
export class NostrController extends EventEmitter {
|
||||||
private static instance: NostrController
|
private static instance: NostrController
|
||||||
private specialMetadataRelay = 'wss://purplepag.es'
|
|
||||||
|
|
||||||
private bunkerNDK: NDK | undefined
|
private bunkerNDK: NDK | undefined
|
||||||
private remoteSigner: NDKNip46Signer | undefined
|
private remoteSigner: NDKNip46Signer | undefined
|
||||||
|
|
||||||
|
private connectedRelays: Relay[] | undefined
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@ -390,12 +411,7 @@ export class NostrController extends EventEmitter {
|
|||||||
getRelayMap = async (
|
getRelayMap = async (
|
||||||
npub: string
|
npub: string
|
||||||
): Promise<{ map: RelayMap; mapUpdated: number }> => {
|
): Promise<{ map: RelayMap; mapUpdated: number }> => {
|
||||||
const mostPopularRelays = import.meta.env.VITE_MOST_POPULAR_RELAYS
|
const mostPopularRelays = await this.getMostPopularRelays()
|
||||||
const hardcodedPopularRelays = (mostPopularRelays || '').split(' ')
|
|
||||||
const popularRelayURIs = [
|
|
||||||
this.specialMetadataRelay,
|
|
||||||
...hardcodedPopularRelays
|
|
||||||
]
|
|
||||||
|
|
||||||
const pool = new SimplePool()
|
const pool = new SimplePool()
|
||||||
|
|
||||||
@ -405,7 +421,9 @@ export class NostrController extends EventEmitter {
|
|||||||
authors: [npub]
|
authors: [npub]
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = await pool.get(popularRelayURIs, eventFilter).catch((err) => {
|
const event = await pool
|
||||||
|
.get(mostPopularRelays, eventFilter)
|
||||||
|
.catch((err) => {
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -427,6 +445,10 @@ export class NostrController extends EventEmitter {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.getRelayInfo(Object.keys(relaysMap))
|
||||||
|
|
||||||
|
this.connectToRelays(Object.keys(relaysMap))
|
||||||
|
|
||||||
return Promise.resolve({ map: relaysMap, mapUpdated: event.created_at })
|
return Promise.resolve({ map: relaysMap, mapUpdated: event.created_at })
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject('User relays were not found.')
|
return Promise.reject('User relays were not found.')
|
||||||
@ -437,11 +459,13 @@ export class NostrController extends EventEmitter {
|
|||||||
* Publishes relay map.
|
* Publishes relay map.
|
||||||
* @param relayMap - relay map.
|
* @param relayMap - relay map.
|
||||||
* @param npub - user's npub.
|
* @param npub - user's npub.
|
||||||
|
* @param extraRelaysToPublish - optional relays to publish relay map.
|
||||||
* @returns - promise that resolves into a string representing publishing result.
|
* @returns - promise that resolves into a string representing publishing result.
|
||||||
*/
|
*/
|
||||||
publishRelayMap = async (
|
publishRelayMap = async (
|
||||||
relayMap: RelayMap,
|
relayMap: RelayMap,
|
||||||
npub: string
|
npub: string,
|
||||||
|
extraRelaysToPublish?: string[]
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
const timestamp = Math.floor(Date.now() / 1000)
|
const timestamp = Math.floor(Date.now() / 1000)
|
||||||
const relayURIs = Object.keys(relayMap)
|
const relayURIs = Object.keys(relayMap)
|
||||||
@ -471,18 +495,266 @@ export class NostrController extends EventEmitter {
|
|||||||
|
|
||||||
let relaysToPublish = relayURIs
|
let relaysToPublish = relayURIs
|
||||||
|
|
||||||
// If relay map is empty, use most popular relay URIs
|
// Add extra relays if provided
|
||||||
if (!relaysToPublish.length) {
|
if (extraRelaysToPublish) {
|
||||||
const mostPopularRelays = import.meta.env.VITE_MOST_POPULAR_RELAYS
|
relaysToPublish = [...relaysToPublish, ...extraRelaysToPublish]
|
||||||
const hardcodedPopularRelays = (mostPopularRelays || '').split(' ')
|
|
||||||
|
|
||||||
relaysToPublish = [this.specialMetadataRelay, ...hardcodedPopularRelays]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.publishEvent(signedEvent, relaysToPublish)
|
// If relay map is empty, use most popular relay URIs
|
||||||
|
if (!relaysToPublish.length) {
|
||||||
|
relaysToPublish = await this.getMostPopularRelays()
|
||||||
|
}
|
||||||
|
|
||||||
|
const publishResult = await this.publishEvent(signedEvent, relaysToPublish)
|
||||||
|
|
||||||
|
if (publishResult && publishResult.length) {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
`Relay Map published on: ${relaysToPublish.join('\n')}`
|
`Relay Map published on: ${publishResult.join('\n')}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject('Publishing updated relay map was unsuccessful.')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides most popular relays.
|
||||||
|
* @param numberOfTopRelays - number representing how many most popular relays to provide
|
||||||
|
* @returns - promise that resolves into an array of most popular relays
|
||||||
|
*/
|
||||||
|
getMostPopularRelays = async (
|
||||||
|
numberOfTopRelays: number = 30
|
||||||
|
): Promise<string[]> => {
|
||||||
|
const mostPopularRelaysState = store.getState().relays?.mostPopular
|
||||||
|
|
||||||
|
// return most popular relays from app state if present
|
||||||
|
if (mostPopularRelaysState) return mostPopularRelaysState
|
||||||
|
|
||||||
|
// relays in env
|
||||||
|
const { VITE_MOST_POPULAR_RELAYS } = import.meta.env
|
||||||
|
const hardcodedPopularRelays = (VITE_MOST_POPULAR_RELAYS || '').split(' ')
|
||||||
|
const url = `https://stats.nostr.band/stats_api?method=stats`
|
||||||
|
|
||||||
|
const response = await axios.get<RelayStats>(url).catch(() => undefined)
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return hardcodedPopularRelays //return hardcoded relay list
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return hardcodedPopularRelays //return hardcoded relay list
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiTopRelays = data.relay_stats.user_picks.read_relays
|
||||||
|
.slice(0, numberOfTopRelays)
|
||||||
|
.map((relay: ReadRelay) => relay.d)
|
||||||
|
|
||||||
|
if (!apiTopRelays.length) {
|
||||||
|
return Promise.reject(`Couldn't fetch popular relays.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.getState().auth?.loggedIn) {
|
||||||
|
store.dispatch(setMostPopularRelaysAction(apiTopRelays))
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiTopRelays
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets information about relays into relays.info app state.
|
||||||
|
* @param relayURIs - relay URIs to get information about
|
||||||
|
*/
|
||||||
|
getRelayInfo = async (relayURIs: string[]) => {
|
||||||
|
// initialize job request
|
||||||
|
const jobEventTemplate: EventTemplate = {
|
||||||
|
content: '',
|
||||||
|
created_at: Math.round(Date.now() / 1000),
|
||||||
|
kind: 68001,
|
||||||
|
tags: [
|
||||||
|
['i', `${JSON.stringify(relayURIs)}`],
|
||||||
|
['j', 'relay-info']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign job request event
|
||||||
|
const jobSignedEvent = await this.signEvent(jobEventTemplate)
|
||||||
|
|
||||||
|
const relays = [
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://relay.primal.net',
|
||||||
|
'wss://relayable.org'
|
||||||
|
]
|
||||||
|
|
||||||
|
// publish job request
|
||||||
|
await this.publishEvent(jobSignedEvent, relays)
|
||||||
|
|
||||||
|
console.log('jobSignedEvent :>> ', jobSignedEvent)
|
||||||
|
|
||||||
|
const subscribeWithTimeout = (
|
||||||
|
subscription: NDKSubscription,
|
||||||
|
timeoutMs: number
|
||||||
|
): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const eventHandler = (event: NDKEvent) => {
|
||||||
|
subscription.stop()
|
||||||
|
resolve(event.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.on('event', eventHandler)
|
||||||
|
|
||||||
|
// Set up a timeout to stop the subscription after a specified time
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
subscription.stop() // Stop the subscription
|
||||||
|
reject(new Error('Subscription timed out')) // Reject the promise with a timeout error
|
||||||
|
}, timeoutMs)
|
||||||
|
|
||||||
|
// Handle subscription close event
|
||||||
|
subscription.on('close', () => clearTimeout(timeout))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const dvmNDK = new NDK({
|
||||||
|
explicitRelayUrls: relays
|
||||||
|
})
|
||||||
|
|
||||||
|
await dvmNDK.connect(2000)
|
||||||
|
|
||||||
|
// filter for getting DVM job's result
|
||||||
|
const sub = dvmNDK.subscribe({
|
||||||
|
kinds: [68002 as number],
|
||||||
|
'#e': [jobSignedEvent.id],
|
||||||
|
'#p': [jobSignedEvent.pubkey]
|
||||||
|
})
|
||||||
|
|
||||||
|
// asynchronously get block number from dvm job with 20 seconds timeout
|
||||||
|
const dvmJobResult = await subscribeWithTimeout(sub, 20000)
|
||||||
|
|
||||||
|
if (!dvmJobResult) {
|
||||||
|
return Promise.reject(`Relay(s) information wasn't received`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let relaysInfo: RelayInfoObject
|
||||||
|
|
||||||
|
try {
|
||||||
|
relaysInfo = JSON.parse(dvmJobResult)
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(`Invalid relay(s) information.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
relaysInfo &&
|
||||||
|
!compareObjects(store.getState().relays?.info, relaysInfo)
|
||||||
|
) {
|
||||||
|
store.dispatch(setRelayInfoAction(relaysInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establishes connection to relays.
|
||||||
|
* @param relayURIs - an array of relay URIs
|
||||||
|
* @returns - promise that resolves into an array of connections
|
||||||
|
*/
|
||||||
|
connectToRelays = async (relayURIs: string[]) => {
|
||||||
|
// Copy of relay connection status
|
||||||
|
const relayConnectionsStatus: RelayConnectionStatus = JSON.parse(
|
||||||
|
JSON.stringify(store.getState().relays?.connectionStatus || {})
|
||||||
|
)
|
||||||
|
|
||||||
|
const connectedRelayURLs = this.connectedRelays
|
||||||
|
? this.connectedRelays.map((relay) => relay.url)
|
||||||
|
: []
|
||||||
|
|
||||||
|
// Check if connections already established
|
||||||
|
if (compareObjects(connectedRelayURLs, relayURIs)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const connections = relayURIs
|
||||||
|
.filter((relayURI) => !connectedRelayURLs.includes(relayURI))
|
||||||
|
.map((relayURI) =>
|
||||||
|
Relay.connect(relayURI)
|
||||||
|
.then((relay) => {
|
||||||
|
// put connection status into relayConnectionsStatus object
|
||||||
|
relayConnectionsStatus[relayURI] = relay.connected
|
||||||
|
? RelayConnectionState.Connected
|
||||||
|
: RelayConnectionState.NotConnected
|
||||||
|
|
||||||
|
return relay
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
relayConnectionsStatus[relayURI] = RelayConnectionState.NotConnected
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const connected = await Promise.all(connections)
|
||||||
|
|
||||||
|
// put connected relays into connectedRelays private property, so it can be closed later
|
||||||
|
this.connectedRelays = connected.filter(
|
||||||
|
(relay) => relay instanceof Relay && relay.connected
|
||||||
|
) as Relay[]
|
||||||
|
|
||||||
|
if (Object.keys(relayConnectionsStatus)) {
|
||||||
|
if (
|
||||||
|
!compareObjects(
|
||||||
|
store.getState().relays?.connectionStatus,
|
||||||
|
relayConnectionsStatus
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
store.dispatch(setRelayConnectionStatusAction(relayConnectionsStatus))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(relayConnectionsStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects from relays.
|
||||||
|
* @param relayURIs - array of relay URIs to disconnect from
|
||||||
|
*/
|
||||||
|
disconnectFromRelays = async (relayURIs: string[]) => {
|
||||||
|
const connectedRelayURLs = this.connectedRelays
|
||||||
|
? this.connectedRelays.map((relay) => relay.url)
|
||||||
|
: []
|
||||||
|
|
||||||
|
relayURIs
|
||||||
|
.filter((relayURI) => connectedRelayURLs.includes(relayURI))
|
||||||
|
.forEach((relayURI) => {
|
||||||
|
if (this.connectedRelays) {
|
||||||
|
const relay = this.connectedRelays.find(
|
||||||
|
(relay) => relay.url === relayURI
|
||||||
|
)
|
||||||
|
|
||||||
|
if (relay) {
|
||||||
|
// close relay connection
|
||||||
|
relay.close()
|
||||||
|
|
||||||
|
// remove relay from connectedRelays property
|
||||||
|
this.connectedRelays = this.connectedRelays.filter(
|
||||||
|
(relay) => relay.url !== relayURI
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (store.getState().relays?.connectionStatus) {
|
||||||
|
const connectionStatus = JSON.parse(
|
||||||
|
JSON.stringify(store.getState().relays?.connectionStatus)
|
||||||
|
)
|
||||||
|
|
||||||
|
relayURIs.forEach((relay) => {
|
||||||
|
delete connectionStatus[relay]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (
|
||||||
|
!compareObjects(
|
||||||
|
store.getState().relays?.connectionStatus,
|
||||||
|
connectionStatus
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Update app state
|
||||||
|
store.dispatch(setRelayConnectionStatusAction(connectionStatus))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,12 +6,32 @@ import Switch from '@mui/material/Switch'
|
|||||||
import ListItemText from '@mui/material/ListItemText'
|
import ListItemText from '@mui/material/ListItemText'
|
||||||
import Divider from '@mui/material/Divider'
|
import Divider from '@mui/material/Divider'
|
||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import { RelayMap } from '../../types'
|
import {
|
||||||
|
RelayMap,
|
||||||
|
RelayInfoObject,
|
||||||
|
RelayFee,
|
||||||
|
RelayConnectionState
|
||||||
|
} from '../../types'
|
||||||
import LogoutIcon from '@mui/icons-material/Logout'
|
import LogoutIcon from '@mui/icons-material/Logout'
|
||||||
import { useAppSelector, useAppDispatch } from '../../hooks'
|
import { useAppSelector, useAppDispatch } from '../../hooks'
|
||||||
import { compareObjects } from '../../utils'
|
import {
|
||||||
import { setRelayMapAction } from '../../store/actions'
|
compareObjects,
|
||||||
|
shorten,
|
||||||
|
hexToNpub,
|
||||||
|
capitalizeFirstLetter
|
||||||
|
} from '../../utils'
|
||||||
|
import {
|
||||||
|
setRelayMapAction,
|
||||||
|
setRelayMapUpdatedAction
|
||||||
|
} from '../../store/actions'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
||||||
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'
|
||||||
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
||||||
|
import ElectricBoltIcon from '@mui/icons-material/ElectricBolt'
|
||||||
|
import { Tooltip } from '@mui/material'
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
|
||||||
export const RelaysPage = () => {
|
export const RelaysPage = () => {
|
||||||
const nostrController = NostrController.getInstance()
|
const nostrController = NostrController.getInstance()
|
||||||
@ -26,6 +46,30 @@ export const RelaysPage = () => {
|
|||||||
const [relayMap, setRelayMap] = useState<RelayMap | undefined>(
|
const [relayMap, setRelayMap] = useState<RelayMap | undefined>(
|
||||||
relaysState?.map
|
relaysState?.map
|
||||||
)
|
)
|
||||||
|
const [relaysInfo, setRelaysInfo] = useState<RelayInfoObject | undefined>(
|
||||||
|
relaysState?.info
|
||||||
|
)
|
||||||
|
const [displayRelaysInfo, setDisplayRelaysInfo] = useState<string[]>([])
|
||||||
|
const [relaysConnectionStatus, setRelaysConnectionStatus] = useState(
|
||||||
|
relaysState?.connectionStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
const webSocketPrefix = 'wss://'
|
||||||
|
|
||||||
|
// Update relay connection status
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!compareObjects(relaysConnectionStatus, relaysState?.connectionStatus)
|
||||||
|
) {
|
||||||
|
setRelaysConnectionStatus(relaysState?.connectionStatus)
|
||||||
|
}
|
||||||
|
}, [relaysConnectionStatus, relaysState?.connectionStatus])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!compareObjects(relaysInfo, relaysState?.info)) {
|
||||||
|
setRelaysInfo(relaysState?.info)
|
||||||
|
}
|
||||||
|
}, [relaysInfo, relaysState?.info])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = false
|
let isMounted = false
|
||||||
@ -42,7 +86,7 @@ export const RelaysPage = () => {
|
|||||||
if (
|
if (
|
||||||
!relaysState?.mapUpdated ||
|
!relaysState?.mapUpdated ||
|
||||||
newRelayMap.mapUpdated > relaysState?.mapUpdated
|
newRelayMap.mapUpdated > relaysState?.mapUpdated
|
||||||
)
|
) {
|
||||||
if (
|
if (
|
||||||
!relaysState?.map ||
|
!relaysState?.map ||
|
||||||
!compareObjects(relaysState.map, newRelayMap)
|
!compareObjects(relaysState.map, newRelayMap)
|
||||||
@ -50,6 +94,10 @@ export const RelaysPage = () => {
|
|||||||
setRelayMap(newRelayMap.map)
|
setRelayMap(newRelayMap.map)
|
||||||
|
|
||||||
dispatch(setRelayMapAction(newRelayMap.map))
|
dispatch(setRelayMapAction(newRelayMap.map))
|
||||||
|
} else {
|
||||||
|
// Update relay map updated timestamp
|
||||||
|
dispatch(setRelayMapUpdatedAction())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,13 +111,32 @@ export const RelaysPage = () => {
|
|||||||
Date.now() - relaysState?.mapUpdated > 5 * 60 * 1000) // 5 minutes
|
Date.now() - relaysState?.mapUpdated > 5 * 60 * 1000) // 5 minutes
|
||||||
) {
|
) {
|
||||||
fetchData()
|
fetchData()
|
||||||
|
|
||||||
|
// Update relay connection status
|
||||||
|
if (relaysConnectionStatus) {
|
||||||
|
const notConnectedRelays = Object.keys(relaysConnectionStatus).filter(
|
||||||
|
(key) =>
|
||||||
|
relaysConnectionStatus[key] === RelayConnectionState.NotConnected
|
||||||
|
)
|
||||||
|
|
||||||
|
if (notConnectedRelays.length) {
|
||||||
|
nostrController.connectToRelays(notConnectedRelays)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup func
|
// cleanup func
|
||||||
return () => {
|
return () => {
|
||||||
isMounted = false
|
isMounted = false
|
||||||
}
|
}
|
||||||
}, [dispatch, usersPubkey, relaysState, nostrController])
|
}, [
|
||||||
|
dispatch,
|
||||||
|
usersPubkey,
|
||||||
|
relaysState?.map,
|
||||||
|
relaysState?.mapUpdated,
|
||||||
|
nostrController,
|
||||||
|
relaysConnectionStatus
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Display notification if an empty relay map has been received
|
// Display notification if an empty relay map has been received
|
||||||
@ -83,26 +150,39 @@ export const RelaysPage = () => {
|
|||||||
|
|
||||||
const handleLeaveRelay = async (relay: string) => {
|
const handleLeaveRelay = async (relay: string) => {
|
||||||
if (relayMap) {
|
if (relayMap) {
|
||||||
if (Object.keys(relayMap).length === 1) {
|
const relaysInMap = Object.keys(relayMap).length
|
||||||
|
const writeRelays = Object.keys(relayMap).filter(
|
||||||
|
(key) => relayMap[key].write
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if at least one write relay is present in relay map
|
||||||
|
if (
|
||||||
|
relaysInMap <= 1 ||
|
||||||
|
(writeRelays.length === 1 && writeRelays.includes(relay))
|
||||||
|
) {
|
||||||
relayRequirementWarning()
|
relayRequirementWarning()
|
||||||
} else {
|
} else {
|
||||||
const relayMapCopy = JSON.parse(JSON.stringify(relayMap))
|
const relayMapCopy = JSON.parse(JSON.stringify(relayMap))
|
||||||
// Remove relay from relay map
|
// Remove relay from relay map
|
||||||
delete relayMapCopy[relay]
|
delete relayMapCopy[relay]
|
||||||
|
|
||||||
setRelayMap(relayMapCopy)
|
|
||||||
|
|
||||||
dispatch(setRelayMapAction(relayMapCopy))
|
|
||||||
|
|
||||||
if (usersPubkey) {
|
if (usersPubkey) {
|
||||||
// Publish updated relay map.
|
// Publish updated relay map.
|
||||||
const relayMapPublishingRes = await nostrController
|
const relayMapPublishingRes = await nostrController
|
||||||
.publishRelayMap(relayMapCopy, usersPubkey)
|
.publishRelayMap(relayMapCopy, usersPubkey, [relay])
|
||||||
.catch((err) => handlePublishRelayMapError(err))
|
.catch((err) => handlePublishRelayMapError(err))
|
||||||
|
|
||||||
if (relayMapPublishingRes) toast.success(relayMapPublishingRes)
|
if (relayMapPublishingRes) {
|
||||||
|
toast.success(relayMapPublishingRes)
|
||||||
|
|
||||||
|
setRelayMap(relayMapCopy)
|
||||||
|
|
||||||
|
dispatch(setRelayMapAction(relayMapCopy))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nostrController.disconnectFromRelays([relay])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,67 +215,120 @@ export const RelaysPage = () => {
|
|||||||
const relayMapCopy = JSON.parse(JSON.stringify(relayMap))
|
const relayMapCopy = JSON.parse(JSON.stringify(relayMap))
|
||||||
relayMapCopy[relay].write = event.target.checked
|
relayMapCopy[relay].write = event.target.checked
|
||||||
|
|
||||||
setRelayMap(relayMapCopy)
|
|
||||||
|
|
||||||
dispatch(setRelayMapAction(relayMapCopy))
|
|
||||||
|
|
||||||
if (usersPubkey) {
|
if (usersPubkey) {
|
||||||
// Publish updated relay map
|
// Publish updated relay map
|
||||||
const relayMapPublishingRes = await nostrController
|
const relayMapPublishingRes = await nostrController
|
||||||
.publishRelayMap(relayMapCopy, usersPubkey)
|
.publishRelayMap(relayMapCopy, usersPubkey)
|
||||||
.catch((err) => handlePublishRelayMapError(err))
|
.catch((err) => handlePublishRelayMapError(err))
|
||||||
|
|
||||||
if (relayMapPublishingRes) toast.success(relayMapPublishingRes)
|
if (relayMapPublishingRes) {
|
||||||
|
toast.success(relayMapPublishingRes)
|
||||||
|
|
||||||
|
setRelayMap(relayMapCopy)
|
||||||
|
|
||||||
|
dispatch(setRelayMapAction(relayMapCopy))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTextFieldChange = async () => {
|
const handleAddNewRelay = async () => {
|
||||||
|
const relayURI = `${webSocketPrefix}${newRelayURI?.trim().replace(webSocketPrefix, '')}`
|
||||||
|
|
||||||
// Check if new relay URI is a valid string
|
// Check if new relay URI is a valid string
|
||||||
if (
|
if (
|
||||||
newRelayURI &&
|
relayURI &&
|
||||||
!/^wss:\/\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/.test(
|
!/^wss:\/\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/.test(
|
||||||
newRelayURI
|
relayURI
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
if (relayURI !== webSocketPrefix) {
|
||||||
setNewRelayURIerror(
|
setNewRelayURIerror(
|
||||||
'New relay URI is not valid. Example of valid relay URI: wss://sigit.relay.io'
|
'New relay URI is not valid. Example of valid relay URI: wss://sigit.relay.io'
|
||||||
)
|
)
|
||||||
} else if (newRelayURI) {
|
}
|
||||||
|
} else if (relayURI && usersPubkey) {
|
||||||
|
const connectionStatus = await nostrController.connectToRelays([relayURI])
|
||||||
|
|
||||||
|
if (
|
||||||
|
connectionStatus &&
|
||||||
|
connectionStatus[relayURI] &&
|
||||||
|
connectionStatus[relayURI] === RelayConnectionState.Connected
|
||||||
|
) {
|
||||||
const relayMapCopy = JSON.parse(JSON.stringify(relayMap))
|
const relayMapCopy = JSON.parse(JSON.stringify(relayMap))
|
||||||
|
|
||||||
relayMapCopy[newRelayURI.trim()] = { write: true, read: true }
|
relayMapCopy[relayURI] = { write: true, read: true }
|
||||||
|
|
||||||
|
// Publish updated relay map
|
||||||
|
const relayMapPublishingRes = await nostrController
|
||||||
|
.publishRelayMap(relayMapCopy, usersPubkey)
|
||||||
|
.catch((err) => handlePublishRelayMapError(err))
|
||||||
|
|
||||||
|
if (relayMapPublishingRes) {
|
||||||
setRelayMap(relayMapCopy)
|
setRelayMap(relayMapCopy)
|
||||||
setNewRelayURI('')
|
setNewRelayURI('')
|
||||||
|
|
||||||
dispatch(setRelayMapAction(relayMapCopy))
|
dispatch(setRelayMapAction(relayMapCopy))
|
||||||
|
|
||||||
if (usersPubkey) {
|
nostrController.getRelayInfo([relayURI])
|
||||||
// Publish updated relay map
|
|
||||||
const relayMapPublishingRes = await nostrController
|
|
||||||
.publishRelayMap(relayMapCopy, usersPubkey)
|
|
||||||
.catch((err) => handlePublishRelayMapError(err))
|
|
||||||
|
|
||||||
if (relayMapPublishingRes) toast.success(relayMapPublishingRes)
|
toast.success(relayMapPublishingRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewRelayURIerror(undefined)
|
||||||
|
} else {
|
||||||
|
toast.error(`Relay '${relayURI}' wasn't added.`)
|
||||||
|
|
||||||
|
setNewRelayURIerror(`Connection to '${relayURI}' was unsuccessful.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle relay open and close state
|
||||||
|
const handleRelayInfo = (relay: string) => {
|
||||||
|
if (relaysInfo) {
|
||||||
|
const info = relaysInfo[relay]
|
||||||
|
|
||||||
|
if (info) {
|
||||||
|
let displayRelaysInfoCopy: string[] = JSON.parse(
|
||||||
|
JSON.stringify(displayRelaysInfo)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (displayRelaysInfoCopy.includes(relay)) {
|
||||||
|
displayRelaysInfoCopy = displayRelaysInfoCopy.filter(
|
||||||
|
(rel) => rel !== relay
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
displayRelaysInfoCopy.push(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisplayRelaysInfo(displayRelaysInfoCopy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={styles.container}>
|
<Box className={styles.container}>
|
||||||
<Box>
|
<Box className={styles.relayAddContainer}>
|
||||||
<TextField
|
<TextField
|
||||||
label="Add new relay"
|
label="Add new relay"
|
||||||
value={newRelayURI}
|
value={newRelayURI}
|
||||||
onBlur={() => handleTextFieldChange()}
|
|
||||||
onChange={(e) => setNewRelayURI(e.target.value)}
|
onChange={(e) => setNewRelayURI(e.target.value)}
|
||||||
helperText={newRelayURIerror}
|
helperText={newRelayURIerror}
|
||||||
error={!!newRelayURIerror}
|
error={!!newRelayURIerror}
|
||||||
placeholder="wss://"
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
{webSocketPrefix}
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
className={styles.relayURItextfield}
|
className={styles.relayURItextfield}
|
||||||
/>
|
/>
|
||||||
|
<Button variant="contained" onClick={() => handleAddNewRelay()}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Box className={styles.sectionTitle}>
|
<Box className={styles.sectionTitle}>
|
||||||
<RouterIcon className={styles.sectionIcon} />
|
<RouterIcon className={styles.sectionIcon} />
|
||||||
@ -207,7 +340,32 @@ export const RelaysPage = () => {
|
|||||||
<Box className={styles.relay} key={`relay_${i}`}>
|
<Box className={styles.relay} key={`relay_${i}`}>
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
styles.connectionStatus,
|
||||||
|
relaysConnectionStatus
|
||||||
|
? relaysConnectionStatus[relay] ===
|
||||||
|
RelayConnectionState.Connected
|
||||||
|
? styles.connectionStatusConnected
|
||||||
|
: styles.connectionStatusNotConnected
|
||||||
|
: styles.connectionStatusUnknown
|
||||||
|
].join(' ')}
|
||||||
|
/>
|
||||||
|
{relaysInfo &&
|
||||||
|
relaysInfo[relay] &&
|
||||||
|
relaysInfo[relay].limitation &&
|
||||||
|
relaysInfo[relay].limitation?.payment_required && (
|
||||||
|
<Tooltip title="Paid Relay" arrow placement="top">
|
||||||
|
<ElectricBoltIcon
|
||||||
|
className={styles.lightningIcon}
|
||||||
|
color="warning"
|
||||||
|
onClick={() => handleRelayInfo(relay)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
<ListItemText primary={relay} />
|
<ListItemText primary={relay} />
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
className={styles.leaveRelayContainer}
|
className={styles.leaveRelayContainer}
|
||||||
onClick={() => handleLeaveRelay(relay)}
|
onClick={() => handleLeaveRelay(relay)}
|
||||||
@ -218,12 +376,132 @@ export const RelaysPage = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider className={styles.relayDivider} />
|
<Divider className={styles.relayDivider} />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="Publish to this relay?" />
|
<ListItemText
|
||||||
|
primary="Publish to this relay?"
|
||||||
|
secondary={
|
||||||
|
relaysInfo && relaysInfo[relay] ? (
|
||||||
|
<span
|
||||||
|
onClick={() => handleRelayInfo(relay)}
|
||||||
|
className={styles.showInfo}
|
||||||
|
>
|
||||||
|
Show info{' '}
|
||||||
|
{displayRelaysInfo.includes(relay) ? (
|
||||||
|
<KeyboardArrowUpIcon
|
||||||
|
className={styles.showInfoIcon}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<KeyboardArrowDownIcon
|
||||||
|
className={styles.showInfoIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
checked={relayMap[relay].write}
|
checked={relayMap[relay].write}
|
||||||
onChange={(event) => handleRelayWriteChange(relay, event)}
|
onChange={(event) => handleRelayWriteChange(relay, event)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
{displayRelaysInfo.includes(relay) && (
|
||||||
|
<>
|
||||||
|
<Divider className={styles.relayDivider} />
|
||||||
|
<ListItem>
|
||||||
|
<Box className={styles.relayInfoContainer}>
|
||||||
|
{relaysInfo &&
|
||||||
|
relaysInfo[relay] &&
|
||||||
|
Object.keys(relaysInfo[relay]).map((key: string) => {
|
||||||
|
const infoTitle = capitalizeFirstLetter(
|
||||||
|
key.replace('_', ' ')
|
||||||
|
)
|
||||||
|
let infoValue = (relaysInfo[relay] as any)[key]
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'pubkey':
|
||||||
|
infoValue = shorten(hexToNpub(infoValue), 15)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'limitation':
|
||||||
|
infoValue = (
|
||||||
|
<ul key={`${i}_${key}`}>
|
||||||
|
{Object.keys(infoValue).map((valueKey) => (
|
||||||
|
<li key={`${i}_${key}_${valueKey}`}>
|
||||||
|
<span
|
||||||
|
className={styles.relayInfoSubTitle}
|
||||||
|
>
|
||||||
|
{capitalizeFirstLetter(
|
||||||
|
valueKey.split('_').join(' ')
|
||||||
|
)}
|
||||||
|
:
|
||||||
|
</span>{' '}
|
||||||
|
{`${infoValue[valueKey]}`}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'fees':
|
||||||
|
infoValue = (
|
||||||
|
<ul>
|
||||||
|
{Object.keys(infoValue).map((valueKey) => (
|
||||||
|
<li key={`${i}_${key}_${valueKey}`}>
|
||||||
|
<span
|
||||||
|
className={styles.relayInfoSubTitle}
|
||||||
|
>
|
||||||
|
{capitalizeFirstLetter(
|
||||||
|
valueKey.split('_').join(' ')
|
||||||
|
)}
|
||||||
|
:
|
||||||
|
</span>{' '}
|
||||||
|
{`${infoValue[valueKey].map((fee: RelayFee) => `${fee.amount} ${fee.unit}`)}`}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(infoValue)) {
|
||||||
|
infoValue = infoValue.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={`${i}_${key}_container`}>
|
||||||
|
<span className={styles.relayInfoTitle}>
|
||||||
|
{infoTitle}:
|
||||||
|
</span>{' '}
|
||||||
|
{infoValue}
|
||||||
|
{key === 'pubkey' ? (
|
||||||
|
<ContentCopyIcon
|
||||||
|
className={styles.copyItem}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
hexToNpub(
|
||||||
|
(relaysInfo[relay] as any)[key]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
toast.success('Copied to clipboard', {
|
||||||
|
autoClose: 1000,
|
||||||
|
hideProgressBar: true
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</List>
|
</List>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
@ -7,6 +7,13 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.relayAddContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.sectionIcon {
|
.sectionIcon {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
}
|
}
|
||||||
@ -43,5 +50,57 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.showInfo {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showInfoIcon {
|
||||||
|
margin-right: 3px;
|
||||||
|
margin-bottom: auto;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relayInfoContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
text-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relayInfoTitle {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relayInfoSubTitle {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyItem {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #34495e;
|
||||||
|
vertical-align: bottom;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectionStatus {
|
||||||
|
border-radius: 9999px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectionStatusConnected {
|
||||||
|
background-color: $review-feedback-correct;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectionStatusNotConnected {
|
||||||
|
background-color: $review-feedback-incorrect;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectionStatusUnknown {
|
||||||
|
background-color: $input-text-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
export const RESTORE_STATE = 'RESTORE_STATE'
|
export const RESTORE_STATE = 'RESTORE_STATE'
|
||||||
|
|
||||||
|
export const USER_LOGOUT = 'USER_LOGOUT'
|
||||||
|
|
||||||
export const SET_AUTH_STATE = 'SET_AUTH_STATE'
|
export const SET_AUTH_STATE = 'SET_AUTH_STATE'
|
||||||
export const UPDATE_LOGIN_METHOD = 'UPDATE_LOGIN_METHOD'
|
export const UPDATE_LOGIN_METHOD = 'UPDATE_LOGIN_METHOD'
|
||||||
export const UPDATE_KEYPAIR = 'UPDATE_KEYPAIR'
|
export const UPDATE_KEYPAIR = 'UPDATE_KEYPAIR'
|
||||||
@ -11,3 +13,7 @@ export const SET_METADATA_EVENT = 'SET_METADATA_EVENT'
|
|||||||
export const SET_USER_ROBOT_IMAGE = 'SET_USER_ROBOT_IMAGE'
|
export const SET_USER_ROBOT_IMAGE = 'SET_USER_ROBOT_IMAGE'
|
||||||
|
|
||||||
export const SET_RELAY_MAP = 'SET_RELAY_MAP'
|
export const SET_RELAY_MAP = 'SET_RELAY_MAP'
|
||||||
|
export const SET_RELAY_INFO = 'SET_RELAY_INFO'
|
||||||
|
export const SET_RELAY_MAP_UPDATED = 'SET_RELAY_MAP_UPDATED'
|
||||||
|
export const SET_MOST_POPULAR_RELAYS = 'SET_MOST_POPULAR_RELAYS'
|
||||||
|
export const SET_RELAY_CONNECTION_STATUS = 'SET_RELAY_CONNECTION_STATUS'
|
||||||
|
@ -16,3 +16,9 @@ export interface RestoreState {
|
|||||||
type: typeof ActionTypes.RESTORE_STATE
|
type: typeof ActionTypes.RESTORE_STATE
|
||||||
payload: State
|
payload: State
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const userLogOutAction = () => {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.USER_LOGOUT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,8 +1,39 @@
|
|||||||
import * as ActionTypes from '../actionTypes'
|
import * as ActionTypes from '../actionTypes'
|
||||||
import { SetRelayMapAction } from './types'
|
import {
|
||||||
import { RelayMap } from '../../types'
|
SetRelayMapAction,
|
||||||
|
SetMostPopularRelaysAction,
|
||||||
|
SetRelayInfoAction,
|
||||||
|
SetRelayConnectionStatusAction,
|
||||||
|
SetRelayMapUpdatedAction
|
||||||
|
} from './types'
|
||||||
|
import { RelayMap, RelayInfoObject, RelayConnectionStatus } from '../../types'
|
||||||
|
|
||||||
export const setRelayMapAction = (payload: RelayMap): SetRelayMapAction => ({
|
export const setRelayMapAction = (payload: RelayMap): SetRelayMapAction => ({
|
||||||
type: ActionTypes.SET_RELAY_MAP,
|
type: ActionTypes.SET_RELAY_MAP,
|
||||||
payload
|
payload
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const setRelayInfoAction = (
|
||||||
|
payload: RelayInfoObject
|
||||||
|
): SetRelayInfoAction => ({
|
||||||
|
type: ActionTypes.SET_RELAY_INFO,
|
||||||
|
payload
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setMostPopularRelaysAction = (
|
||||||
|
payload: string[]
|
||||||
|
): SetMostPopularRelaysAction => ({
|
||||||
|
type: ActionTypes.SET_MOST_POPULAR_RELAYS,
|
||||||
|
payload
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setRelayConnectionStatusAction = (
|
||||||
|
payload: RelayConnectionStatus
|
||||||
|
): SetRelayConnectionStatusAction => ({
|
||||||
|
type: ActionTypes.SET_RELAY_CONNECTION_STATUS,
|
||||||
|
payload
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setRelayMapUpdatedAction = (): SetRelayMapUpdatedAction => ({
|
||||||
|
type: ActionTypes.SET_RELAY_MAP_UPDATED
|
||||||
|
})
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import * as ActionTypes from '../actionTypes'
|
import * as ActionTypes from '../actionTypes'
|
||||||
import { RelaysDispatchTypes, RelaysState } from './types'
|
import { RelaysDispatchTypes, RelaysState } from './types'
|
||||||
|
|
||||||
const initialState: RelaysState | null = null
|
const initialState: RelaysState = {
|
||||||
|
map: undefined,
|
||||||
|
mapUpdated: undefined,
|
||||||
|
mostPopular: undefined,
|
||||||
|
info: undefined,
|
||||||
|
connectionStatus: undefined
|
||||||
|
}
|
||||||
|
|
||||||
const reducer = (
|
const reducer = (
|
||||||
state = initialState,
|
state = initialState,
|
||||||
@ -9,7 +15,25 @@ const reducer = (
|
|||||||
): RelaysState | null => {
|
): RelaysState | null => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.SET_RELAY_MAP:
|
case ActionTypes.SET_RELAY_MAP:
|
||||||
return { map: action.payload, mapUpdated: Date.now() }
|
return { ...state, map: action.payload, mapUpdated: Date.now() }
|
||||||
|
|
||||||
|
case ActionTypes.SET_RELAY_MAP_UPDATED:
|
||||||
|
return { ...state, mapUpdated: Date.now() }
|
||||||
|
|
||||||
|
case ActionTypes.SET_RELAY_INFO:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
info: { ...state.info, ...action.payload }
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.SET_RELAY_CONNECTION_STATUS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
connectionStatus: action.payload
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionTypes.SET_MOST_POPULAR_RELAYS:
|
||||||
|
return { ...state, mostPopular: action.payload }
|
||||||
|
|
||||||
case ActionTypes.RESTORE_STATE:
|
case ActionTypes.RESTORE_STATE:
|
||||||
return action.payload.relays
|
return action.payload.relays
|
||||||
|
@ -1,12 +1,43 @@
|
|||||||
import * as ActionTypes from '../actionTypes'
|
import * as ActionTypes from '../actionTypes'
|
||||||
import { RestoreState } from '../actions'
|
import { RestoreState } from '../actions'
|
||||||
import { RelayMap } from '../../types'
|
import { RelayMap, RelayInfoObject, RelayConnectionStatus } from '../../types'
|
||||||
|
|
||||||
export type RelaysState = { map: RelayMap; mapUpdated: number }
|
export type RelaysState = {
|
||||||
|
map?: RelayMap
|
||||||
|
mapUpdated?: number
|
||||||
|
mostPopular?: string[]
|
||||||
|
info?: RelayInfoObject
|
||||||
|
connectionStatus?: RelayConnectionStatus
|
||||||
|
}
|
||||||
|
|
||||||
export interface SetRelayMapAction {
|
export interface SetRelayMapAction {
|
||||||
type: typeof ActionTypes.SET_RELAY_MAP
|
type: typeof ActionTypes.SET_RELAY_MAP
|
||||||
payload: RelayMap
|
payload: RelayMap
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RelaysDispatchTypes = SetRelayMapAction | RestoreState
|
export interface SetMostPopularRelaysAction {
|
||||||
|
type: typeof ActionTypes.SET_MOST_POPULAR_RELAYS
|
||||||
|
payload: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetRelayInfoAction {
|
||||||
|
type: typeof ActionTypes.SET_RELAY_INFO
|
||||||
|
payload: RelayInfoObject
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetRelayConnectionStatusAction {
|
||||||
|
type: typeof ActionTypes.SET_RELAY_CONNECTION_STATUS
|
||||||
|
payload: RelayConnectionStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetRelayMapUpdatedAction {
|
||||||
|
type: typeof ActionTypes.SET_RELAY_MAP_UPDATED
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RelaysDispatchTypes =
|
||||||
|
| SetRelayMapAction
|
||||||
|
| SetRelayInfoAction
|
||||||
|
| SetRelayMapUpdatedAction
|
||||||
|
| SetMostPopularRelaysAction
|
||||||
|
| SetRelayConnectionStatusAction
|
||||||
|
| RestoreState
|
||||||
|
@ -6,6 +6,7 @@ import metadataReducer from './metadata/reducer'
|
|||||||
import userRobotImageReducer from './userRobotImage/reducer'
|
import userRobotImageReducer from './userRobotImage/reducer'
|
||||||
import { RelaysState } from './relays/types'
|
import { RelaysState } from './relays/types'
|
||||||
import relaysReducer from './relays/reducer'
|
import relaysReducer from './relays/reducer'
|
||||||
|
import * as ActionTypes from './actionTypes'
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
auth: AuthState
|
auth: AuthState
|
||||||
@ -14,9 +15,20 @@ export interface State {
|
|||||||
relays: RelaysState
|
relays: RelaysState
|
||||||
}
|
}
|
||||||
|
|
||||||
export default combineReducers({
|
export const appReducer = combineReducers({
|
||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
metadata: metadataReducer,
|
metadata: metadataReducer,
|
||||||
userRobotImage: userRobotImageReducer,
|
userRobotImage: userRobotImageReducer,
|
||||||
relays: relaysReducer
|
relays: relaysReducer
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// FIXME: define types
|
||||||
|
export default (state: any, action: any) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.USER_LOGOUT:
|
||||||
|
return appReducer(undefined, action)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return appReducer(state, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,3 +2,4 @@ export * from './core'
|
|||||||
export * from './nostr'
|
export * from './nostr'
|
||||||
export * from './profile'
|
export * from './profile'
|
||||||
export * from './zip'
|
export * from './zip'
|
||||||
|
export * from './relay'
|
||||||
|
@ -8,19 +8,7 @@ export interface SignedEvent {
|
|||||||
sig: string
|
sig: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RelaySet {
|
|
||||||
read: string[]
|
|
||||||
write: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NostrJoiningBlock {
|
export interface NostrJoiningBlock {
|
||||||
block: number
|
block: number
|
||||||
encodedEventPointer: string
|
encodedEventPointer: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RelayMap = {
|
|
||||||
[key: string]: {
|
|
||||||
read: boolean
|
|
||||||
write: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
232
src/types/relay.ts
Normal file
232
src/types/relay.ts
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
export interface RelaySet {
|
||||||
|
read: string[]
|
||||||
|
write: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RelayMap = {
|
||||||
|
[key: string]: {
|
||||||
|
read: boolean
|
||||||
|
write: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelayStats {
|
||||||
|
relays: number
|
||||||
|
pubKeys: number
|
||||||
|
users: number
|
||||||
|
trusted_users: number
|
||||||
|
events: number
|
||||||
|
posts: number
|
||||||
|
zaps: number
|
||||||
|
zap_amount: number
|
||||||
|
daily: Daily
|
||||||
|
daily_totals: DailyTotals
|
||||||
|
relay_stats: RelayStats
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelayStats {
|
||||||
|
user_picks: UserPicks
|
||||||
|
written: Written
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Written {
|
||||||
|
last_week: LastWeek[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LastWeek {
|
||||||
|
d: string
|
||||||
|
p: number
|
||||||
|
ps: number
|
||||||
|
e: number
|
||||||
|
es: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserPicks {
|
||||||
|
read_relays: ReadRelay[]
|
||||||
|
write_relays: ReadRelay[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReadRelay {
|
||||||
|
d: string
|
||||||
|
r: number
|
||||||
|
w: number
|
||||||
|
rs: number
|
||||||
|
ws: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DailyTotals {
|
||||||
|
datasets: Datasets2
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Datasets2 {
|
||||||
|
kind_0: Kind0[]
|
||||||
|
kind_1: Kind0[]
|
||||||
|
kind_2: Kind0[]
|
||||||
|
kind_3: Kind0[]
|
||||||
|
kind_5: Kind0[]
|
||||||
|
kind_6: Kind0[]
|
||||||
|
kind_7: Kind0[]
|
||||||
|
kind_1984: Kind0[]
|
||||||
|
kind_9735: Kind0[]
|
||||||
|
kind_1063: Kind0[]
|
||||||
|
kind_6969: Kind0[]
|
||||||
|
kind_9802: Kind0[]
|
||||||
|
kind_30000: Kind0[]
|
||||||
|
kind_30001: Kind0[]
|
||||||
|
kind_30008: Kind0[]
|
||||||
|
kind_30009: Kind0[]
|
||||||
|
kind_30017: Kind0[]
|
||||||
|
kind_30018: Kind0[]
|
||||||
|
kind_30023: Kind0[]
|
||||||
|
kind_31337: Kind0[]
|
||||||
|
totals: Kind0[]
|
||||||
|
new_profiles: Kind0[]
|
||||||
|
new_pubkeys: Kind0[]
|
||||||
|
new_contact_lists: Kind0[]
|
||||||
|
new_ln: Kind0[]
|
||||||
|
new_users: Kind0[]
|
||||||
|
total_zap_amount: Kind0[]
|
||||||
|
zappers: Kind0[]
|
||||||
|
zapped_pubkeys: Kind0[]
|
||||||
|
zapped_events: Kind0[]
|
||||||
|
zap_providers: Kind0[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Daily {
|
||||||
|
datasets: Datasets
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Datasets {
|
||||||
|
kind_0: Kind0[]
|
||||||
|
kind_1: Kind0[]
|
||||||
|
kind_2: Kind0[]
|
||||||
|
kind_3: Kind0[]
|
||||||
|
kind_5: Kind0[]
|
||||||
|
kind_6: Kind0[]
|
||||||
|
kind_7: Kind0[]
|
||||||
|
kind_1984: Kind0[]
|
||||||
|
kind_9735: Kind0[]
|
||||||
|
kind_1063: Kind0[]
|
||||||
|
kind_6969: Kind0[]
|
||||||
|
kind_9802: Kind0[]
|
||||||
|
kind_30000: Kind0[]
|
||||||
|
kind_30001: Kind0[]
|
||||||
|
kind_30008: Kind0[]
|
||||||
|
kind_30009: Kind0[]
|
||||||
|
kind_30017: Kind0[]
|
||||||
|
kind_30018: Kind0[]
|
||||||
|
kind_30023: Kind0[]
|
||||||
|
kind_31337: Kind0[]
|
||||||
|
totals: Kind0[]
|
||||||
|
new_profiles: Kind0[]
|
||||||
|
new_pubkeys: Kind0[]
|
||||||
|
new_contact_lists: Kind0[]
|
||||||
|
new_ln: Kind0[]
|
||||||
|
new_users: Kind0[]
|
||||||
|
max_zap_amount: Kind0[]
|
||||||
|
avg_zap_amount: Kind0[]
|
||||||
|
total_zap_amount: Kind0[]
|
||||||
|
active_pubkeys: Kind0[]
|
||||||
|
active_pubkeys_total: Kind0[]
|
||||||
|
active_pubkeys_week: Kind0[]
|
||||||
|
active_pubkeys_total_week: Kind0[]
|
||||||
|
active_relays: Kind0[]
|
||||||
|
zappers: Kind0[]
|
||||||
|
zapped_pubkeys: Kind0[]
|
||||||
|
zapped_events: Kind0[]
|
||||||
|
zap_providers: Kind0[]
|
||||||
|
retention: Retention
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Retention {
|
||||||
|
all: All[]
|
||||||
|
tr: All[]
|
||||||
|
bio: All[]
|
||||||
|
all_curves: Allcurve[]
|
||||||
|
tr_curves: Allcurve[]
|
||||||
|
bio_curves: Allcurve[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Allcurve {
|
||||||
|
day: number
|
||||||
|
'2023-02': number
|
||||||
|
'2023-03': number
|
||||||
|
'2023-04': number
|
||||||
|
'2023-05': number
|
||||||
|
'2023-06': number
|
||||||
|
'2023-07': number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface All {
|
||||||
|
d: string
|
||||||
|
signups: number
|
||||||
|
retained: number
|
||||||
|
retd_posts: number
|
||||||
|
retd_replies: number
|
||||||
|
retd_reposts: number
|
||||||
|
retd_likes: number
|
||||||
|
retd_liked: number
|
||||||
|
retd_liked_pubkeys: number
|
||||||
|
retd_replied: number
|
||||||
|
retd_replied_pubkeys: number
|
||||||
|
retd_zaps_received: number
|
||||||
|
retd_zaps_received_msats: number
|
||||||
|
retd_zaps_sent: number
|
||||||
|
retd_zaps_sent_msats: number
|
||||||
|
retd_following: number
|
||||||
|
retd_followers: number
|
||||||
|
lost_posts: number
|
||||||
|
lost_replies: number
|
||||||
|
lost_reposts: number
|
||||||
|
lost_likes: number
|
||||||
|
lost_liked: number
|
||||||
|
lost_liked_pubkeys: number
|
||||||
|
lost_replied: number
|
||||||
|
lost_replied_pubkeys: number
|
||||||
|
lost_zaps_received: number
|
||||||
|
lost_zaps_received_msats: number
|
||||||
|
lost_zaps_sent: number
|
||||||
|
lost_zaps_sent_msats: number
|
||||||
|
lost_following: number
|
||||||
|
lost_followers: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Kind0 {
|
||||||
|
d: string
|
||||||
|
c: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelayFee {
|
||||||
|
amount: number
|
||||||
|
unit: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelayInfo {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
pubkey: string
|
||||||
|
contact: string
|
||||||
|
supported_nips: number[]
|
||||||
|
software: string
|
||||||
|
version: string
|
||||||
|
limitation?: { [key: string]: number | boolean }
|
||||||
|
fees?: { [key: string]: RelayFee[] }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelayInfoObject {
|
||||||
|
[key: string]: RelayInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelayInfoItem {
|
||||||
|
uri: string
|
||||||
|
info: RelayInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RelayConnectionState {
|
||||||
|
Connected = 'Connected',
|
||||||
|
NotConnected = 'Failed to connect'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelayConnectionStatus {
|
||||||
|
[key: string]: RelayConnectionState
|
||||||
|
}
|
@ -78,3 +78,11 @@ export const parseJson = <T>(content: string): Promise<T> => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capitalizes the first character in the string
|
||||||
|
* @param str string to modify
|
||||||
|
* @returns modified string
|
||||||
|
*/
|
||||||
|
export const capitalizeFirstLetter = (str: string) =>
|
||||||
|
str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
|
||||||
|
Loading…
Reference in New Issue
Block a user