diff --git a/web/modals/keyserver-selection/add-keyserver-modal.css b/web/modals/keyserver-selection/add-keyserver-modal.css index bd5fe791f..69961d800 100644 --- a/web/modals/keyserver-selection/add-keyserver-modal.css +++ b/web/modals/keyserver-selection/add-keyserver-modal.css @@ -1,17 +1,22 @@ .container { padding: 32px; } .inputTitle { color: var(--text-background-primary-default); margin-bottom: 8px; } .buttonContainer { background-color: var(--modal-background-secondary-default); padding: 16px 32px; } .button { width: 100%; } + +.errorMessage { + color: var(--text-background-danger-default); + margin-top: 8px; +} diff --git a/web/modals/keyserver-selection/add-keyserver-modal.react.js b/web/modals/keyserver-selection/add-keyserver-modal.react.js index f90a0944d..95fc0cf72 100644 --- a/web/modals/keyserver-selection/add-keyserver-modal.react.js +++ b/web/modals/keyserver-selection/add-keyserver-modal.react.js @@ -1,89 +1,132 @@ // @flow import * as React from 'react'; import { addKeyserverActionType } from 'lib/actions/keyserver-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; +import { useIsKeyserverURLValid } from 'lib/shared/keyserver-utils.js'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import css from './add-keyserver-modal.css'; import Button, { buttonThemes } from '../../components/button.react.js'; import { useSelector } from '../../redux/redux-utils.js'; import { useStaffCanSee } from '../../utils/staff-utils.js'; import Input from '../input.react.js'; import Modal from '../modal.react.js'; function AddKeyserverModal(): React.Node { const { popModal } = useModalContext(); const dispatch = useDispatch(); const staffCanSee = useStaffCanSee(); const currentUserID = useSelector(state => state.currentUserInfo?.id); const customServer = useSelector(state => state.customServer); const [keyserverURL, setKeyserverURL] = React.useState( customServer && staffCanSee ? customServer : '', ); + const [showErrorMessage, setShowErrorMessage] = + React.useState(false); const onChangeKeyserverURL = React.useCallback( (event: SyntheticEvent) => setKeyserverURL(event.currentTarget.value), [], ); - const onClickAddKeyserver = React.useCallback(() => { + const isKeyserverURLValidCallback = useIsKeyserverURLValid(keyserverURL); + + const onClickAddKeyserver = React.useCallback(async () => { + setShowErrorMessage(false); + if (!currentUserID || !keyserverURL) { + return; + } + + const isKeyserverURLValid = await isKeyserverURLValidCallback(); + if (!isKeyserverURLValid) { + setShowErrorMessage(true); + return; + } + const newKeyserverInfo: KeyserverInfo = { cookie: null, updatesCurrentAsOf: 0, urlPrefix: keyserverURL, connection: defaultConnectionInfo, lastCommunicatedPlatformDetails: null, deviceToken: null, }; dispatch({ type: addKeyserverActionType, payload: { keyserverAdminUserID: currentUserID, newKeyserverInfo, }, }); popModal(); - }, [currentUserID, dispatch, keyserverURL, popModal]); + }, [ + currentUserID, + dispatch, + keyserverURL, + popModal, + isKeyserverURLValidCallback, + ]); + + const errorMessage = React.useMemo(() => { + if (!showErrorMessage) { + return null; + } + + return ( +
+ Cannot connect to keyserver. Please check the URL or your connection and + try again. +
+ ); + }, [showErrorMessage]); const addKeyserverModal = React.useMemo( () => (
Keyserver URL
+ {errorMessage}
), - [keyserverURL, onChangeKeyserverURL, onClickAddKeyserver, popModal], + [ + errorMessage, + keyserverURL, + onChangeKeyserverURL, + onClickAddKeyserver, + popModal, + ], ); return addKeyserverModal; } export default AddKeyserverModal; diff --git a/web/redux/action-types.js b/web/redux/action-types.js index d395cfa46..872728e1f 100644 --- a/web/redux/action-types.js +++ b/web/redux/action-types.js @@ -1,172 +1,184 @@ // @flow import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { extractKeyserverIDFromID } from 'lib/utils/action-utils.js'; import { useKeyserverCall } from 'lib/utils/keyserver-call.js'; import type { CallKeyserverEndpoint } from 'lib/utils/keyserver-call.js'; import type { URLInfo } from 'lib/utils/url-utils.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import type { ExcludedData, InitialReduxState, InitialReduxStateResponse, InitialKeyserverInfo, InitialReduxStateRequest, } from '../types/redux-types.js'; export const updateNavInfoActionType = 'UPDATE_NAV_INFO'; export const updateWindowDimensionsActionType = 'UPDATE_WINDOW_DIMENSIONS'; export const updateWindowActiveActionType = 'UPDATE_WINDOW_ACTIVE'; export const setInitialReduxState = 'SET_INITIAL_REDUX_STATE'; const getInitialReduxStateCallServerEndpointOptions = { timeout: 300000 }; type GetInitialReduxStateInput = { +urlInfo: URLInfo, +excludedData: ExcludedData, +allUpdatesCurrentAsOf: { +[keyserverID: string]: number, }, }; const getInitialReduxState = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: GetInitialReduxStateInput) => Promise) => async input => { const requests: { [string]: InitialReduxStateRequest } = {}; const { urlInfo, excludedData, allUpdatesCurrentAsOf } = input; const { thread, inviteSecret, ...rest } = urlInfo; const threadKeyserverID = thread ? extractKeyserverIDFromID(thread) : null; for (const keyserverID of allKeyserverIDs) { + // As of Nov 2023, the only validation we have for adding a new keyserver + // is we check if the keyserver URL is valid. This is not a very + // extensive check, and gives the user the feeling of a false sucesses + // when they add new keyservers to the keyserver store. ENG-5371 tracks + // the task for initialzing a proper connection with the newly added + // keyserver, and at that point we can make the validation checks + // for adding a new keyserver more extensive. However, for the time being + // we need to add this check below so that we aren't trying to make calls + // to nonexistant keyservers that are in our keyserver store. + if (keyserverID !== ashoatKeyserverID) { + continue; + } const clientUpdatesCurrentAsOf = allUpdatesCurrentAsOf[keyserverID]; const keyserverExcludedData: ExcludedData = { threadStore: !!excludedData.threadStore && !!clientUpdatesCurrentAsOf, }; if (keyserverID === threadKeyserverID) { requests[keyserverID] = { urlInfo, excludedData: keyserverExcludedData, clientUpdatesCurrentAsOf, }; } else { requests[keyserverID] = { urlInfo: rest, excludedData: keyserverExcludedData, clientUpdatesCurrentAsOf, }; } } const responses: { +[string]: InitialReduxStateResponse } = await callKeyserverEndpoint( 'get_initial_redux_state', requests, getInitialReduxStateCallServerEndpointOptions, ); const { currentUserInfo, userInfos, pushApiPublicKey, commServicesAccessToken, navInfo, } = responses[ashoatKeyserverID]; const dataLoaded = currentUserInfo && !currentUserInfo.anonymous; const actualizedCalendarQuery = { startDate: navInfo.startDate, endDate: navInfo.endDate, filters: defaultCalendarFilters, }; const entryStore = { daysToEntries: {}, entryInfos: {}, lastUserInteractionCalendar: 0, }; const threadStore = { threadInfos: {}, }; const messageStore = { currentAsOf: {}, local: {}, messages: {}, threads: {}, }; const inviteLinksStore = { links: {}, }; let keyserverInfos: { [keyserverID: string]: InitialKeyserverInfo } = {}; for (const keyserverID in responses) { entryStore.daysToEntries = { ...entryStore.daysToEntries, ...responses[keyserverID].entryStore.daysToEntries, }; entryStore.entryInfos = { ...entryStore.entryInfos, ...responses[keyserverID].entryStore.entryInfos, }; entryStore.lastUserInteractionCalendar = Math.max( entryStore.lastUserInteractionCalendar, responses[keyserverID].entryStore.lastUserInteractionCalendar, ); threadStore.threadInfos = { ...threadStore.threadInfos, ...responses[keyserverID].threadStore.threadInfos, }; messageStore.currentAsOf = { ...messageStore.currentAsOf, ...responses[keyserverID].messageStore.currentAsOf, }; messageStore.messages = { ...messageStore.messages, ...responses[keyserverID].messageStore.messages, }; messageStore.threads = { ...messageStore.threads, ...responses[keyserverID].messageStore.threads, }; inviteLinksStore.links = { ...inviteLinksStore.links, ...responses[keyserverID].inviteLinksStore.links, }; keyserverInfos = { ...keyserverInfos, [keyserverID]: responses[keyserverID].keyserverInfo, }; } return { navInfo: { ...navInfo, inviteSecret, }, currentUserInfo, entryStore, threadStore, userInfos, actualizedCalendarQuery, messageStore, dataLoaded, pushApiPublicKey, commServicesAccessToken, inviteLinksStore, keyserverInfos, }; }; function useGetInitialReduxState(): ( input: GetInitialReduxStateInput, ) => Promise { return useKeyserverCall(getInitialReduxState); } export { useGetInitialReduxState };