diff --git a/lib/reducers/keyserver-reducer.js b/lib/reducers/keyserver-reducer.js --- a/lib/reducers/keyserver-reducer.js +++ b/lib/reducers/keyserver-reducer.js @@ -86,12 +86,19 @@ keyserverStoreOperations: $ReadOnlyArray, } { if (action.type === addKeyserverActionType) { + const { keyserverAdminUserID, newKeyserverInfo } = action.payload; + if (state.keyserverInfos[keyserverAdminUserID]) { + return { + keyserverStore: state, + keyserverStoreOperations: [], + }; + } const replaceOperation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { - id: action.payload.keyserverAdminUserID, + id: keyserverAdminUserID, keyserverInfo: { - ...action.payload.newKeyserverInfo, + ...newKeyserverInfo, }, }, }; diff --git a/lib/reducers/master-reducer.js b/lib/reducers/master-reducer.js --- a/lib/reducers/master-reducer.js +++ b/lib/reducers/master-reducer.js @@ -29,7 +29,10 @@ import reduceTunnelbrokerDeviceToken from './tunnelbroker-device-token-reducer.js'; import { reduceCurrentUserInfo, reduceUserInfos } from './user-reducer.js'; import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; -import { addKeyserverActionType } from '../actions/keyserver-actions.js'; +import { + addKeyserverActionType, + removeKeyserverActionType, +} from '../actions/keyserver-actions.js'; import { fetchPendingUpdatesActionTypes } from '../actions/update-actions.js'; import { legacyLogInActionTypes, @@ -234,7 +237,9 @@ }; if ( - action.type === processDMOpsActionType && + (action.type === processDMOpsActionType || + action.type === addKeyserverActionType || + action.type === removeKeyserverActionType) && action.payload.outboundP2PMessages ) { storeOperations = { diff --git a/lib/shared/community-utils.js b/lib/shared/community-utils.js --- a/lib/shared/community-utils.js +++ b/lib/shared/community-utils.js @@ -3,7 +3,7 @@ import * as React from 'react'; import type { KeyserverOverride } from './invite-links.js'; -import { useIsKeyserverURLValid } from './keyserver-utils.js'; +import { useAddKeyserver, useIsKeyserverURLValid } from './keyserver-utils.js'; import { useThreadHasPermission } from './thread-utils.js'; import { permissionsAndAuthRelatedRequestTimeout } from './timeouts.js'; import { @@ -12,7 +12,6 @@ deleteFarcasterChannelTagActionTypes, useDeleteFarcasterChannelTag, } from '../actions/community-actions.js'; -import { addKeyserverActionType } from '../actions/keyserver-actions.js'; import { joinThreadActionTypes } from '../actions/thread-action-types.js'; import type { LinkStatus } from '../hooks/invite-links.js'; import { useJoinKeyserverThread } from '../hooks/thread-hooks.js'; @@ -281,6 +280,7 @@ threadID, ]); + const addKeyserver = useAddKeyserver(); React.useEffect(() => { void (async () => { if (!ongoingJoinData || step !== 'add_keyserver') { @@ -292,21 +292,16 @@ } const isValid = await isKeyserverURLValid(); - if (!isValid || !keyserverURL) { + if (!isValid || !keyserverURL || !keyserverID) { setLinkStatus?.('invalid'); ongoingJoinData.reject(); setOngoingJoinData(null); return; } - dispatch({ - type: addKeyserverActionType, - payload: { - keyserverAdminUserID: keyserverID, - newKeyserverInfo: defaultKeyserverInfo(keyserverURL), - }, - }); + await addKeyserver(keyserverID, defaultKeyserverInfo(keyserverURL)); })(); }, [ + addKeyserver, dispatch, isKeyserverKnown, isKeyserverURLValid, diff --git a/lib/shared/dm-ops/dm-op-utils.js b/lib/shared/dm-ops/dm-op-utils.js --- a/lib/shared/dm-ops/dm-op-utils.js +++ b/lib/shared/dm-ops/dm-op-utils.js @@ -49,6 +49,7 @@ } from '../../types/update-types.js'; import { getContentSigningKey } from '../../utils/crypto-utils.js'; import { useSelector, useDispatch } from '../../utils/redux-utils.js'; +import { IdentityClientContext } from '../identity-client-context.js'; import { type MessageNotifyType, messageNotifyTypes, @@ -62,17 +63,13 @@ import { getCreateThickRawThreadInfoInputFromThreadInfo } from '../threads/protocols/dm-thread-protocol.js'; function generateMessagesToPeers( - message: DMOperation, + plaintextMessage: string, peers: $ReadOnlyArray<{ +userID: string, +deviceID: string, }>, + supportsAutoRetry: boolean, ): $ReadOnlyArray { - const opMessage: DMOperationP2PMessage = { - type: userActionsP2PMessageTypes.DM_OPERATION, - op: message, - }; - const plaintext = JSON.stringify(opMessage); const outboundP2PMessages = []; for (const peer of peers) { const messageToPeer: OutboundP2PMessage = { @@ -80,10 +77,10 @@ deviceID: peer.deviceID, userID: peer.userID, timestamp: new Date().getTime().toString(), - plaintext, + plaintext: plaintextMessage, ciphertext: '', status: outboundP2PMessageStatuses.persisted, - supportsAutoRetry: dmOpSpecs[message.type].supportsAutoRetry, + supportsAutoRetry, }; outboundP2PMessages.push(messageToPeer); } @@ -248,7 +245,16 @@ const targetPeers = peerUserIDAndDeviceIDs.filter( peer => peer.deviceID !== thisDeviceID, ); - return generateMessagesToPeers(operation, targetPeers); + const opMessage: DMOperationP2PMessage = { + type: userActionsP2PMessageTypes.DM_OPERATION, + op: operation, + }; + const plaintext = JSON.stringify(opMessage); + return generateMessagesToPeers( + plaintext, + targetPeers, + dmOpSpecs[operation.type].supportsAutoRetry, + ); }, [allPeerUserIDAndDeviceIDs, getMissingPeers, utilities], ); @@ -448,9 +454,32 @@ ); } +function useGenerateP2PMessageToOwnDevices(): ( + plaintext: string, +) => Promise<$ReadOnlyArray> { + const allPeerUserIDAndDeviceIDs = useSelector(getAllPeerUserIDAndDeviceIDs); + const identityContext = React.useContext(IdentityClientContext); + invariant(identityContext, 'Identity context should be set'); + const { getAuthMetadata } = identityContext; + + return React.useCallback( + async (plaintext: string) => { + const { deviceID, userID } = await getAuthMetadata(); + + const targetPeers = allPeerUserIDAndDeviceIDs + .filter(peer => peer.userID === userID) + .filter(peer => peer.deviceID !== deviceID); + + return generateMessagesToPeers(plaintext, targetPeers, true); + }, + [allPeerUserIDAndDeviceIDs, getAuthMetadata], + ); +} + export { useCreateMessagesToPeersFromDMOp, useAddDMThreadMembers, getThreadUpdatesForNewMessages, useSendDMOperationUtils, + useGenerateP2PMessageToOwnDevices, }; diff --git a/lib/shared/keyserver-utils.js b/lib/shared/keyserver-utils.js --- a/lib/shared/keyserver-utils.js +++ b/lib/shared/keyserver-utils.js @@ -2,14 +2,23 @@ import * as React from 'react'; +import { useGenerateP2PMessageToOwnDevices } from './dm-ops/dm-op-utils.js'; import { useGetVersion, getVersionActionTypes, } from '../actions/device-actions.js'; +import { + addKeyserverActionType, + removeKeyserverActionType, +} from '../actions/keyserver-actions.js'; import { urlsToIDsSelector } from '../selectors/keyserver-selectors.js'; import type { VersionResponse } from '../types/device-types.js'; +import { type KeyserverInfo } from '../types/keyserver-types.js'; +import type { AddKeyserverP2PMessage } from '../types/tunnelbroker/syncing-peer-to-peer-message-types.js'; +import { userActionsP2PMessageTypes } from '../types/tunnelbroker/user-actions-peer-to-peer-message-types.js'; +import type { RemoveKeyserverMessage } from '../types/tunnelbroker/user-actions-peer-to-peer-message-types.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; -import { useSelector } from '../utils/redux-utils.js'; +import { useDispatch, useSelector } from '../utils/redux-utils.js'; function useIsKeyserverURLValid( keyserverURL?: string, @@ -64,4 +73,59 @@ }, [dispatchActionPromise, getVersionCall, keyserverURL]); } -export { useIsKeyserverURLValid }; +function useRemoveKeyserver(): (keyserverAdminUserID: string) => Promise { + const dispatch = useDispatch(); + const generateMessagesToPeers = useGenerateP2PMessageToOwnDevices(); + + return React.useCallback( + async (keyserverAdminUserID: string) => { + const removeKeyserverP2PMessage: RemoveKeyserverMessage = { + type: userActionsP2PMessageTypes.REMOVE_KEYSERVER, + keyserverAdminUserID, + }; + const plaintext = JSON.stringify(removeKeyserverP2PMessage); + const outboundP2PMessages = await generateMessagesToPeers(plaintext); + + dispatch({ + type: removeKeyserverActionType, + payload: { + keyserverAdminUserID, + outboundP2PMessages, + }, + }); + }, + [dispatch, generateMessagesToPeers], + ); +} + +function useAddKeyserver(): ( + keyserverAdminUserID: string, + newKeyserverInfo: KeyserverInfo, +) => Promise { + const dispatch = useDispatch(); + const generateMessagesToPeers = useGenerateP2PMessageToOwnDevices(); + + return React.useCallback( + async (keyserverAdminUserID: string, newKeyserverInfo: KeyserverInfo) => { + const addKeyserverP2PMessage: AddKeyserverP2PMessage = { + type: userActionsP2PMessageTypes.ADD_KEYSERVER, + keyserverAdminUserID, + urlPrefix: newKeyserverInfo.urlPrefix, + }; + const plaintext = JSON.stringify(addKeyserverP2PMessage); + const outboundP2PMessages = await generateMessagesToPeers(plaintext); + + dispatch({ + type: addKeyserverActionType, + payload: { + keyserverAdminUserID, + newKeyserverInfo, + outboundP2PMessages, + }, + }); + }, + [dispatch, generateMessagesToPeers], + ); +} + +export { useIsKeyserverURLValid, useRemoveKeyserver, useAddKeyserver }; diff --git a/lib/tunnelbroker/use-peer-to-peer-message-handler.js b/lib/tunnelbroker/use-peer-to-peer-message-handler.js --- a/lib/tunnelbroker/use-peer-to-peer-message-handler.js +++ b/lib/tunnelbroker/use-peer-to-peer-message-handler.js @@ -12,6 +12,10 @@ useResendP2PMessagesToNewPrimaryDevices, } from './use-resend-peer-to-peer-messages.js'; import { removePeerUsersActionType } from '../actions/aux-user-actions.js'; +import { + addKeyserverActionType, + removeKeyserverActionType, +} from '../actions/keyserver-actions.js'; import { invalidateTunnelbrokerDeviceTokenActionType } from '../actions/tunnelbroker-actions.js'; import { logOutActionTypes, useBaseLogOut } from '../actions/user-actions.js'; import { useUserDataRestoreContext } from '../backup/user-data-restore-context.js'; @@ -43,6 +47,7 @@ IdentityServiceClient, DeviceOlmInboundKeys, } from '../types/identity-service-types'; +import { defaultKeyserverInfo } from '../types/keyserver-types.js'; import { peerToPeerMessageTypes, type PeerToPeerMessage, @@ -271,6 +276,27 @@ } finally { await removeAndConfirmMessage(messageID, senderInfo.deviceID); } + } else if ( + userActionMessage.type === userActionsP2PMessageTypes.ADD_KEYSERVER + ) { + const { keyserverAdminUserID, urlPrefix } = userActionMessage; + dispatch({ + type: addKeyserverActionType, + payload: { + keyserverAdminUserID, + newKeyserverInfo: defaultKeyserverInfo(urlPrefix), + }, + }); + } else if ( + userActionMessage.type === userActionsP2PMessageTypes.REMOVE_KEYSERVER + ) { + const { keyserverAdminUserID } = userActionMessage; + dispatch({ + type: removeKeyserverActionType, + payload: { + keyserverAdminUserID, + }, + }); } else { console.warn( 'Unsupported P2P user action message:', diff --git a/lib/types/keyserver-types.js b/lib/types/keyserver-types.js --- a/lib/types/keyserver-types.js +++ b/lib/types/keyserver-types.js @@ -13,6 +13,7 @@ connectionInfoValidator, defaultConnectionInfo, } from './socket-types.js'; +import type { OutboundP2PMessage } from './sqlite-types.js'; import type { GlobalAccountUserInfo } from './user-types.js'; import { getConfig } from '../utils/config.js'; import { tShape, tPlatformDetails } from '../utils/validation-utils.js'; @@ -61,10 +62,12 @@ export type AddKeyserverPayload = { +keyserverAdminUserID: string, +newKeyserverInfo: KeyserverInfo, + +outboundP2PMessages?: $ReadOnlyArray, }; export type RemoveKeyserverPayload = { +keyserverAdminUserID: string, + +outboundP2PMessages?: $ReadOnlyArray, }; export const keyserverInfoValidator: TInterface = diff --git a/lib/types/tunnelbroker/syncing-peer-to-peer-message-types.js b/lib/types/tunnelbroker/syncing-peer-to-peer-message-types.js new file mode 100644 --- /dev/null +++ b/lib/types/tunnelbroker/syncing-peer-to-peer-message-types.js @@ -0,0 +1,42 @@ +// @flow + +import type { TInterface, TUnion } from 'tcomb'; +import t from 'tcomb'; + +import { tShape, tString } from '../../utils/validation-utils.js'; + +export const syncingP2PMessageTypes = Object.freeze({ + ADD_KEYSERVER: 'ADD_KEYSERVER', + REMOVE_KEYSERVER: 'REMOVE_KEYSERVER', +}); + +export type AddKeyserverP2PMessage = { + +type: 'ADD_KEYSERVER', + +keyserverAdminUserID: string, + +urlPrefix: string, +}; +export const addKeyserverP2PMessageValidator: TInterface = + tShape({ + type: tString(syncingP2PMessageTypes.ADD_KEYSERVER), + keyserverAdminUserID: t.String, + urlPrefix: t.String, + }); + +export type RemoveKeyserverP2PMessage = { + +type: 'REMOVE_KEYSERVER', + +keyserverAdminUserID: string, +}; +export const removeKeyserverP2PMessageValidator: TInterface = + tShape({ + type: tString(syncingP2PMessageTypes.REMOVE_KEYSERVER), + keyserverAdminUserID: t.String, + }); + +export type SyncingP2PMessage = + | AddKeyserverP2PMessage + | RemoveKeyserverP2PMessage; + +export const syncingP2PMessageValidator: TUnion = t.union([ + addKeyserverP2PMessageValidator, + removeKeyserverP2PMessageValidator, +]); diff --git a/lib/types/tunnelbroker/user-actions-peer-to-peer-message-types.js b/lib/types/tunnelbroker/user-actions-peer-to-peer-message-types.js --- a/lib/types/tunnelbroker/user-actions-peer-to-peer-message-types.js +++ b/lib/types/tunnelbroker/user-actions-peer-to-peer-message-types.js @@ -14,6 +14,8 @@ DM_OPERATION: 'DM_OPERATION', BACKUP_DATA: 'BACKUP_DATA', FARCASTER_CONNECTION_UPDATED: 'FARCASTER_CONNECTION_UPDATED', + ADD_KEYSERVER: 'ADD_KEYSERVER', + REMOVE_KEYSERVER: 'REMOVE_KEYSERVER', }); export type DeviceLogoutP2PMessage = { @@ -79,13 +81,37 @@ hasDCsToken: t.Boolean, }); +export type AddKeyserverMessage = { + +type: 'ADD_KEYSERVER', + +keyserverAdminUserID: string, + +urlPrefix: string, +}; +export const addKeyserverMessageValidator: TInterface = + tShape({ + type: tString(userActionsP2PMessageTypes.ADD_KEYSERVER), + keyserverAdminUserID: t.String, + urlPrefix: t.String, + }); + +export type RemoveKeyserverMessage = { + +type: 'REMOVE_KEYSERVER', + +keyserverAdminUserID: string, +}; +export const removeKeyserverMessageValidator: TInterface = + tShape({ + type: tString(userActionsP2PMessageTypes.REMOVE_KEYSERVER), + keyserverAdminUserID: t.String, + }); + export type UserActionP2PMessage = | DeviceLogoutP2PMessage | SecondaryDeviceLogoutP2PMessage | AccountDeletionP2PMessage | DMOperationP2PMessage | BackupDataP2PMessage - | FarcasterConnectionUpdated; + | FarcasterConnectionUpdated + | AddKeyserverMessage + | RemoveKeyserverMessage; export const userActionP2PMessageValidator: TUnion = t.union([ @@ -95,4 +121,6 @@ dmOperationP2PMessageValidator, backupDataP2PMessageValidator, farcasterConnectionUpdatedValidator, + addKeyserverMessageValidator, + removeKeyserverMessageValidator, ]); diff --git a/native/profile/add-keyserver.react.js b/native/profile/add-keyserver.react.js --- a/native/profile/add-keyserver.react.js +++ b/native/profile/add-keyserver.react.js @@ -4,12 +4,13 @@ import * as React from 'react'; import { View, Text } from 'react-native'; -import { addKeyserverActionType } from 'lib/actions/keyserver-actions.js'; -import { useIsKeyserverURLValid } from 'lib/shared/keyserver-utils.js'; +import { + useAddKeyserver, + useIsKeyserverURLValid, +} from 'lib/shared/keyserver-utils.js'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; import { defaultKeyserverInfo } from 'lib/types/keyserver-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; -import { useDispatch } from 'lib/utils/redux-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import TextInput from '../components/text-input.react.js'; @@ -40,8 +41,6 @@ function AddKeyserver(props: Props): React.Node { const { goBack, setOptions } = useNavigation(); - const dispatch = useDispatch(); - const staffCanSee = useStaffCanSee(); const currentUserID = useSelector(state => state.currentUserInfo?.id); @@ -59,6 +58,7 @@ const isKeyserverURLValidCallback = useIsKeyserverURLValid(urlInput); + const addKeyserver = useAddKeyserver(); const onPressSave = React.useCallback(async () => { if (!currentUserID || !urlInput) { return; @@ -80,17 +80,18 @@ setStatus(keyserverCheckStatusInactive); const newKeyserverInfo: KeyserverInfo = defaultKeyserverInfo(urlInput); - - dispatch({ - type: addKeyserverActionType, - payload: { - keyserverAdminUserID: keyserverVersionData.ownerID, - newKeyserverInfo, - }, - }); + if (keyserverVersionData.ownerID) { + await addKeyserver(keyserverVersionData.ownerID, newKeyserverInfo); + } goBack(); - }, [currentUserID, dispatch, goBack, isKeyserverURLValidCallback, urlInput]); + }, [ + currentUserID, + addKeyserver, + goBack, + isKeyserverURLValidCallback, + urlInput, + ]); const buttonDisabled = !urlInput || status.status === 'loading'; React.useEffect(() => { diff --git a/native/profile/keyserver-selection-bottom-sheet.react.js b/native/profile/keyserver-selection-bottom-sheet.react.js --- a/native/profile/keyserver-selection-bottom-sheet.react.js +++ b/native/profile/keyserver-selection-bottom-sheet.react.js @@ -6,7 +6,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { setCustomServerActionType } from 'lib/actions/custom-server-actions.js'; -import { removeKeyserverActionType } from 'lib/actions/keyserver-actions.js'; +import { useRemoveKeyserver } from 'lib/shared/keyserver-utils.js'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; import type { GlobalAccountUserInfo } from 'lib/types/user-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; @@ -96,13 +96,9 @@ const dispatch = useDispatch(); - const onDeleteKeyserver = React.useCallback(() => { - dispatch({ - type: removeKeyserverActionType, - payload: { - keyserverAdminUserID: keyserverAdminUserInfo.id, - }, - }); + const removeKeyserver = useRemoveKeyserver(); + const onDeleteKeyserver = React.useCallback(async () => { + await removeKeyserver(keyserverAdminUserInfo.id); if (staffCanSee) { dispatch({ @@ -116,6 +112,7 @@ keyserverAdminUserInfo.id, keyserverInfo.urlPrefix, staffCanSee, + removeKeyserver, ]); const onPressRemoveKeyserver = React.useCallback(() => { @@ -137,7 +134,7 @@ ); }, [onDeleteKeyserver]); - const removeKeyserver = React.useMemo(() => { + const removeKeyserverContent = React.useMemo(() => { if (keyserverInfo.connection.status !== 'connected') { return ( <> @@ -206,7 +203,7 @@ {keyserverInfo.urlPrefix} - {removeKeyserver} + {removeKeyserverContent} ); diff --git a/web/modals/keyserver-selection/add-keyserver-modal.react.js b/web/modals/keyserver-selection/add-keyserver-modal.react.js --- a/web/modals/keyserver-selection/add-keyserver-modal.react.js +++ b/web/modals/keyserver-selection/add-keyserver-modal.react.js @@ -2,13 +2,14 @@ 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 { + useAddKeyserver, + useIsKeyserverURLValid, +} from 'lib/shared/keyserver-utils.js'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; import { defaultKeyserverInfo } from 'lib/types/keyserver-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; -import { useDispatch } from 'lib/utils/redux-utils.js'; import css from './add-keyserver-modal.css'; import Button, { buttonThemes } from '../../components/button.react.js'; @@ -33,8 +34,6 @@ function AddKeyserverModal(): React.Node { const { popModal } = useModalContext(); - const dispatch = useDispatch(); - const staffCanSee = useStaffCanSee(); const currentUserID = useSelector(state => state.currentUserInfo?.id); @@ -54,6 +53,7 @@ ); const isKeyserverURLValidCallback = useIsKeyserverURLValid(keyserverURL); + const addKeyserver = useAddKeyserver(); const onClickAddKeyserver = React.useCallback(async () => { if (!currentUserID || !keyserverURL) { @@ -76,20 +76,15 @@ setStatus(keyserverCheckStatusInactive); const newKeyserverInfo: KeyserverInfo = defaultKeyserverInfo(keyserverURL); - - dispatch({ - type: addKeyserverActionType, - payload: { - keyserverAdminUserID: keyserverVersionData.ownerID, - newKeyserverInfo, - }, - }); + if (keyserverVersionData.ownerID) { + await addKeyserver(keyserverVersionData.ownerID, newKeyserverInfo); + } popModal(); }, [ currentUserID, - dispatch, keyserverURL, + addKeyserver, popModal, isKeyserverURLValidCallback, ]); diff --git a/web/modals/keyserver-selection/keyserver-selection-modal.react.js b/web/modals/keyserver-selection/keyserver-selection-modal.react.js --- a/web/modals/keyserver-selection/keyserver-selection-modal.react.js +++ b/web/modals/keyserver-selection/keyserver-selection-modal.react.js @@ -3,8 +3,8 @@ import * as React from 'react'; import { setCustomServerActionType } from 'lib/actions/custom-server-actions.js'; -import { removeKeyserverActionType } from 'lib/actions/keyserver-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; +import { useRemoveKeyserver } from 'lib/shared/keyserver-utils.js'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; import type { GlobalAccountUserInfo } from 'lib/types/user-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; @@ -45,13 +45,9 @@ const dispatch = useDispatch(); - const onDeleteKeyserver = React.useCallback(() => { - dispatch({ - type: removeKeyserverActionType, - payload: { - keyserverAdminUserID: keyserverAdminUserInfo.id, - }, - }); + const removeKeyserver = useRemoveKeyserver(); + const onDeleteKeyserver = React.useCallback(async () => { + await removeKeyserver(keyserverAdminUserInfo.id); if (staffCanSee) { dispatch({ @@ -66,6 +62,7 @@ dispatch, keyserverAdminUserInfo.id, keyserverInfo.urlPrefix, + removeKeyserver, staffCanSee, ]);