feat(Relays): improved relays page
This commit is contained in:
parent
d3d87be2c3
commit
8790a943c3
@ -6,12 +6,32 @@ 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 {
|
||||
RelayMap,
|
||||
RelayInfoObject,
|
||||
RelayFee,
|
||||
RelayConnectionState
|
||||
} from '../../types'
|
||||
import LogoutIcon from '@mui/icons-material/Logout'
|
||||
import { useAppSelector, useAppDispatch } from '../../hooks'
|
||||
import { compareObjects } from '../../utils'
|
||||
import { setRelayMapAction } from '../../store/actions'
|
||||
import {
|
||||
compareObjects,
|
||||
shorten,
|
||||
hexToNpub,
|
||||
capitalizeFirstLetter
|
||||
} from '../../utils'
|
||||
import {
|
||||
setRelayMapAction,
|
||||
setRelayMapUpdatedAction
|
||||
} from '../../store/actions'
|
||||
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 = () => {
|
||||
const nostrController = NostrController.getInstance()
|
||||
@ -26,6 +46,30 @@ export const RelaysPage = () => {
|
||||
const [relayMap, setRelayMap] = useState<RelayMap | undefined>(
|
||||
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(() => {
|
||||
let isMounted = false
|
||||
@ -42,7 +86,7 @@ export const RelaysPage = () => {
|
||||
if (
|
||||
!relaysState?.mapUpdated ||
|
||||
newRelayMap.mapUpdated > relaysState?.mapUpdated
|
||||
)
|
||||
) {
|
||||
if (
|
||||
!relaysState?.map ||
|
||||
!compareObjects(relaysState.map, newRelayMap)
|
||||
@ -50,6 +94,10 @@ export const RelaysPage = () => {
|
||||
setRelayMap(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
|
||||
) {
|
||||
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
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}, [dispatch, usersPubkey, relaysState, nostrController])
|
||||
}, [
|
||||
dispatch,
|
||||
usersPubkey,
|
||||
relaysState?.map,
|
||||
relaysState?.mapUpdated,
|
||||
nostrController,
|
||||
relaysConnectionStatus
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
// Display notification if an empty relay map has been received
|
||||
@ -83,26 +150,39 @@ export const RelaysPage = () => {
|
||||
|
||||
const handleLeaveRelay = async (relay: string) => {
|
||||
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()
|
||||
} 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)
|
||||
.publishRelayMap(relayMapCopy, usersPubkey, [relay])
|
||||
.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))
|
||||
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)
|
||||
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
|
||||
if (
|
||||
newRelayURI &&
|
||||
relayURI &&
|
||||
!/^wss:\/\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/.test(
|
||||
newRelayURI
|
||||
relayURI
|
||||
)
|
||||
) {
|
||||
if (relayURI !== webSocketPrefix) {
|
||||
setNewRelayURIerror(
|
||||
'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))
|
||||
|
||||
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)
|
||||
setNewRelayURI('')
|
||||
|
||||
dispatch(setRelayMapAction(relayMapCopy))
|
||||
|
||||
if (usersPubkey) {
|
||||
// Publish updated relay map
|
||||
const relayMapPublishingRes = await nostrController
|
||||
.publishRelayMap(relayMapCopy, usersPubkey)
|
||||
.catch((err) => handlePublishRelayMapError(err))
|
||||
nostrController.getRelayInfo([relayURI])
|
||||
|
||||
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 (
|
||||
<Box className={styles.container}>
|
||||
<Box>
|
||||
<Box className={styles.relayAddContainer}>
|
||||
<TextField
|
||||
label="Add new relay"
|
||||
value={newRelayURI}
|
||||
onBlur={() => handleTextFieldChange()}
|
||||
onChange={(e) => setNewRelayURI(e.target.value)}
|
||||
helperText={newRelayURIerror}
|
||||
error={!!newRelayURIerror}
|
||||
placeholder="wss://"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
{webSocketPrefix}
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
className={styles.relayURItextfield}
|
||||
/>
|
||||
<Button variant="contained" onClick={() => handleAddNewRelay()}>
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
<Box className={styles.sectionTitle}>
|
||||
<RouterIcon className={styles.sectionIcon} />
|
||||
@ -207,7 +340,32 @@ export const RelaysPage = () => {
|
||||
<Box className={styles.relay} key={`relay_${i}`}>
|
||||
<List>
|
||||
<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} />
|
||||
|
||||
<Box
|
||||
className={styles.leaveRelayContainer}
|
||||
onClick={() => handleLeaveRelay(relay)}
|
||||
@ -218,12 +376,132 @@ export const RelaysPage = () => {
|
||||
</ListItem>
|
||||
<Divider className={styles.relayDivider} />
|
||||
<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
|
||||
checked={relayMap[relay].write}
|
||||
onChange={(event) => handleRelayWriteChange(relay, event)}
|
||||
/>
|
||||
</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>
|
||||
</Box>
|
||||
))}
|
||||
|
@ -7,6 +7,13 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.relayAddContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sectionIcon {
|
||||
font-size: 30px;
|
||||
}
|
||||
@ -43,5 +50,57 @@
|
||||
gap: 10px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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