diff --git a/lib/actions/backup-actions.js b/lib/actions/backup-actions.js --- a/lib/actions/backup-actions.js +++ b/lib/actions/backup-actions.js @@ -27,6 +27,7 @@ const resetBackupRestoreStateActionType = 'RESET_BACKUP_RESTOTE_STATE'; const markBackupAsRestoredActionType = 'MARK_BACKUP_AS_RESTORED'; +const restorationNotifyPeersActionType = 'RESTORATION_NOTIFY_PEERS'; const sendBackupDataToSecondaryActionTypes = Object.freeze({ started: 'SEND_BACKUP_DATA_TO_SECONDARY_STARTED', @@ -41,4 +42,5 @@ resetBackupRestoreStateActionType, markBackupAsRestoredActionType, sendBackupDataToSecondaryActionTypes, + restorationNotifyPeersActionType, }; diff --git a/lib/handlers/restore-backup-handler.react.js b/lib/handlers/restore-backup-handler.react.js --- a/lib/handlers/restore-backup-handler.react.js +++ b/lib/handlers/restore-backup-handler.react.js @@ -2,27 +2,71 @@ import * as React from 'react'; +import { restorationNotifyPeersActionType } from '../actions/backup-actions.js'; import { useUserDataRestoreContext } from '../backup/user-data-restore-context.js'; +import { useBroadcastDeviceListUpdates } from '../hooks/peer-list-hooks.js'; import { useDeviceKind } from '../hooks/primary-device-hooks.js'; import { usePersistedStateLoaded } from '../selectors/app-state-selectors.js'; -import { useSelector } from '../utils/redux-utils.js'; +import { getForeignPeerDeviceIDs } from '../selectors/user-selectors.js'; +import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; +import { useSelector, useDispatch } from '../utils/redux-utils.js'; import { fullBackupSupport } from '../utils/services-utils.js'; function RestoreBackupHandler(): React.Node { const persistedStateLoaded = usePersistedStateLoaded(); - // We want this handler to be executed only once - const executed = React.useRef(false); const restoreBackupState = useSelector(state => state.restoreBackupState); const userID = useSelector(state => state.currentUserInfo?.id); const accessToken = useSelector(state => state.commServicesAccessToken); const { userDataRestore } = useUserDataRestoreContext(); const deviceKind = useDeviceKind(); + const dispatch = useDispatch(); + const peerDevices = useSelector(getForeignPeerDeviceIDs); + const broadcastDeviceListUpdates = useBroadcastDeviceListUpdates(); + const { socketState } = useTunnelbroker(); + + // - Peer notify handler + const shouldNotifyPeers = + restoreBackupState.status === 'user_data_restore_completed' && + !restoreBackupState.payload.peersNotified; + React.useEffect(() => { + if ( + !fullBackupSupport || + !shouldNotifyPeers || + !persistedStateLoaded || + !socketState.isAuthorized || + deviceKind === 'unknown' + ) { + return; + } + + void (async () => { + // only primary device should broadcast device list update + if (deviceKind === 'primary') { + await broadcastDeviceListUpdates(peerDevices); + } + + // dispatch success to stop re-triggering this effect + dispatch({ type: restorationNotifyPeersActionType }); + })(); + }, [ + broadcastDeviceListUpdates, + deviceKind, + dispatch, + peerDevices, + persistedStateLoaded, + shouldNotifyPeers, + socketState.isAuthorized, + ]); + + // - Restoration retry handler + // We want this handler to be executed only once + const restoreExecuted = React.useRef(false); React.useEffect(() => { if ( !fullBackupSupport || !persistedStateLoaded || - executed.current || + restoreExecuted.current || !userID || !accessToken || deviceKind === 'unknown' @@ -37,7 +81,7 @@ } void userDataRestore(deviceKind === 'primary', userID, accessToken); - executed.current = true; + restoreExecuted.current = true; }, [ accessToken, deviceKind, diff --git a/lib/reducers/backup-reducer.js b/lib/reducers/backup-reducer.js --- a/lib/reducers/backup-reducer.js +++ b/lib/reducers/backup-reducer.js @@ -9,6 +9,7 @@ markBackupAsRestoredActionType, resetBackupRestoreStateActionType, sendBackupDataToSecondaryActionTypes, + restorationNotifyPeersActionType, } from '../actions/backup-actions.js'; import { changeIdentityUserPasswordActionTypes, @@ -91,7 +92,24 @@ return { ...store, status: 'user_data_restore_completed', - payload: { forced: true }, + payload: { forced: true, peersNotified: false }, + }; + } else if (action.type === restorationNotifyPeersActionType) { + if (store.status !== 'user_data_restore_completed') { + console.error( + 'Peers notified about UserData restore,', + `but restore state was ${store.status}. Ignoring.`, + ); + return store; + } + + return { + ...store, + status: 'user_data_restore_completed', + payload: { + ...store.payload, + peersNotified: true, + }, }; } else if (action.type === restoreUserActionTypes.success) { invariant( @@ -146,7 +164,7 @@ if (step === 'copy_content_from_backup_db') { return { status: 'user_data_restore_completed', - payload: {}, + payload: { peersNotified: false }, }; } return { diff --git a/lib/types/backup-types.js b/lib/types/backup-types.js --- a/lib/types/backup-types.js +++ b/lib/types/backup-types.js @@ -154,5 +154,6 @@ +status: 'user_data_restore_completed', +payload: { +forced?: boolean, + +peersNotified: boolean, }, }; diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -1687,6 +1687,10 @@ +type: 'MARK_BACKUP_AS_RESTORED', +payload?: void, } + | { + +type: 'RESTORATION_NOTIFY_PEERS', + +payload?: void, + } | { +type: 'SEND_BACKUP_DATA_TO_SECONDARY_STARTED', +loadingInfo: LoadingInfo,