feat(Relays): added logic to manage relays #63
@ -17,7 +17,11 @@ import Username from '../username'
|
||||
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { MetadataController, NostrController } from '../../controllers'
|
||||
import { appPublicRoutes, getProfileRoute } from '../../routes'
|
||||
import {
|
||||
appPublicRoutes,
|
||||
appPrivateRoutes,
|
||||
getProfileRoute
|
||||
} from '../../routes'
|
||||
import {
|
||||
clearAuthToken,
|
||||
clearState,
|
||||
@ -160,6 +164,16 @@ export const AppBar = () => {
|
||||
>
|
||||
Profile
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
navigate(appPrivateRoutes.relays)
|
||||
}}
|
||||
sx={{
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
Relays
|
||||
</MenuItem>
|
||||
<Link
|
||||
to={appPublicRoutes.help}
|
||||
target="_blank"
|
||||
|
@ -1,13 +1,18 @@
|
||||
import { EventTemplate } from 'nostr-tools'
|
||||
import { MetadataController, NostrController } from '.'
|
||||
import { setAuthState, setMetadataEvent } from '../store/actions'
|
||||
import {
|
||||
setAuthState,
|
||||
setMetadataEvent,
|
||||
setRelayMapAction
|
||||
} from '../store/actions'
|
||||
import store from '../store/store'
|
||||
import {
|
||||
base64DecodeAuthToken,
|
||||
base64EncodeSignedEvent,
|
||||
getAuthToken,
|
||||
getVisitedLink,
|
||||
saveAuthToken
|
||||
saveAuthToken,
|
||||
compareObjects
|
||||
} from '../utils'
|
||||
import { appPrivateRoutes } from '../routes'
|
||||
import { SignedEvent } from '../types'
|
||||
@ -30,7 +35,7 @@ export class AuthController {
|
||||
* @returns url to redirect if authentication successfull
|
||||
* or error if otherwise
|
||||
*/
|
||||
async authenticateAndFindMetadata(pubkey: string) {
|
||||
async authAndGetMetadataAndRelaysMap(pubkey: string) {
|
||||
const emptyMetadata = this.metadataController.getEmptyMetadataEvent()
|
||||
|
||||
this.metadataController
|
||||
@ -69,6 +74,27 @@ 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)
|
||||
|
||||
if (Object.keys(relayMap).length < 1) {
|
||||
// Navigate user to relays page
|
||||
return Promise.resolve(appPrivateRoutes.relays)
|
||||
}
|
||||
|
||||
store.dispatch(setRelayMapAction(relayMap.map))
|
||||
}
|
||||
|
||||
const visitedLink = getVisitedLink()
|
||||
|
||||
if (visitedLink) {
|
||||
|
@ -10,19 +10,22 @@ import {
|
||||
EventTemplate,
|
||||
SimplePool,
|
||||
UnsignedEvent,
|
||||
Filter,
|
||||
finalizeEvent,
|
||||
nip04,
|
||||
nip19
|
||||
nip19,
|
||||
kinds
|
||||
} from 'nostr-tools'
|
||||
import { EventEmitter } from 'tseep'
|
||||
import { updateNsecbunkerPubkey } from '../store/actions'
|
||||
import { AuthState, LoginMethods } from '../store/auth/types'
|
||||
import store from '../store/store'
|
||||
import { SignedEvent } from '../types'
|
||||
import { SignedEvent, RelayMap } from '../types'
|
||||
import { getNsecBunkerDelegatedKey, verifySignedEvent } from '../utils'
|
||||
|
||||
export class NostrController extends EventEmitter {
|
||||
private static instance: NostrController
|
||||
private specialMetadataRelay = 'wss://purplepag.es'
|
||||
|
||||
private bunkerNDK: NDK | undefined
|
||||
private remoteSigner: NDKNip46Signer | undefined
|
||||
@ -216,12 +219,16 @@ export class NostrController extends EventEmitter {
|
||||
|
||||
if (publishedRelays.length === 0) {
|
||||
const failedPublishes: any[] = []
|
||||
const fallbackRejectionReason =
|
||||
'Attempt to publish an event has been rejected with unknown reason.'
|
||||
|
||||
results.forEach((res, index) => {
|
||||
if (res.status === 'rejected') {
|
||||
failedPublishes.push({
|
||||
relay: relays[index],
|
||||
error: res.reason.message
|
||||
error: res.reason
|
||||
? res.reason.message || fallbackRejectionReason
|
||||
: fallbackRejectionReason
|
||||
})
|
||||
}
|
||||
})
|
||||
@ -374,4 +381,108 @@ export class NostrController extends EventEmitter {
|
||||
generateDelegatedKey = (): string => {
|
||||
return NDKPrivateKeySigner.generate().privateKey!
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides relay map.
|
||||
* @param npub - user's npub
|
||||
* @returns - promise that resolves into relay map and a timestamp when it has been updated.
|
||||
*/
|
||||
getRelayMap = async (
|
||||
npub: string
|
||||
): Promise<{ map: RelayMap; mapUpdated: number }> => {
|
||||
const mostPopularRelays = import.meta.env.VITE_MOST_POPULAR_RELAYS
|
||||
const hardcodedPopularRelays = (mostPopularRelays || '').split(' ')
|
||||
|
||||
const popularRelayURIs = [
|
||||
this.specialMetadataRelay,
|
||||
...hardcodedPopularRelays
|
||||
]
|
||||
|
||||
const pool = new SimplePool()
|
||||
|
||||
// More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md
|
||||
const eventFilter: Filter = {
|
||||
kinds: [kinds.RelayList],
|
||||
authors: [npub]
|
||||
}
|
||||
|
||||
const event = await pool.get(popularRelayURIs, eventFilter).catch((err) => {
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
if (event) {
|
||||
// Handle founded 10002 event
|
||||
const relaysMap: RelayMap = {}
|
||||
|
||||
// 'r' stands for 'relay'
|
||||
const relayTags = event.tags.filter((tag) => tag[0] === 'r')
|
||||
|
||||
relayTags.forEach((tag) => {
|
||||
const uri = tag[1]
|
||||
const relayType = tag[2]
|
||||
|
||||
// if 3rd element of relay tag is undefined, relay is WRITE and READ
|
||||
relaysMap[uri] = {
|
||||
write: relayType ? relayType === 'write' : true,
|
||||
read: relayType ? relayType === 'read' : true
|
||||
}
|
||||
})
|
||||
|
||||
return Promise.resolve({ map: relaysMap, mapUpdated: event.created_at })
|
||||
} else {
|
||||
return Promise.reject('User relays were not found.')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes relay map.
|
||||
* @param relayMap - relay map.
|
||||
* @param npub - user's npub.
|
||||
* @returns - promise that resolves into a string representing publishing result.
|
||||
*/
|
||||
publishRelayMap = async (
|
||||
relayMap: RelayMap,
|
||||
npub: string
|
||||
): Promise<string> => {
|
||||
const timestamp = Math.floor(Date.now() / 1000)
|
||||
const relayURIs = Object.keys(relayMap)
|
||||
|
||||
// More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md
|
||||
const tags: string[][] = relayURIs.map((relayURI) =>
|
||||
[
|
||||
'r',
|
||||
relayURI,
|
||||
relayMap[relayURI].read && relayMap[relayURI].write
|
||||
? ''
|
||||
: relayMap[relayURI].write
|
||||
? 'write'
|
||||
: 'read'
|
||||
].filter((value) => value !== '')
|
||||
)
|
||||
|
||||
const newRelayMapEvent: UnsignedEvent = {
|
||||
kind: kinds.RelayList,
|
||||
tags,
|
||||
content: '',
|
||||
pubkey: npub,
|
||||
created_at: timestamp
|
||||
}
|
||||
|
||||
const signedEvent = await this.signEvent(newRelayMapEvent)
|
||||
|
||||
let relaysToPublish = relayURIs
|
||||
|
||||
// If relay map is empty, use most popular relay URIs
|
||||
if (!relaysToPublish.length) {
|
||||
const mostPopularRelays = import.meta.env.VITE_MOST_POPULAR_RELAYS
|
||||
const hardcodedPopularRelays = (mostPopularRelays || '').split(' ')
|
||||
|
||||
relaysToPublish = [this.specialMetadataRelay, ...hardcodedPopularRelays]
|
||||
}
|
||||
|
||||
await this.publishEvent(signedEvent, relaysToPublish)
|
||||
|
||||
return Promise.resolve(
|
||||
`Relay Map published on: ${relaysToPublish.join('\n')}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
1
src/hooks/index.ts
Normal file
1
src/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './store'
|
6
src/hooks/store.ts
Normal file
6
src/hooks/store.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import type { Dispatch, RootState } from '../store/store'
|
||||
|
||||
// Use instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch = useDispatch.withTypes<Dispatch>()
|
||||
export const useAppSelector = useSelector.withTypes<RootState>()
|
@ -15,7 +15,8 @@ store.subscribe(
|
||||
saveState({
|
||||
auth: store.getState().auth,
|
||||
metadata: store.getState().metadata,
|
||||
userRobotImage: store.getState().userRobotImage
|
||||
userRobotImage: store.getState().userRobotImage,
|
||||
relays: store.getState().relays
|
||||
})
|
||||
}, 1000)
|
||||
)
|
||||
|
@ -79,7 +79,7 @@ export const Login = () => {
|
||||
|
||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||
const redirectPath =
|
||||
await authController.authenticateAndFindMetadata(pubkey)
|
||||
await authController.authAndGetMetadataAndRelaysMap(pubkey)
|
||||
|
||||
navigateAfterLogin(redirectPath)
|
||||
})
|
||||
@ -118,7 +118,7 @@ export const Login = () => {
|
||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||
|
||||
const redirectPath = await authController
|
||||
.authenticateAndFindMetadata(publickey)
|
||||
.authAndGetMetadataAndRelaysMap(publickey)
|
||||
.catch((err) => {
|
||||
toast.error('Error occurred in authentication: ' + err)
|
||||
return null
|
||||
@ -213,7 +213,7 @@ export const Login = () => {
|
||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||
|
||||
const redirectPath = await authController
|
||||
.authenticateAndFindMetadata(pubkey!)
|
||||
.authAndGetMetadataAndRelaysMap(pubkey!)
|
||||
.catch((err) => {
|
||||
toast.error('Error occurred in authentication: ' + err)
|
||||
return null
|
||||
@ -273,7 +273,7 @@ export const Login = () => {
|
||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||
|
||||
const redirectPath = await authController
|
||||
.authenticateAndFindMetadata(pubkey!)
|
||||
.authAndGetMetadataAndRelaysMap(pubkey!)
|
||||
.catch((err) => {
|
||||
toast.error('Error occurred in authentication: ' + err)
|
||||
return null
|
||||
|
234
src/pages/relays/index.tsx
Normal file
234
src/pages/relays/index.tsx
Normal file
@ -0,0 +1,234 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Box, List, ListItem, TextField } from '@mui/material'
|
||||
import RouterIcon from '@mui/icons-material/Router'
|
||||
import styles from './style.module.scss'
|
||||
import Switch from '@mui/material/Switch'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import { NostrController } from '../../controllers'
|
||||
import { RelayMap } from '../../types'
|
||||
import LogoutIcon from '@mui/icons-material/Logout'
|
||||
import { useAppSelector, useAppDispatch } from '../../hooks'
|
||||
import { compareObjects } from '../../utils'
|
||||
import { setRelayMapAction } from '../../store/actions'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
export const RelaysPage = () => {
|
||||
const nostrController = NostrController.getInstance()
|
||||
|
||||
const relaysState = useAppSelector((state) => state.relays)
|
||||
const usersPubkey = useAppSelector((state) => state.auth?.usersPubkey)
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const [newRelayURI, setNewRelayURI] = useState<string>()
|
||||
const [newRelayURIerror, setNewRelayURIerror] = useState<string>()
|
||||
const [relayMap, setRelayMap] = useState<RelayMap | undefined>(
|
||||
relaysState?.map
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = false
|
||||
|
||||
const fetchData = async () => {
|
||||
if (usersPubkey) {
|
||||
isMounted = true
|
||||
|
||||
// call async func to fetch relay map
|
||||
const newRelayMap = await nostrController.getRelayMap(usersPubkey)
|
||||
|
||||
// handle fetched relay map
|
||||
if (isMounted) {
|
||||
if (
|
||||
!relaysState?.mapUpdated ||
|
||||
newRelayMap.mapUpdated > relaysState?.mapUpdated
|
||||
)
|
||||
if (
|
||||
!relaysState?.map ||
|
||||
!compareObjects(relaysState.map, newRelayMap)
|
||||
) {
|
||||
setRelayMap(newRelayMap.map)
|
||||
|
||||
dispatch(setRelayMapAction(newRelayMap.map))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Publishing relay map can take some time.
|
||||
// This is why data fetch should happen only if relay map was received more than 5 minutes ago.
|
||||
if (
|
||||
usersPubkey &&
|
||||
(!relaysState?.mapUpdated ||
|
||||
Date.now() - relaysState?.mapUpdated > 5 * 60 * 1000) // 5 minutes
|
||||
) {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// cleanup func
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}, [dispatch, usersPubkey, relaysState, nostrController])
|
||||
|
||||
useEffect(() => {
|
||||
// Display notification if an empty relay map has been received
|
||||
if (relayMap && Object.keys(relayMap).length === 0) {
|
||||
relayRequirementWarning()
|
||||
}
|
||||
}, [relayMap])
|
||||
|
||||
const relayRequirementWarning = () =>
|
||||
toast.warning('At least one write relay is needed for SIGit to work.')
|
||||
|
||||
const handleLeaveRelay = async (relay: string) => {
|
||||
if (relayMap) {
|
||||
if (Object.keys(relayMap).length === 1) {
|
||||
relayRequirementWarning()
|
||||
} else {
|
||||
const relayMapCopy = JSON.parse(JSON.stringify(relayMap))
|
||||
// Remove relay from relay map
|
||||
delete relayMapCopy[relay]
|
||||
|
||||
setRelayMap(relayMapCopy)
|
||||
|
||||
dispatch(setRelayMapAction(relayMapCopy))
|
||||
|
||||
if (usersPubkey) {
|
||||
// Publish updated relay map.
|
||||
const relayMapPublishingRes = await nostrController
|
||||
.publishRelayMap(relayMapCopy, usersPubkey)
|
||||
.catch((err) => handlePublishRelayMapError(err))
|
||||
|
||||
if (relayMapPublishingRes) toast.success(relayMapPublishingRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handlePublishRelayMapError = (err: any) => {
|
||||
const errorPrefix = 'Error while publishing Relay Map'
|
||||
|
||||
if (Array.isArray(err)) {
|
||||
err.forEach((errorObj: { relay: string; error: string }) => {
|
||||
toast.error(
|
||||
`${errorPrefix} to ${errorObj.relay}. Error: ${errorObj.error || 'Unknown'}`
|
||||
)
|
||||
})
|
||||
} else {
|
||||
toast.error(`${errorPrefix}. Error: ${err.message || 'Unknown'}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRelayWriteChange = async (
|
||||
relay: string,
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (relayMap && relayMap[relay]) {
|
||||
if (
|
||||
!event.target.checked &&
|
||||
Object.keys(relayMap).filter((relay) => relayMap[relay].write)
|
||||
.length === 1
|
||||
) {
|
||||
relayRequirementWarning()
|
||||
} else {
|
||||
const relayMapCopy = JSON.parse(JSON.stringify(relayMap))
|
||||
relayMapCopy[relay].write = event.target.checked
|
||||
|
||||
setRelayMap(relayMapCopy)
|
||||
|
||||
dispatch(setRelayMapAction(relayMapCopy))
|
||||
|
||||
if (usersPubkey) {
|
||||
// Publish updated relay map
|
||||
const relayMapPublishingRes = await nostrController
|
||||
.publishRelayMap(relayMapCopy, usersPubkey)
|
||||
.catch((err) => handlePublishRelayMapError(err))
|
||||
|
||||
if (relayMapPublishingRes) toast.success(relayMapPublishingRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleTextFieldChange = async () => {
|
||||
// Check if new relay URI is a valid string
|
||||
if (
|
||||
newRelayURI &&
|
||||
!/^wss:\/\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/.test(
|
||||
newRelayURI
|
||||
)
|
||||
) {
|
||||
setNewRelayURIerror(
|
||||
'New relay URI is not valid. Example of valid relay URI: wss://sigit.relay.io'
|
||||
)
|
||||
} else if (newRelayURI) {
|
||||
const relayMapCopy = JSON.parse(JSON.stringify(relayMap))
|
||||
|
||||
relayMapCopy[newRelayURI.trim()] = { write: true, read: true }
|
||||
|
||||
setRelayMap(relayMapCopy)
|
||||
setNewRelayURI('')
|
||||
|
||||
dispatch(setRelayMapAction(relayMapCopy))
|
||||
|
||||
if (usersPubkey) {
|
||||
// Publish updated relay map
|
||||
const relayMapPublishingRes = await nostrController
|
||||
.publishRelayMap(relayMapCopy, usersPubkey)
|
||||
.catch((err) => handlePublishRelayMapError(err))
|
||||
|
||||
if (relayMapPublishingRes) toast.success(relayMapPublishingRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={styles.container}>
|
||||
<Box>
|
||||
<TextField
|
||||
label="Add new relay"
|
||||
value={newRelayURI}
|
||||
onBlur={() => handleTextFieldChange()}
|
||||
onChange={(e) => setNewRelayURI(e.target.value)}
|
||||
helperText={newRelayURIerror}
|
||||
error={!!newRelayURIerror}
|
||||
placeholder="wss://"
|
||||
className={styles.relayURItextfield}
|
||||
/>
|
||||
</Box>
|
||||
<Box className={styles.sectionTitle}>
|
||||
<RouterIcon className={styles.sectionIcon} />
|
||||
<span>YOUR RELAYS</span>
|
||||
</Box>
|
||||
{relayMap && (
|
||||
<Box className={styles.relaysContainer}>
|
||||
{Object.keys(relayMap).map((relay, i) => (
|
||||
<Box className={styles.relay} key={`relay_${i}`}>
|
||||
<List>
|
||||
m
commented
I suggest implementing an indication (green/red dot) if the relay actually works. Because relay can be:
I suggest implementing an indication (green/red dot) if the relay actually works. Because relay can be:
- Unreachable (down)
- Requiring payment
![image example](https://paste.4gl.io/?b27a689748f5d0e4#FPrJLQbXDrKv3CAZ7iaYEma4cKgTNGihXEpWprFdWvhw)
y
commented
Good suggestion! Good suggestion!
I think we should implement this feature later as it feels like nice to have, but is not crucial.
|
||||
<ListItem>
|
||||
<ListItemText primary={relay} />
|
||||
<Box
|
||||
className={styles.leaveRelayContainer}
|
||||
onClick={() => handleLeaveRelay(relay)}
|
||||
>
|
||||
<LogoutIcon />
|
||||
<span>Leave</span>
|
||||
</Box>
|
||||
</ListItem>
|
||||
<Divider className={styles.relayDivider} />
|
||||
<ListItem>
|
||||
<ListItemText primary="Publish to this relay?" />
|
||||
<Switch
|
||||
checked={relayMap[relay].write}
|
||||
onChange={(event) => handleRelayWriteChange(relay, event)}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
47
src/pages/relays/style.module.scss
Normal file
47
src/pages/relays/style.module.scss
Normal file
@ -0,0 +1,47 @@
|
||||
@import '../../colors.scss';
|
||||
|
||||
.container {
|
||||
margin-top: 25px;
|
||||
|
||||
.relayURItextfield {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sectionIcon {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
margin-top: 35px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.relaysContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.relay {
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 4px;
|
||||
|
||||
.relayDivider {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.leaveRelayContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,12 +6,14 @@ import { ProfilePage } from '../pages/profile'
|
||||
import { hexToNpub } from '../utils'
|
||||
import { SignPage } from '../pages/sign'
|
||||
import { VerifyPage } from '../pages/verify'
|
||||
import { RelaysPage } from '../pages/relays'
|
||||
|
||||
export const appPrivateRoutes = {
|
||||
homePage: '/',
|
||||
create: '/create',
|
||||
sign: '/sign',
|
||||
verify: '/verify'
|
||||
verify: '/verify',
|
||||
relays: '/relays'
|
||||
}
|
||||
|
||||
export const appPublicRoutes = {
|
||||
@ -57,5 +59,9 @@ export const privateRoutes = [
|
||||
{
|
||||
path: appPrivateRoutes.verify,
|
||||
element: <VerifyPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.relays,
|
||||
element: <RelaysPage />
|
||||
}
|
||||
]
|
||||
|
@ -9,3 +9,5 @@ export const UPDATE_NSECBUNKER_RELAYS = 'UPDATE_NSECBUNKER_RELAYS'
|
||||
export const SET_METADATA_EVENT = 'SET_METADATA_EVENT'
|
||||
|
||||
export const SET_USER_ROBOT_IMAGE = 'SET_USER_ROBOT_IMAGE'
|
||||
|
||||
export const SET_RELAY_MAP = 'SET_RELAY_MAP'
|
||||
|
@ -3,6 +3,7 @@ import { State } from './rootReducer'
|
||||
|
||||
export * from './auth/action'
|
||||
export * from './metadata/action'
|
||||
export * from './relays/action'
|
||||
|
||||
export const restoreState = (payload: State) => {
|
||||
return {
|
||||
|
8
src/store/relays/action.ts
Normal file
8
src/store/relays/action.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import * as ActionTypes from '../actionTypes'
|
||||
import { SetRelayMapAction } from './types'
|
||||
import { RelayMap } from '../../types'
|
||||
|
||||
export const setRelayMapAction = (payload: RelayMap): SetRelayMapAction => ({
|
||||
type: ActionTypes.SET_RELAY_MAP,
|
||||
payload
|
||||
})
|
22
src/store/relays/reducer.ts
Normal file
22
src/store/relays/reducer.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import * as ActionTypes from '../actionTypes'
|
||||
import { RelaysDispatchTypes, RelaysState } from './types'
|
||||
|
||||
const initialState: RelaysState | null = null
|
||||
|
||||
const reducer = (
|
||||
state = initialState,
|
||||
action: RelaysDispatchTypes
|
||||
): RelaysState | null => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_RELAY_MAP:
|
||||
return { map: action.payload, mapUpdated: Date.now() }
|
||||
|
||||
case ActionTypes.RESTORE_STATE:
|
||||
return action.payload.relays
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default reducer
|
12
src/store/relays/types.ts
Normal file
12
src/store/relays/types.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import * as ActionTypes from '../actionTypes'
|
||||
import { RestoreState } from '../actions'
|
||||
import { RelayMap } from '../../types'
|
||||
|
||||
export type RelaysState = { map: RelayMap; mapUpdated: number }
|
||||
|
||||
export interface SetRelayMapAction {
|
||||
type: typeof ActionTypes.SET_RELAY_MAP
|
||||
payload: RelayMap
|
||||
}
|
||||
|
||||
export type RelaysDispatchTypes = SetRelayMapAction | RestoreState
|
@ -4,15 +4,19 @@ import authReducer from './auth/reducer'
|
||||
import { AuthState } from './auth/types'
|
||||
import metadataReducer from './metadata/reducer'
|
||||
import userRobotImageReducer from './userRobotImage/reducer'
|
||||
import { RelaysState } from './relays/types'
|
||||
import relaysReducer from './relays/reducer'
|
||||
|
||||
export interface State {
|
||||
auth: AuthState
|
||||
metadata?: Event
|
||||
userRobotImage?: string
|
||||
relays: RelaysState
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
auth: authReducer,
|
||||
metadata: metadataReducer,
|
||||
userRobotImage: userRobotImageReducer
|
||||
userRobotImage: userRobotImageReducer,
|
||||
relays: relaysReducer
|
||||
})
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import rootReducer from './rootReducer'
|
||||
|
||||
const store = configureStore({ reducer: rootReducer })
|
||||
const store = configureStore({
|
||||
reducer: rootReducer
|
||||
})
|
||||
|
||||
export default store
|
||||
|
||||
export type Dispatch = typeof store.dispatch
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
|
@ -17,3 +17,10 @@ export interface NostrJoiningBlock {
|
||||
block: number
|
||||
encodedEventPointer: string
|
||||
}
|
||||
|
||||
export type RelayMap = {
|
||||
[key: string]: {
|
||||
read: boolean
|
||||
write: boolean
|
||||
}
|
||||
}
|
||||
|
@ -5,3 +5,4 @@ export * from './misc'
|
||||
export * from './nostr'
|
||||
export * from './string'
|
||||
export * from './zip'
|
||||
export * from './utils'
|
||||
|
13
src/utils/utils.ts
Normal file
13
src/utils/utils.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const compareObjects = (
|
||||
obj1: object | null | undefined,
|
||||
obj2: object | null | undefined
|
||||
): boolean => {
|
||||
if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
||||
const obj1Copy = [...obj1].sort()
|
||||
const obj2Copy = [...obj2].sort()
|
||||
|
||||
return JSON.stringify(obj1Copy) === JSON.stringify(obj2Copy)
|
||||
}
|
||||
|
||||
return JSON.stringify(obj1) === JSON.stringify(obj2)
|
||||
}
|
Loading…
Reference in New Issue
Block a user
Does this mean we will not use a https://stats.nostr.band/stats_api?method=stats service to fetch most popular relays?
do we already use such an approach to fetch relays?
In the
MetadataController
we also use env.We do have in the other project, sending you link in the DM