From 31d1630ab1fe5c46a13edf6366f58792e7b8f8e1 Mon Sep 17 00:00:00 2001 From: Stixx Date: Tue, 17 Dec 2024 21:08:51 +0100 Subject: [PATCH] feat: publish and read from relays a list of preferred file servers, run validation while adding new server --- docs/blossom-flow.drawio | 106 ++++++++ src/App.scss | 15 ++ src/pages/settings/Settings.tsx | 7 + src/pages/settings/servers/index.tsx | 256 +++++++++++++++++++ src/pages/settings/servers/style.module.scss | 111 ++++++++ src/routes/index.tsx | 1 + src/routes/util.tsx | 5 + src/types/core.ts | 1 + src/types/file-server.ts | 6 + src/types/index.ts | 1 + src/utils/file-servers.ts | 115 +++++++++ src/utils/nostr.ts | 4 +- 12 files changed, 626 insertions(+), 2 deletions(-) create mode 100644 docs/blossom-flow.drawio create mode 100644 src/pages/settings/servers/index.tsx create mode 100644 src/pages/settings/servers/style.module.scss create mode 100644 src/types/file-server.ts create mode 100644 src/utils/file-servers.ts diff --git a/docs/blossom-flow.drawio b/docs/blossom-flow.drawio new file mode 100644 index 0000000..1cfee04 --- /dev/null +++ b/docs/blossom-flow.drawio @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.scss b/src/App.scss index 29e7e8f..462b284 100644 --- a/src/App.scss +++ b/src/App.scss @@ -169,3 +169,18 @@ li { color: rgba(0, 0, 0, 0.25); font-size: 14px; } + +.settings-container { + width: 100%; + background: white; + padding: 15px; + border-radius: 5px; + box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + grid-gap: 15px; +} + +.text-center { + text-align: center; +} diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index 5acdd9c..3cfc592 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -2,6 +2,7 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle' import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos' import CachedIcon from '@mui/icons-material/Cached' import RouterIcon from '@mui/icons-material/Router' +import StorageIcon from '@mui/icons-material/Storage' import { ListItem, useTheme } from '@mui/material' import List from '@mui/material/List' import ListItemIcon from '@mui/material/ListItemIcon' @@ -74,6 +75,12 @@ export const SettingsPage = () => { {listItem('Relays')} + + + + + {listItem('Servers')} + diff --git a/src/pages/settings/servers/index.tsx b/src/pages/settings/servers/index.tsx new file mode 100644 index 0000000..a5fb842 --- /dev/null +++ b/src/pages/settings/servers/index.tsx @@ -0,0 +1,256 @@ +import styles from './style.module.scss' +import { Container } from '../../../components/Container' +import { Footer } from '../../../components/Footer/Footer.tsx' +import { + Box, + Button, + CircularProgress, + InputAdornment, + List, + ListItem, + ListItemText, + TextField +} from '@mui/material' +import StorageIcon from '@mui/icons-material/Storage' +import DeleteIcon from '@mui/icons-material/Delete' +import { useState } from 'react' +import { toast } from 'react-toastify' +import { FileServerMap, KeyboardCode } from '../../../types' +import { + getFileServers, + publishFileServer +} from '../../../utils/file-servers.ts' +import { useAppSelector } from '../../../hooks/store.ts' +import { useDidMount } from '../../../hooks' +import { SIGIT_BLOSSOM } from '../../../utils' +import axios from 'axios' +import { cloneDeep } from 'lodash' + +const protocol = 'https://' + +const errors = { + urlNotValid: + 'New server URL is not valid. Example of valid server URL: blossom.sigit.io' +} + +export const ServersPage = () => { + const usersPubkey = useAppSelector((state) => state.auth?.usersPubkey) + + const [newServerURL, setNewServerURL] = useState('') + const [newRelayURLerror, setNewRelayURLerror] = useState() + const [loadingServers, setLoadingServers] = useState(true) + + const [blossomServersMap, setBlossomServersMap] = useState({}) + + useDidMount(() => { + fetchFileServers() + }) + + const fetchFileServers = async () => { + if (usersPubkey) { + await getFileServers(usersPubkey).then((res) => { + if (res.map) { + if (Object.keys(res.map).length === 0) { + serverRequirementWarning() + } + + setBlossomServersMap(res.map) + } + }) + } else { + noUserKeyWarning() + } + + setLoadingServers(false) + } + + const noUserKeyWarning = () => toast.warning('No user key available.') + + const serverRequirementWarning = () => + toast.warning('At least one Blossom server is needed for SIGit to work.') + + const handleAddNewServer = async () => { + if (!newServerURL.length) { + setNewRelayURLerror(errors.urlNotValid) + return + } + + const serverURL = `${protocol}${newServerURL?.trim().replace(protocol, '')}` + if (!serverURL) return + + // Check if new server is a valid URL + if ( + !/^(https?:\/\/)(?!-)([A-Za-z0-9-]{1,63}\.)+[A-Za-z]{2,6}$/gim.test( + serverURL + ) + ) { + if (serverURL !== protocol) { + setNewRelayURLerror(errors.urlNotValid) + return + } + } + + if (Object.keys(blossomServersMap).includes(serverURL)) + return toast.warning('This server is already added.') + + const valid = await validateFileServer(serverURL).catch(() => null) + if (!valid) + return toast.warning( + `Server URL ${serverURL} does not seem to be a valid file server.` + ) + + setNewRelayURLerror('') + const tempBlossomServersMap = blossomServersMap + tempBlossomServersMap[serverURL] = { write: true, read: true } + setBlossomServersMap(tempBlossomServersMap) + setNewServerURL('') + + publishFileServersList(tempBlossomServersMap) + } + + const handleDeleteServer = (serverURL: string) => { + if ( + serverURL === SIGIT_BLOSSOM && + Object.keys(blossomServersMap).length === 1 + ) + return serverRequirementWarning() + + // Remove server from the list + const tempBlossomServersMap = cloneDeep(blossomServersMap) + delete tempBlossomServersMap[serverURL] + + setBlossomServersMap(tempBlossomServersMap) + // Publish new list to the relays + publishFileServersList(tempBlossomServersMap) + } + + const publishFileServersList = (fileServersMap: FileServerMap) => { + if (!usersPubkey) + return toast.warning( + 'No user key available, please reload and try again.' + ) + + publishFileServer(fileServersMap, usersPubkey) + .then((res) => { + toast.success(res) + }) + .catch((err) => { + toast.error(err) + }) + } + + const handleInputKeydown = (event: React.KeyboardEvent) => { + if ( + event.code === KeyboardCode.Enter || + event.code === KeyboardCode.NumpadEnter + ) { + event.preventDefault() + handleAddNewServer() + } + } + + /** + * Checks if the file server is up and valid + * For now check will just include sending a GET request and checking if + * returned HTML includes word `Blossom`. + * + * Probably later, there will be appropriate sepc universal to all file servers + * which would include some kind of "check" endpoint. + * @param serverURL + */ + const validateFileServer = (serverURL: string) => { + return new Promise((resolve, reject) => { + axios + .get(serverURL) + .then((res) => { + if (res && res.data?.toLowerCase().includes('blossom server')) { + resolve(true) + } else { + reject(false) + } + }) + .catch((err) => { + reject(err) + }) + }) + } + + return ( + + + setNewServerURL(e.target.value)} + helperText={newRelayURLerror} + error={!!newRelayURLerror} + InputProps={{ + startAdornment: ( + {protocol} + ) + }} + className={styles.serverURItextfield} + /> + + + + + YOUR BLOSSOM SERVERS + + + {loadingServers && ( +
+ +
+ )} + + {blossomServersMap && ( + + {Object.keys(blossomServersMap).map((key) => ( + + ))} + + )} +