diff --git a/lib/actions/aux-user-actions.js b/lib/actions/aux-user-actions.js index 0151159da..8b0545d46 100644 --- a/lib/actions/aux-user-actions.js +++ b/lib/actions/aux-user-actions.js @@ -1,16 +1,19 @@ // @flow const setAuxUserFIDsActionType = 'SET_AUX_USER_FIDS'; const addAuxUserFIDsActionType = 'ADD_AUX_USER_FIDS'; const clearAuxUserFIDsActionType = 'CLEAR_AUX_USER_FIDS'; const setPeerDeviceListsActionType = 'SET_PEER_DEVICE_LISTS'; +const removePeerUsersActionType = 'REMOVE_PEER_USERS'; + export { setAuxUserFIDsActionType, addAuxUserFIDsActionType, clearAuxUserFIDsActionType, setPeerDeviceListsActionType, + removePeerUsersActionType, }; diff --git a/lib/reducers/aux-user-reducer.js b/lib/reducers/aux-user-reducer.js index 841214e89..adf92127c 100644 --- a/lib/reducers/aux-user-reducer.js +++ b/lib/reducers/aux-user-reducer.js @@ -1,197 +1,210 @@ // @flow import { setAuxUserFIDsActionType, addAuxUserFIDsActionType, clearAuxUserFIDsActionType, + removePeerUsersActionType, setPeerDeviceListsActionType, } from '../actions/aux-user-actions.js'; import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; import { auxUserStoreOpsHandlers, type AuxUserStoreOperation, type RemoveAuxUserInfosOperation, type ReplaceAuxUserInfoOperation, } from '../ops/aux-user-store-ops.js'; import type { AuxUserStore } from '../types/aux-user-types.js'; import type { BaseAction } from '../types/redux-types'; import { serverRequestTypes, processServerRequestsActionType, } from '../types/request-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { relyingOnAuthoritativeKeyserver } from '../utils/services-utils.js'; const { processStoreOperations: processStoreOps } = auxUserStoreOpsHandlers; function reduceAuxUserStore( state: AuxUserStore, action: BaseAction, ): { +auxUserStore: AuxUserStore, +auxUserStoreOperations: $ReadOnlyArray, } { if (action.type === setAuxUserFIDsActionType) { const toUpdateUserIDs = new Set( action.payload.farcasterUsers.map(farcasterUser => farcasterUser.userID), ); const replaceOperations: ReplaceAuxUserInfoOperation[] = []; for (const userID in state.auxUserInfos) { if ( state.auxUserInfos[userID].fid !== null && !toUpdateUserIDs.has(userID) ) { replaceOperations.push({ type: 'replace_aux_user_info', payload: { id: userID, auxUserInfo: { ...state.auxUserInfos[userID], fid: null, }, }, }); } } for (const farcasterUser of action.payload.farcasterUsers) { replaceOperations.push({ type: 'replace_aux_user_info', payload: { id: farcasterUser.userID, auxUserInfo: { ...state.auxUserInfos[farcasterUser.userID], fid: farcasterUser.farcasterID, }, }, }); } return { auxUserStore: processStoreOps(state, replaceOperations), auxUserStoreOperations: replaceOperations, }; } else if (action.type === addAuxUserFIDsActionType) { const replaceOperations: ReplaceAuxUserInfoOperation[] = []; for (const farcasterUser of action.payload.farcasterUsers) { replaceOperations.push({ type: 'replace_aux_user_info', payload: { id: farcasterUser.userID, auxUserInfo: { ...state.auxUserInfos[farcasterUser.userID], fid: farcasterUser.farcasterID, }, }, }); } return { auxUserStore: processStoreOps(state, replaceOperations), auxUserStoreOperations: replaceOperations, }; } else if (action.type === clearAuxUserFIDsActionType) { const replaceOperations: ReplaceAuxUserInfoOperation[] = []; for (const userID in state.auxUserInfos) { if (state.auxUserInfos[userID].fid !== null) { replaceOperations.push({ type: 'replace_aux_user_info', payload: { id: userID, auxUserInfo: { ...state.auxUserInfos[userID], fid: null, }, }, }); } } return { auxUserStore: processStoreOps(state, replaceOperations), auxUserStoreOperations: replaceOperations, }; } else if (action.type === setClientDBStoreActionType) { const newAuxUserInfos = action.payload.auxUserInfos; if (!newAuxUserInfos) { return { auxUserStore: state, auxUserStoreOperations: [], }; } const newAuxUserStore: AuxUserStore = { ...state, auxUserInfos: newAuxUserInfos, }; return { auxUserStore: newAuxUserStore, auxUserStoreOperations: [], }; } else if (action.type === setPeerDeviceListsActionType) { const replaceOperations: ReplaceAuxUserInfoOperation[] = []; for (const userID in action.payload.deviceLists) { replaceOperations.push({ type: 'replace_aux_user_info', payload: { id: userID, auxUserInfo: { ...state.auxUserInfos[userID], fid: state.auxUserInfos[userID]?.fid ?? null, deviceList: action.payload.deviceLists[userID], devicesPlatformDetails: action.payload.usersPlatformDetails[userID], }, }, }); } return { auxUserStore: processStoreOps(state, replaceOperations), auxUserStoreOperations: replaceOperations, }; } else if ( action.type === processServerRequestsActionType && relyingOnAuthoritativeKeyserver ) { if (action.payload.keyserverID !== authoritativeKeyserverID()) { return { auxUserStore: state, auxUserStoreOperations: [], }; } const checkStateRequest = action.payload.serverRequests.find( candidate => candidate.type === serverRequestTypes.CHECK_STATE, ); if (!checkStateRequest || !checkStateRequest.stateChanges) { return { auxUserStore: state, auxUserStoreOperations: [], }; } const { deleteUserInfoIDs } = checkStateRequest.stateChanges; if (!deleteUserInfoIDs) { return { auxUserStore: state, auxUserStoreOperations: [], }; } const removeUsersOps: RemoveAuxUserInfosOperation[] = []; if (deleteUserInfoIDs) { removeUsersOps.push({ type: 'remove_aux_user_infos', payload: { ids: deleteUserInfoIDs }, }); } + return { + auxUserStore: processStoreOps(state, removeUsersOps), + auxUserStoreOperations: removeUsersOps, + }; + } else if (action.type === removePeerUsersActionType) { + const removeUsersOps: RemoveAuxUserInfosOperation[] = [ + { + type: 'remove_aux_user_infos', + payload: { ids: action.payload.userIDs }, + }, + ]; + return { auxUserStore: processStoreOps(state, removeUsersOps), auxUserStoreOperations: removeUsersOps, }; } return { auxUserStore: state, auxUserStoreOperations: [], }; } export { reduceAuxUserStore }; diff --git a/lib/reducers/user-reducer.js b/lib/reducers/user-reducer.js index 392845abf..2d2fcf841 100644 --- a/lib/reducers/user-reducer.js +++ b/lib/reducers/user-reducer.js @@ -1,565 +1,584 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import _keyBy from 'lodash/fp/keyBy.js'; +import { removePeerUsersActionType } from '../actions/aux-user-actions.js'; import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; import { legacySiweAuthActionTypes } from '../actions/siwe-actions.js'; import { joinThreadActionTypes, newThreadActionTypes, } from '../actions/thread-actions.js'; import { fetchPendingUpdatesActionTypes } from '../actions/update-actions.js'; import { findUserIdentitiesActionTypes, processNewUserIDsActionType, identityLogInActionTypes, identityRegisterActionTypes, deleteAccountActionTypes, keyserverAuthActionTypes, logOutActionTypes, legacyLogInActionTypes, legacyKeyserverRegisterActionTypes, setUserSettingsActionTypes, updateUserAvatarActionTypes, } from '../actions/user-actions.js'; import { extractKeyserverIDFromIDOptional } from '../keyserver-conn/keyserver-call-utils.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import { convertUserInfosToReplaceUserOps, type UserStoreOperation, userStoreOpsHandlers, } from '../ops/user-store-ops.js'; import { stateSyncSpecs } from '../shared/state-sync/state-sync-specs.js'; import { updateSpecs } from '../shared/updates/update-specs.js'; import type { BaseAction } from '../types/redux-types.js'; import type { ClientUserInconsistencyReportCreationRequest } from '../types/report-types.js'; import { serverRequestTypes, processServerRequestsActionType, } from '../types/request-types.js'; import { fullStateSyncActionType, incrementalStateSyncActionType, stateSyncPayloadTypes, type ClientStateSyncIncrementalSocketResult, type StateSyncIncrementalActionPayload, } from '../types/socket-types.js'; import { updateTypes } from '../types/update-types-enum.js'; import { processUpdatesActionType, type ClientUpdateInfo, type ClientUpdatesResultWithUserInfos, } from '../types/update-types.js'; import type { CurrentUserInfo, UserInfos, UserStore, } from '../types/user-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { relyingOnAuthoritativeKeyserver, usingCommServicesAccessToken, } from '../utils/services-utils.js'; function handleCurrentUserUpdates( state: ?CurrentUserInfo, newUpdates: $ReadOnlyArray, ): ?CurrentUserInfo { return newUpdates.reduce((reducedState, update) => { const { reduceCurrentUser } = updateSpecs[update.type]; return reduceCurrentUser ? reduceCurrentUser(reducedState, update) : reducedState; }, state); } function reduceCurrentUserInfo( state: ?CurrentUserInfo, action: BaseAction, ): ?CurrentUserInfo { if ( action.type === identityLogInActionTypes.success || action.type === identityRegisterActionTypes.success ) { const newUserInfo = { id: action.payload.userID, username: action.payload.username, }; if (!_isEqual(newUserInfo)(state)) { return newUserInfo; } } else if ( action.type === legacyLogInActionTypes.success || action.type === legacySiweAuthActionTypes.success || action.type === legacyKeyserverRegisterActionTypes.success || action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success ) { if (!_isEqual(action.payload.currentUserInfo)(state)) { return action.payload.currentUserInfo; } } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && action.payload.keyserverID === authoritativeKeyserverID() && relyingOnAuthoritativeKeyserver ) { const actionUserInfo = action.payload.sessionChange.currentUserInfo; if (!actionUserInfo?.id) { return actionUserInfo; } else if (!usingCommServicesAccessToken) { if (!_isEqual(actionUserInfo)(state)) { return actionUserInfo; } } else if (!state?.id || actionUserInfo.id !== state.id) { console.log( 'keyserver auth returned a different user info than identity login', ); } else { const newUserInfo = { ...state, avatar: actionUserInfo.avatar, settings: actionUserInfo.settings, }; if (!_isEqual(newUserInfo)(state)) { return newUserInfo; } } } else if ( (action.type === fullStateSyncActionType || (action.type === fetchPendingUpdatesActionTypes.success && action.payload.type === stateSyncPayloadTypes.FULL)) && relyingOnAuthoritativeKeyserver ) { if (action.payload.keyserverID !== authoritativeKeyserverID()) { return state; } const { currentUserInfo } = action.payload; if (!_isEqual(currentUserInfo)(state)) { return currentUserInfo; } } else if ( (action.type === incrementalStateSyncActionType || action.type === processUpdatesActionType) && relyingOnAuthoritativeKeyserver ) { if (action.payload.keyserverID !== authoritativeKeyserverID()) { return state; } return handleCurrentUserUpdates( state, action.payload.updatesResult.newUpdates, ); } else if (action.type === fetchPendingUpdatesActionTypes.success) { if (!relyingOnAuthoritativeKeyserver) { return state; } const { payload } = action; if (payload.type !== stateSyncPayloadTypes.INCREMENTAL) { return state; } const { newUpdates } = payload.updatesResult; if (action.payload.keyserverID !== authoritativeKeyserverID()) { return state; } return handleCurrentUserUpdates(state, newUpdates); } else if ( action.type === processServerRequestsActionType && relyingOnAuthoritativeKeyserver ) { if (action.payload.keyserverID !== authoritativeKeyserverID()) { return state; } const checkStateRequest = action.payload.serverRequests.find( candidate => candidate.type === serverRequestTypes.CHECK_STATE, ); const newCurrentUserInfo = checkStateRequest?.stateChanges?.currentUserInfo; if (newCurrentUserInfo && !_isEqual(newCurrentUserInfo)(state)) { return newCurrentUserInfo; } } else if ( action.type === updateUserAvatarActionTypes.success && state && !state.anonymous ) { const { viewerUpdates } = action.payload.updates; for (const update of viewerUpdates) { if ( update.type === updateTypes.UPDATE_CURRENT_USER && !_isEqual(update.currentUserInfo.avatar)(state.avatar) ) { return { ...state, avatar: update.currentUserInfo.avatar, }; } } return state; } else if (action.type === setUserSettingsActionTypes.success) { if (state?.settings) { return { ...state, settings: { ...state.settings, ...action.payload, }, }; } } return state; } const { processStoreOperations: processUserStoreOps } = userStoreOpsHandlers; function generateOpsForUserUpdates(payload: { +updatesResult: { +newUpdates: $ReadOnlyArray, ... }, ... }): $ReadOnlyArray { return payload.updatesResult.newUpdates .map(update => updateSpecs[update.type].generateOpsForUserInfoUpdates?.(update), ) .filter(Boolean) .flat(); } type ReduceUserInfosResult = [ UserStore, $ReadOnlyArray, $ReadOnlyArray, ]; function handleUserInfoUpdates( state: UserStore, payload: | ClientStateSyncIncrementalSocketResult | StateSyncIncrementalActionPayload | ClientUpdatesResultWithUserInfos, ): ReduceUserInfosResult { if (payload.keyserverID !== authoritativeKeyserverID()) { return [state, [], []]; } const newUserInfos = _keyBy(userInfo => userInfo.id)(payload.userInfos); const userStoreOps: $ReadOnlyArray = [ ...convertUserInfosToReplaceUserOps(newUserInfos), ...generateOpsForUserUpdates(payload), ]; const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); if (_isEqual(state.userInfos)(processedUserInfos)) { return [state, [], []]; } return [ { ...state, userInfos: processedUserInfos, }, [], userStoreOps, ]; } function reduceUserInfos( state: UserStore, action: BaseAction, ): ReduceUserInfosResult { if (action.type === processNewUserIDsActionType) { const filteredUserIDs = action.payload.userIDs.filter( id => !state.userInfos[id], ); if (filteredUserIDs.length === 0) { return [state, [], []]; } const newUserInfosArray = filteredUserIDs.map(id => ({ id, username: null, })); const newUserInfos: UserInfos = _keyBy(userInfo => userInfo.id)( newUserInfosArray, ); const userStoreOps: $ReadOnlyArray = convertUserInfosToReplaceUserOps(newUserInfos); const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); if (!_isEqual(state.userInfos)(processedUserInfos)) { return [ { ...state, userInfos: processedUserInfos, }, [], userStoreOps, ]; } } else if ( (action.type === joinThreadActionTypes.success || action.type === newThreadActionTypes.success) && relyingOnAuthoritativeKeyserver ) { let keyserverID; if (action.type === joinThreadActionTypes.success) { keyserverID = action.payload.keyserverID; } else { keyserverID = extractKeyserverIDFromIDOptional( action.payload.newThreadID, ); } if (keyserverID !== authoritativeKeyserverID()) { return [state, [], []]; } const newUserInfos: UserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const userStoreOps: $ReadOnlyArray = convertUserInfosToReplaceUserOps(newUserInfos); const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); if (!_isEqual(state.userInfos)(processedUserInfos)) { return [ { ...state, userInfos: processedUserInfos, }, [], userStoreOps, ]; } } else if (action.type === findUserIdentitiesActionTypes.success) { const newUserInfos = action.payload.userInfos.reduce( (acc, userInfo) => ({ ...acc, [userInfo.id]: { ...state.userInfos[userInfo.id], username: userInfo.username, }, }), {}, ); const userStoreOps: $ReadOnlyArray = convertUserInfosToReplaceUserOps(newUserInfos); const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); if (!_isEqual(state.userInfos)(processedUserInfos)) { return [ { ...state, userInfos: processedUserInfos, }, [], userStoreOps, ]; } } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated && action.payload.keyserverID === authoritativeKeyserverID() && relyingOnAuthoritativeKeyserver ) { const userStoreOps: $ReadOnlyArray = [ { type: 'remove_all_users' }, ]; const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); if (Object.keys(state.userInfos).length === 0) { return [state, [], []]; } return [ { userInfos: processedUserInfos, }, [], userStoreOps, ]; } else if ( (action.type === fullStateSyncActionType || (action.type === fetchPendingUpdatesActionTypes.success && action.payload.type === stateSyncPayloadTypes.FULL)) && relyingOnAuthoritativeKeyserver ) { if (action.payload.keyserverID !== authoritativeKeyserverID()) { return [state, [], []]; } const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const userStoreOps: $ReadOnlyArray = [ { type: 'remove_all_users' }, ...convertUserInfosToReplaceUserOps(newUserInfos), ]; const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); if (!_isEqual(state.userInfos)(processedUserInfos)) { return [ { userInfos: processedUserInfos, }, [], userStoreOps, ]; } } else if ( action.type === legacyLogInActionTypes.success || action.type === legacySiweAuthActionTypes.success || action.type === legacyKeyserverRegisterActionTypes.success ) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const userStoreOps: $ReadOnlyArray = [ { type: 'remove_all_users' }, ...convertUserInfosToReplaceUserOps(newUserInfos), ]; const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); return [ { userInfos: processedUserInfos, }, [], userStoreOps, ]; } else if ( action.type === keyserverAuthActionTypes.success && relyingOnAuthoritativeKeyserver ) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const userStoreOps: $ReadOnlyArray = convertUserInfosToReplaceUserOps(newUserInfos); const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); if (!_isEqual(state.userInfos)(processedUserInfos)) { return [ { ...state, userInfos: processedUserInfos, }, [], userStoreOps, ]; } } else if ( (action.type === incrementalStateSyncActionType || action.type === processUpdatesActionType) && relyingOnAuthoritativeKeyserver ) { return handleUserInfoUpdates(state, action.payload); } else if (action.type === fetchPendingUpdatesActionTypes.success) { if (!relyingOnAuthoritativeKeyserver) { return [state, [], []]; } const { payload } = action; if (payload.type === stateSyncPayloadTypes.INCREMENTAL) { return handleUserInfoUpdates(state, payload); } } else if ( action.type === processServerRequestsActionType && relyingOnAuthoritativeKeyserver ) { if (action.payload.keyserverID !== authoritativeKeyserverID()) { return [state, [], []]; } const checkStateRequest = action.payload.serverRequests.find( candidate => candidate.type === serverRequestTypes.CHECK_STATE, ); if (!checkStateRequest || !checkStateRequest.stateChanges) { return [state, [], []]; } const { userInfos, deleteUserInfoIDs } = checkStateRequest.stateChanges; if (!userInfos && !deleteUserInfoIDs) { return [state, [], []]; } const userStoreOps: UserStoreOperation[] = []; if (userInfos) { for (const userInfo of userInfos) { userStoreOps.push({ type: 'replace_user', payload: { ...userInfo } }); } } if (deleteUserInfoIDs) { userStoreOps.push({ type: 'remove_users', payload: { ids: deleteUserInfoIDs }, }); } const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); const newInconsistencies = stateSyncSpecs.users.findStoreInconsistencies( action, state.userInfos, processedUserInfos, ); return [ { userInfos: processedUserInfos, }, newInconsistencies, userStoreOps, ]; + } else if ( + action.type === removePeerUsersActionType && + !relyingOnAuthoritativeKeyserver + ) { + const userStoreOps: UserStoreOperation[] = [ + { + type: 'remove_users', + payload: { ids: action.payload.userIDs }, + }, + ]; + + return [ + { + userInfos: processUserStoreOps(state.userInfos, userStoreOps), + }, + [], + userStoreOps, + ]; } else if (action.type === updateUserAvatarActionTypes.success) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.updates.userInfos, ); const userStoreOps: $ReadOnlyArray = convertUserInfosToReplaceUserOps(newUserInfos); const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); const newState = !_isEqual(state.userInfos)(processedUserInfos) ? { ...state, userInfos: processedUserInfos, } : state; return [newState, [], userStoreOps]; } else if (action.type === setClientDBStoreActionType) { if (!action.payload.users) { return [state, [], []]; } return [{ userInfos: action.payload.users }, [], []]; } return [state, [], []]; } export { reduceCurrentUserInfo, reduceUserInfos }; diff --git a/lib/tunnelbroker/use-peer-to-peer-message-handler.js b/lib/tunnelbroker/use-peer-to-peer-message-handler.js index 683387636..0482d59e7 100644 --- a/lib/tunnelbroker/use-peer-to-peer-message-handler.js +++ b/lib/tunnelbroker/use-peer-to-peer-message-handler.js @@ -1,372 +1,378 @@ // @flow import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; +import { removePeerUsersActionType } from '../actions/aux-user-actions.js'; import { invalidateTunnelbrokerDeviceToken } from '../actions/tunnelbroker-actions.js'; import { logOutActionTypes, useLogOut } from '../actions/user-actions.js'; import { useBroadcastDeviceListUpdates, useBroadcastAccountDeletion, useGetAndUpdateDeviceListsForUsers, } from '../hooks/peer-list-hooks.js'; import { getAllPeerDevices, getForeignPeerDevices, } from '../selectors/user-selectors.js'; import { verifyAndGetDeviceList, removeDeviceFromDeviceList, } from '../shared/device-list-utils.js'; import { useProcessDMOperation } from '../shared/dm-ops/process-dm-ops.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import type { DeviceOlmInboundKeys } from '../types/identity-service-types.js'; import { peerToPeerMessageTypes, type PeerToPeerMessage, type SenderInfo, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; import { userActionsP2PMessageTypes, userActionP2PMessageValidator, type UserActionP2PMessage, } from '../types/tunnelbroker/user-actions-peer-to-peer-message-types.js'; import { getConfig } from '../utils/config.js'; import { getContentSigningKey } from '../utils/crypto-utils.js'; import { getMessageForException } from '../utils/errors.js'; import { hasHigherDeviceID, olmSessionErrors } from '../utils/olm-utils.js'; import { getClientMessageIDFromTunnelbrokerMessageID } from '../utils/peer-to-peer-communication-utils.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useDispatch, useSelector } from '../utils/redux-utils.js'; // When logout is requested by primary device, logging out of Identity Service // is already handled by the primary device const primaryRequestLogoutOptions = Object.freeze({ skipIdentityLogOut: true }); // When re-broadcasting, we want to do it only to foreign peers // to avoid a vicious circle of deletion messages sent by own devices. const accountDeletionBroadcastOptions = Object.freeze({ broadcastToOwnDevices: false, }); // handles `peerToPeerMessageTypes.ENCRYPTED_MESSAGE` function useHandleOlmMessageToDevice() { const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'Identity context should be set'); const { identityClient, getAuthMetadata } = identityContext; const broadcastDeviceListUpdates = useBroadcastDeviceListUpdates(); const reBroadcastAccountDeletion = useBroadcastAccountDeletion( accountDeletionBroadcastOptions, ); const allPeerDevices = useSelector(getAllPeerDevices); + const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const primaryDeviceRequestedLogOut = useLogOut(primaryRequestLogoutOptions); const processDMOperation = useProcessDMOperation(); return React.useCallback( async (decryptedMessageContent: string, senderInfo: SenderInfo) => { const parsedMessageToDevice = JSON.parse(decryptedMessageContent); // Handle user-action messages if (!userActionP2PMessageValidator.is(parsedMessageToDevice)) { return; } const userActionMessage: UserActionP2PMessage = parsedMessageToDevice; if ( userActionMessage.type === userActionsP2PMessageTypes.LOG_OUT_PRIMARY_DEVICE ) { void dispatchActionPromise( logOutActionTypes, primaryDeviceRequestedLogOut(), ); } else if ( userActionMessage.type === userActionsP2PMessageTypes.LOG_OUT_SECONDARY_DEVICE ) { const { userID, deviceID: deviceIDToLogOut } = senderInfo; await removeDeviceFromDeviceList( identityClient, userID, deviceIDToLogOut, ); await broadcastDeviceListUpdates( allPeerDevices.filter(deviceID => deviceID !== deviceIDToLogOut), ); } else if ( userActionMessage.type === userActionsP2PMessageTypes.DM_OPERATION ) { await processDMOperation(userActionMessage.op); } else if ( userActionMessage.type === userActionsP2PMessageTypes.ACCOUNT_DELETION ) { const { userID: thisUserID } = await getAuthMetadata(); if (!thisUserID) { return; } // own devices re-broadcast account deletion to foreign peer devices if (senderInfo.userID === thisUserID) { await reBroadcastAccountDeletion(); // we treat account deletion the same way as primary-device-requested // logout void dispatchActionPromise( logOutActionTypes, primaryDeviceRequestedLogOut(), ); } else { - // TODO: update peer list + dispatch({ + type: removePeerUsersActionType, + payload: { userIDs: [senderInfo.userID] }, + }); } } else { console.warn( 'Unsupported P2P user action message:', userActionMessage.type, ); } }, [ allPeerDevices, broadcastDeviceListUpdates, + dispatch, dispatchActionPromise, getAuthMetadata, identityClient, primaryDeviceRequestedLogOut, processDMOperation, reBroadcastAccountDeletion, ], ); } function usePeerToPeerMessageHandler(): ( message: PeerToPeerMessage, messageID: string, ) => Promise { const { olmAPI, sqliteAPI } = getConfig(); const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'Identity context should be set'); const { identityClient, getAuthMetadata } = identityContext; const foreignPeerDevices = useSelector(getForeignPeerDevices); const broadcastDeviceListUpdates = useBroadcastDeviceListUpdates(); const getAndUpdateDeviceListsForUsers = useGetAndUpdateDeviceListsForUsers(); const dispatch = useDispatch(); const handleOlmMessageToDevice = useHandleOlmMessageToDevice(); return React.useCallback( async (message: PeerToPeerMessage, messageID: string) => { if (message.type === peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION) { const { senderInfo, encryptedData, sessionVersion } = message; const { userID: senderUserID, deviceID: senderDeviceID } = senderInfo; let deviceKeys: ?DeviceOlmInboundKeys = null; try { const { keys } = await identityClient.getInboundKeysForUser(senderUserID); deviceKeys = keys[senderDeviceID]; } catch (e) { console.log(e.message); } if (!deviceKeys) { console.log( 'Error creating inbound session with device ' + `${senderDeviceID}: No keys for the device, ` + `session version: ${sessionVersion}`, ); return; } try { await olmAPI.initializeCryptoAccount(); const result = await olmAPI.contentInboundSessionCreator( deviceKeys.identityKeysBlob.primaryIdentityPublicKeys, encryptedData, sessionVersion, false, ); console.log( 'Created inbound session with device ' + `${senderDeviceID}: ${result}, ` + `session version: ${sessionVersion}`, ); } catch (e) { if (e.message?.includes(olmSessionErrors.alreadyCreated)) { console.log( 'Received session request with lower session version from ' + `${senderDeviceID}, session version: ${sessionVersion}`, ); } else if (e.message?.includes(olmSessionErrors.raceCondition)) { const currentDeviceID = await getContentSigningKey(); if (hasHigherDeviceID(currentDeviceID, senderDeviceID)) { console.log( 'Race condition while creating session with ' + `${senderDeviceID}, session version: ${sessionVersion}, ` + `this device has a higher deviceID and the session will be kept`, ); } else { const result = await olmAPI.contentInboundSessionCreator( deviceKeys.identityKeysBlob.primaryIdentityPublicKeys, encryptedData, sessionVersion, true, ); console.log( 'Overwrite session with device ' + `${senderDeviceID}: ${result}, ` + `session version: ${sessionVersion}`, ); // Resend all not-yet confirmed messages that were encrypted // with overwrite session. Tracked in ENG-6982. } } else { console.log( 'Error creating inbound session with device ' + `${senderDeviceID}: ${e.message}, ` + `session version: ${sessionVersion}`, ); } } } else if (message.type === peerToPeerMessageTypes.ENCRYPTED_MESSAGE) { try { await olmAPI.initializeCryptoAccount(); const decrypted = await olmAPI.decryptAndPersist( message.encryptedData, message.senderInfo.deviceID, messageID, ); console.log( 'Decrypted message from device ' + `${message.senderInfo.deviceID}: ${decrypted}`, ); try { await handleOlmMessageToDevice(decrypted, message.senderInfo); } catch (e) { console.log('Failed processing Olm P2P message:', e); } } catch (e) { if (e.message?.includes(olmSessionErrors.messageAlreadyDecrypted)) { console.log( 'Received already decrypted message from device ' + `${message.senderInfo.deviceID}.`, ); } else { console.log( 'Error decrypting message from device ' + `${message.senderInfo.deviceID}: ${e.message}`, ); } } } else if (message.type === peerToPeerMessageTypes.REFRESH_KEY_REQUEST) { try { await olmAPI.initializeCryptoAccount(); const oneTimeKeys = await olmAPI.getOneTimeKeys(message.numberOfKeys); await identityClient.uploadOneTimeKeys(oneTimeKeys); } catch (e) { console.log(`Error uploading one-time keys: ${e.message}`); } } else if (message.type === peerToPeerMessageTypes.DEVICE_LIST_UPDATED) { try { const result = await verifyAndGetDeviceList( identityClient, message.userID, null, ); if (!result.valid) { console.log( `Received invalid device list update for user ${message.userID}. Reason: ${result.reason}`, ); } else { console.log( `Received valid device list update for user ${message.userID}`, ); } await getAndUpdateDeviceListsForUsers([message.userID]); if (result.valid && message?.signedDeviceList?.rawDeviceList) { const receivedRawList = JSON.parse( message.signedDeviceList.rawDeviceList, ); // additional check for broadcasted and Identity device // list equality const listsAreEqual = _isEqual(result.deviceList)(receivedRawList); console.log( `Identity and received device lists are ${ listsAreEqual ? '' : 'not' } equal.`, ); } } catch (e) { console.log( `Error verifying device list for user ${message.userID}: ${e}`, ); } } else if ( message.type === peerToPeerMessageTypes.IDENTITY_DEVICE_LIST_UPDATED ) { try { const { userID } = await getAuthMetadata(); if (!userID) { return; } await Promise.all([ broadcastDeviceListUpdates(foreignPeerDevices), getAndUpdateDeviceListsForUsers([userID]), ]); } catch (e) { console.log( `Error updating device list after Identity request: ${ getMessageForException(e) ?? 'unknown error' }`, ); } } else if (message.type === peerToPeerMessageTypes.MESSAGE_PROCESSED) { try { const { deviceID, messageID: tunnelbrokerMessageID } = message; const clientMessageID = getClientMessageIDFromTunnelbrokerMessageID( tunnelbrokerMessageID, ); await sqliteAPI.removeOutboundP2PMessagesOlderThan( clientMessageID, deviceID, ); } catch (e) { console.log( `Error removing message after processing: ${ getMessageForException(e) ?? 'unknown error' }`, ); } } else if (message.type === peerToPeerMessageTypes.BAD_DEVICE_TOKEN) { dispatch({ type: invalidateTunnelbrokerDeviceToken, payload: { deviceToken: message.invalidatedToken, }, }); } }, [ broadcastDeviceListUpdates, dispatch, foreignPeerDevices, getAndUpdateDeviceListsForUsers, getAuthMetadata, handleOlmMessageToDevice, identityClient, olmAPI, sqliteAPI, ], ); } export { usePeerToPeerMessageHandler }; diff --git a/lib/types/aux-user-types.js b/lib/types/aux-user-types.js index fa0a0b1ed..f46b00c30 100644 --- a/lib/types/aux-user-types.js +++ b/lib/types/aux-user-types.js @@ -1,35 +1,37 @@ // @flow import type { FarcasterUser, RawDeviceList, UsersRawDeviceLists, IdentityPlatformDetails, } from './identity-service-types.js'; export type AuxUserInfo = { +fid: ?string, +deviceList?: RawDeviceList, +devicesPlatformDetails?: { +[deviceID: string]: IdentityPlatformDetails }, }; export type AuxUserInfos = { +[userID: string]: AuxUserInfo }; export type AuxUserStore = { +auxUserInfos: AuxUserInfos, }; export type SetAuxUserFIDsPayload = { +farcasterUsers: $ReadOnlyArray, }; export type AddAuxUserFIDsPayload = { +farcasterUsers: $ReadOnlyArray, }; +export type RemovePeerUsersPayload = { +userIDs: $ReadOnlyArray }; + export type SetPeerDeviceListsPayload = { +deviceLists: UsersRawDeviceLists, +usersPlatformDetails: { +[userID: string]: { +[deviceID: string]: IdentityPlatformDetails }, }, }; diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js index f6aa7f694..59c8516b1 100644 --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -1,1613 +1,1618 @@ // @flow import type { PersistState } from 'redux-persist/es/types'; import type { LogOutResult, KeyserverLogOutResult, LegacyLogInStartingPayload, LegacyLogInResult, LegacyRegisterResult, DefaultNotificationPayload, ClaimUsernameResponse, KeyserverAuthResult, } from './account-types.js'; import type { ActivityUpdateSuccessPayload, QueueActivityUpdatesPayload, SetThreadUnreadStatusPayload, } from './activity-types.js'; import type { AlertStore, RecordAlertActionPayload } from './alert-types.js'; import type { AuxUserStore, SetAuxUserFIDsPayload, AddAuxUserFIDsPayload, + RemovePeerUsersPayload, SetPeerDeviceListsPayload, } from './aux-user-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from './avatar-types.js'; import type { CommunityStore, AddCommunityPayload, FetchCommunityInfosResponse, CreateOrUpdateFarcasterChannelTagResponse, DeleteFarcasterChannelTagPayload, } from './community-types.js'; import type { MessageSourceMetadata, DBOpsStore } from './db-ops-types.js'; import type { GetVersionActionPayload, LastCommunicatedPlatformDetails, } from './device-types.js'; import type { ScheduleP2PMessagesPayload, ProcessDMOpsPayload, QueuedDMOperations, QueueDMOpsPayload, PruneDMOpsQueuePayload, ClearQueuedThreadDMOpsPayload, } from './dm-ops.js'; import type { DraftStore } from './draft-types.js'; import type { EnabledApps, SupportedApps } from './enabled-apps.js'; import type { RawEntryInfo, EntryStore, SaveEntryPayload, CreateEntryPayload, DeleteEntryResult, RestoreEntryPayload, FetchEntryInfosResult, CalendarQueryUpdateResult, CalendarQueryUpdateStartingPayload, FetchRevisionsForEntryPayload, } from './entry-types.js'; import type { CalendarFilter, CalendarThreadFilter, SetCalendarDeletedFilterPayload, } from './filter-types.js'; import type { IdentityAuthResult } from './identity-service-types'; import type { IntegrityStore } from './integrity-types.js'; import type { KeyserverStore, AddKeyserverPayload, RemoveKeyserverPayload, } from './keyserver-types.js'; import type { LifecycleState } from './lifecycle-state-types.js'; import type { FetchInviteLinksResponse, InviteLink, InviteLinksStore, InviteLinkVerificationResponse, DisableInviteLinkPayload, } from './link-types.js'; import type { LoadingStatus, LoadingInfo } from './loading-types.js'; import type { UpdateMultimediaMessageMediaPayload } from './media-types.js'; import type { MessageReportCreationResult } from './message-report-types.js'; import type { MessageStore, RawMultimediaMessageInfo, FetchMessageInfosPayload, SendMessagePayload, EditMessagePayload, SaveMessagesPayload, NewMessagesPayload, MessageStorePrunePayload, LocallyComposedMessageInfo, SimpleMessagesPayload, FetchPinnedMessagesResult, SearchMessagesResponse, } from './message-types.js'; import type { RawReactionMessageInfo } from './messages/reaction.js'; import type { RawTextMessageInfo } from './messages/text.js'; import type { BaseNavInfo, WebNavInfo } from './nav-types.js'; import { type ForcePolicyAcknowledgmentPayload, type PolicyAcknowledgmentPayload, type UserPolicies, } from './policy-types.js'; import type { RelationshipErrors } from './relationship-types.js'; import type { EnabledReports, ClearDeliveredReportsPayload, QueueReportsPayload, ReportStore, } from './report-types.js'; import type { ProcessServerRequestAction, GetOlmSessionInitializationDataResponse, } from './request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from './search-types.js'; import type { SetSessionPayload } from './session-types.js'; import type { ConnectionIssue, StateSyncFullActionPayload, StateSyncIncrementalActionPayload, SetActiveSessionRecoveryPayload, ClientStateSyncSocketResult, } from './socket-types.js'; import { type ClientStore } from './store-ops-types.js'; import type { SubscriptionUpdateResult } from './subscription-types.js'; import type { SyncedMetadataStore, SetSyncedMetadataEntryPayload, ClearSyncedMetadataEntryPayload, } from './synced-metadata-types.js'; import type { GlobalThemeInfo } from './theme-types.js'; import type { ThreadActivityStore } from './thread-activity-types.js'; import type { ThreadStore, ChangeThreadSettingsPayload, LeaveThreadPayload, NewThreadResult, ThreadJoinPayload, ToggleMessagePinResult, LegacyThreadStore, RoleModificationPayload, RoleDeletionPayload, } from './thread-types.js'; import type { TunnelbrokerDeviceToken } from './tunnelbroker-device-token-types.js'; import type { ClientUpdatesResultWithUserInfos } from './update-types.js'; import type { CurrentUserInfo, UserInfos, UserStore, UserInfo, } from './user-types.js'; import type { SetDeviceTokenActionPayload, SetDeviceTokenStartedPayload, } from '../actions/device-actions.js'; import type { UpdateConnectionStatusPayload, SetLateResponsePayload, UpdateKeyserverReachabilityPayload, } from '../keyserver-conn/keyserver-conn-types.js'; export type BaseAppState = { +navInfo: NavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +alertStore: AlertStore, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +dataLoaded: boolean, +userPolicies: UserPolicies, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +integrityStore: IntegrityStore, +globalThemeInfo: GlobalThemeInfo, +customServer: ?string, +communityStore: CommunityStore, +dbOpsStore: DBOpsStore, +syncedMetadataStore: SyncedMetadataStore, +auxUserStore: AuxUserStore, +tunnelbrokerDeviceToken: TunnelbrokerDeviceToken, +_persist: ?PersistState, +queuedDMOperations: QueuedDMOperations, ... }; export type NativeAppState = BaseAppState<>; export type WebAppState = BaseAppState<> & { +pushApiPublicKey: ?string, ... }; export type AppState = NativeAppState | WebAppState; export type ClientWebInitialReduxStateResponse = { +navInfo: WebNavInfo, +currentUserInfo: CurrentUserInfo, +entryStore: EntryStore, +threadStore: ThreadStore, +userInfos: UserInfos, +messageStore: MessageStore, +pushApiPublicKey: ?string, +inviteLinksStore: InviteLinksStore, +keyserverInfo: WebInitialKeyserverInfo, }; export type ServerWebInitialReduxStateResponse = { +navInfo: WebNavInfo, +currentUserInfo: CurrentUserInfo, +entryStore: EntryStore, +threadStore: LegacyThreadStore, +userInfos: UserInfos, +messageStore: MessageStore, +pushApiPublicKey: ?string, +inviteLinksStore: InviteLinksStore, +keyserverInfo: WebInitialKeyserverInfo, }; export type WebInitialKeyserverInfo = { +sessionID: ?string, +updatesCurrentAsOf: number, }; export type BaseAction = $ReadOnly<{ +messageSourceMetadata?: MessageSourceMetadata, ... | { +type: '@@redux/INIT', +payload?: void, } | { +type: 'FETCH_ENTRIES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_ENTRIES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_ENTRIES_SUCCESS', +payload: FetchEntryInfosResult, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_SUCCESS', +payload: LogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_SUCCESS', +payload: ClaimUsernameResponse, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_SUCCESS', +payload: KeyserverLogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ACCOUNT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ACCOUNT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ACCOUNT_SUCCESS', +payload: LogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_LOCAL_ENTRY', +payload: RawEntryInfo, } | { +type: 'CREATE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_ENTRY_SUCCESS', +payload: CreateEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_SUCCESS', +payload: SaveEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'CONCURRENT_MODIFICATION_RESET', +payload: { +id: string, +dbText: string, }, } | { +type: 'DELETE_ENTRY_STARTED', +loadingInfo: LoadingInfo, +payload: { +localID: ?string, +serverID: ?string, }, } | { +type: 'DELETE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ENTRY_SUCCESS', +payload: ?DeleteEntryResult, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_LOG_IN_STARTED', +loadingInfo: LoadingInfo, +payload?: void, } | { +type: 'IDENTITY_LOG_IN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_LOG_IN_SUCCESS', +payload: IdentityAuthResult, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_AUTH_STARTED', +loadingInfo: LoadingInfo, +payload?: void, } | { +type: 'KEYSERVER_AUTH_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_AUTH_SUCCESS', +payload: KeyserverAuthResult, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_LOG_IN_STARTED', +loadingInfo: LoadingInfo, +payload: LegacyLogInStartingPayload, } | { +type: 'LEGACY_LOG_IN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_LOG_IN_SUCCESS', +payload: LegacyLogInResult, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_KEYSERVER_REGISTER_STARTED', +loadingInfo: LoadingInfo, +payload: LegacyLogInStartingPayload, } | { +type: 'LEGACY_KEYSERVER_REGISTER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_KEYSERVER_REGISTER_SUCCESS', +payload: LegacyRegisterResult, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_REGISTER_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_REGISTER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_REGISTER_SUCCESS', +payload: IdentityAuthResult, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_GENERATE_NONCE_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_GENERATE_NONCE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_GENERATE_NONCE_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_IDENTITY_USER_PASSWORD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_IDENTITY_USER_PASSWORD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_IDENTITY_USER_PASSWORD_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_SUCCESS', +payload: LeaveThreadPayload, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_SUCCESS', +payload: NewThreadResult, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_SUCCESS', +payload: FetchRevisionsForEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_SUCCESS', +payload: RestoreEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_SUCCESS', +payload: ThreadJoinPayload, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_SUCCESS', +payload: LeaveThreadPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_NEW_SESSION', +payload: SetSessionPayload, } | { +type: 'persist/REHYDRATE', +payload: ?BaseAppState<>, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_SUCCESS', +payload: FetchMessageInfosPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_SUCCESS', +payload: FetchMessageInfosPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_SUCCESS', +payload: SimpleMessagesPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_TEXT_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawTextMessageInfo, } | { +type: 'SEND_TEXT_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, }, +loadingInfo?: LoadingInfo, } | { +type: 'SEND_TEXT_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawMultimediaMessageInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, }, +loadingInfo?: LoadingInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REACTION_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawReactionMessageInfo, } | { +type: 'SEND_REACTION_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, +targetMessageID: string, +reaction: string, +action: string, }, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REACTION_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_SUCCESS', +payload: UserSearchResult, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_SUCCESS', +payload: ExactUserSearchResult, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_DRAFT', +payload: { +key: string, +text: string, }, } | { +type: 'MOVE_DRAFT', +payload: { +oldKey: string, +newKey: string, }, } | { +type: 'SET_CLIENT_DB_STORE', +payload: ClientStore, } | { +type: 'UPDATE_ACTIVITY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_ACTIVITY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_ACTIVITY_SUCCESS', +payload: ActivityUpdateSuccessPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_STARTED', +payload: SetDeviceTokenStartedPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_SUCCESS', +payload: SetDeviceTokenActionPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_SUCCESS', +payload?: ClearDeliveredReportsPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_SUCCESS', +payload?: ClearDeliveredReportsPayload, +loadingInfo: LoadingInfo, } | { +type: 'QUEUE_REPORTS', +payload: QueueReportsPayload, } | { +type: 'SET_URL_PREFIX', +payload: string, } | { +type: 'SAVE_MESSAGES', +payload: SaveMessagesPayload, } | { +type: 'UPDATE_CALENDAR_THREAD_FILTER', +payload: CalendarThreadFilter, } | { +type: 'CLEAR_CALENDAR_THREAD_FILTER', +payload?: void, } | { +type: 'SET_CALENDAR_DELETED_FILTER', +payload: SetCalendarDeletedFilterPayload, } | { +type: 'UPDATE_SUBSCRIPTION_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_SUBSCRIPTION_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_SUBSCRIPTION_SUCCESS', +payload: SubscriptionUpdateResult, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_QUERY_STARTED', +loadingInfo: LoadingInfo, +payload?: CalendarQueryUpdateStartingPayload, } | { +type: 'UPDATE_CALENDAR_QUERY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_QUERY_SUCCESS', +payload: CalendarQueryUpdateResult, +loadingInfo: LoadingInfo, } | { +type: 'FULL_STATE_SYNC', +payload: StateSyncFullActionPayload, } | { +type: 'INCREMENTAL_STATE_SYNC', +payload: StateSyncIncrementalActionPayload, } | ProcessServerRequestAction | { +type: 'UPDATE_CONNECTION_STATUS', +payload: UpdateConnectionStatusPayload, } | { +type: 'QUEUE_ACTIVITY_UPDATES', +payload: QueueActivityUpdatesPayload, } | { +type: 'UNSUPERVISED_BACKGROUND', +payload: { +keyserverID: string }, } | { +type: 'UPDATE_LIFECYCLE_STATE', +payload: LifecycleState, } | { +type: 'ENABLE_APP', +payload: SupportedApps, } | { +type: 'DISABLE_APP', +payload: SupportedApps, } | { +type: 'UPDATE_REPORTS_ENABLED', +payload: Partial, } | { +type: 'PROCESS_UPDATES', +payload: ClientUpdatesResultWithUserInfos, } | { +type: 'PROCESS_MESSAGES', +payload: NewMessagesPayload, } | { +type: 'MESSAGE_STORE_PRUNE', +payload: MessageStorePrunePayload, } | { +type: 'SET_LATE_RESPONSE', +payload: SetLateResponsePayload, } | { +type: 'UPDATE_KEYSERVER_REACHABILITY', +payload: UpdateKeyserverReachabilityPayload, } | { +type: 'REQUEST_ACCESS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'REQUEST_ACCESS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'REQUEST_ACCESS_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_MULTIMEDIA_MESSAGE_MEDIA', +payload: UpdateMultimediaMessageMediaPayload, } | { +type: 'CREATE_LOCAL_MESSAGE', +payload: LocallyComposedMessageInfo, } | { +type: 'UPDATE_RELATIONSHIPS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_RELATIONSHIPS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_RELATIONSHIPS_SUCCESS', +payload: RelationshipErrors, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_STARTED', +payload: { +threadID: string, +unread: boolean, }, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_SUCCESS', +payload: SetThreadUnreadStatusPayload, } | { +type: 'SET_USER_SETTINGS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SET_USER_SETTINGS_SUCCESS', +payload: DefaultNotificationPayload, } | { +type: 'SET_USER_SETTINGS_FAILED', +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_SUCCESS', +payload: MessageReportCreationResult, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FORCE_POLICY_ACKNOWLEDGMENT', +payload: ForcePolicyAcknowledgmentPayload, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_SUCCESS', +payload: PolicyAcknowledgmentPayload, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_SIWE_AUTH_STARTED', +payload: LegacyLogInStartingPayload, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_SIWE_AUTH_SUCCESS', +payload: LegacyLogInResult, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_SIWE_AUTH_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'RECORD_ALERT', +payload: RecordAlertActionPayload, } | { +type: 'UPDATE_USER_AVATAR_STARTED', +payload: UpdateUserAvatarRequest, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_USER_AVATAR_SUCCESS', +payload: UpdateUserAvatarResponse, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_USER_AVATAR_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_EDIT_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'SEND_EDIT_MESSAGE_SUCCESS', +payload: EditMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_EDIT_MESSAGE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'TOGGLE_MESSAGE_PIN_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'TOGGLE_MESSAGE_PIN_SUCCESS', +payload: ToggleMessagePinResult, +loadingInfo: LoadingInfo, } | { +type: 'TOGGLE_MESSAGE_PIN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PINNED_MESSAGES_SUCCESS', +payload: FetchPinnedMessagesResult, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'VERIFY_INVITE_LINK_SUCCESS', +payload: InviteLinkVerificationResponse, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_SUCCESS', +payload: FetchInviteLinksResponse, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CALENDAR_COMMUNITY_FILTER', +payload: void, } | { +type: 'UPDATE_CHAT_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CHAT_COMMUNITY_FILTER', +payload: void, } | { +type: 'SEARCH_MESSAGES_STARTED', +payload: void, +loadingInfo?: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_SUCCESS', +payload: SearchMessagesResponse, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_SUCCESS', +payload: InviteLink, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DISABLE_INVITE_LINK_SUCCESS', +payload: DisableInviteLinkPayload, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', +payload: GetOlmSessionInitializationDataResponse, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DATA_LOADED', +payload: { +dataLoaded: boolean, }, } | { +type: 'GET_VERSION_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_VERSION_SUCCESS', +payload: GetVersionActionPayload, +loadingInfo: LoadingInfo, } | { +type: 'GET_VERSION_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_LAST_COMMUNICATED_PLATFORM_DETAILS', +payload: LastCommunicatedPlatformDetails, } | { +type: 'RESET_USER_STATE', +payload?: void } | { +type: 'MODIFY_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'MODIFY_COMMUNITY_ROLE_SUCCESS', +payload: RoleModificationPayload, +loadingInfo: LoadingInfo, } | { +type: 'MODIFY_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DELETE_COMMUNITY_ROLE_SUCCESS', +payload: RoleDeletionPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_THREAD_LAST_NAVIGATED', +payload: { +threadID: string, +time: number }, } | { +type: 'UPDATE_INTEGRITY_STORE', +payload: { +threadIDsToHash?: $ReadOnlyArray, +threadHashingStatus?: 'starting' | 'running' | 'completed', }, } | { +type: 'UPDATE_THEME_INFO', +payload: Partial, } | { +type: 'ADD_KEYSERVER', +payload: AddKeyserverPayload, } | { +type: 'REMOVE_KEYSERVER', +payload: RemoveKeyserverPayload, } | { +type: 'SET_CUSTOM_SERVER', +payload: string, } | { +type: 'SET_CONNECTION_ISSUE', +payload: { +connectionIssue: ?ConnectionIssue, +keyserverID: string }, } | { +type: 'ADD_COMMUNITY', +payload: AddCommunityPayload, } | { +type: 'SET_SYNCED_METADATA_ENTRY', +payload: SetSyncedMetadataEntryPayload, } | { +type: 'CLEAR_SYNCED_METADATA_ENTRY', +payload: ClearSyncedMetadataEntryPayload, } | { +type: 'SET_ACTIVE_SESSION_RECOVERY', +payload: SetActiveSessionRecoveryPayload, } | { +type: 'SET_AUX_USER_FIDS', +payload: SetAuxUserFIDsPayload, } | { +type: 'ADD_AUX_USER_FIDS', +payload: AddAuxUserFIDsPayload, } | { +type: 'CLEAR_AUX_USER_FIDS', +payload?: void, } + | { + +type: 'REMOVE_PEER_USERS', + +payload: RemovePeerUsersPayload, + } | { +type: 'SET_PEER_DEVICE_LISTS', +payload: SetPeerDeviceListsPayload, } | { +type: 'OPS_PROCESSING_FINISHED_ACTION_TYPE', +payload?: void, } | { +type: 'FETCH_COMMUNITY_INFOS_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_COMMUNITY_INFOS_SUCCESS', +payload: FetchCommunityInfosResponse, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_COMMUNITY_INFOS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_SUCCESS', +payload: CreateOrUpdateFarcasterChannelTagResponse, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_FARCASTER_CHANNEL_TAG_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DELETE_FARCASTER_CHANNEL_TAG_SUCCESS', +payload: DeleteFarcasterChannelTagPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_FARCASTER_CHANNEL_TAG_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'PROCESS_NEW_USER_IDS', +payload: { +userIDs: $ReadOnlyArray, }, +loadingInfo: LoadingInfo, } | { +type: 'FIND_USER_IDENTITIES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FIND_USER_IDENTITIES_SUCCESS', +payload: { +userInfos: $ReadOnlyArray, }, +loadingInfo: LoadingInfo, } | { +type: 'FIND_USER_IDENTITIES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'VERSION_SUPPORTED_BY_IDENTITY_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'VERSION_SUPPORTED_BY_IDENTITY_SUCCESS', +payload: { +supported: boolean, }, +loadingInfo: LoadingInfo, } | { +type: 'VERSION_SUPPORTED_BY_IDENTITY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PENDING_UPDATES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PENDING_UPDATES_SUCCESS', +payload: ClientStateSyncSocketResult, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PENDING_UPDATES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_TB_DEVICE_TOKEN_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'SET_TB_DEVICE_TOKEN_SUCCESS', +payload: { +deviceToken: string, }, +loadingInfo: LoadingInfo, } | { +type: 'SET_TB_DEVICE_TOKEN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'PROCESS_DM_OPS', +payload: ProcessDMOpsPayload, } | { +type: 'SCHEDULE_P2P_MESSAGES', +payload: ScheduleP2PMessagesPayload } | { +type: 'INVALIDATE_TUNNELBROKER_DEVICE_TOKEN', +payload: { +deviceToken: string, }, } | { +type: 'QUEUE_DM_OPS', +payload: QueueDMOpsPayload } | { +type: 'PRUNE_DM_OPS_QUEUE', +payload: PruneDMOpsQueuePayload } | { +type: 'CLEAR_QUEUED_THREAD_DM_OPS', +payload: ClearQueuedThreadDMOpsPayload, }, }>; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); export type DispatchSource = 'tunnelbroker' | 'tab-sync'; export type SuperAction = { +type: string, +payload?: ActionPayload, +loadingInfo?: LoadingInfo, +error?: boolean, +dispatchSource?: DispatchSource, +messageSourceMetadata?: MessageSourceMetadata, }; type ThunkedAction = (dispatch: Dispatch) => void; export type PromisedAction = (dispatch: Dispatch) => Promise; export type Dispatch = ((promisedAction: PromisedAction) => Promise) & ((thunkedAction: ThunkedAction) => void) & ((action: SuperAction) => boolean); // This is lifted from redux-persist/lib/constants.js // I don't want to add redux-persist to the web/server bundles... // import { REHYDRATE } from 'redux-persist'; export const rehydrateActionType = 'persist/REHYDRATE';