diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js index 0a7d576d4..c4c69b1ac 100644 --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -1,1530 +1,1537 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useClearAllHolders } from './holder-actions.js'; import { useBroadcastDeviceListUpdates, useBroadcastAccountDeletion, } from '../hooks/peer-list-hooks.js'; import type { CallSingleKeyserverEndpoint, CallSingleKeyserverEndpointOptions, } from '../keyserver-conn/call-single-keyserver-endpoint.js'; import { extractKeyserverIDFromID, sortThreadIDsPerKeyserver, sortCalendarQueryPerKeyserver, } from '../keyserver-conn/keyserver-call-utils.js'; import { useKeyserverCall } from '../keyserver-conn/keyserver-call.js'; import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import { usePreRequestUserState } from '../selectors/account-selectors.js'; import { getForeignPeerDeviceIDs } from '../selectors/user-selectors.js'; import { getOneTimeKeyValuesFromBlob, getPrekeyValueFromBlob, } from '../shared/crypto-utils.js'; import { fetchLatestDeviceList } from '../shared/device-list-utils.js'; import type { OutboundDMOperationSpecification } from '../shared/dm-ops/dm-op-utils'; import { dmOperationSpecificationTypes } from '../shared/dm-ops/dm-op-utils.js'; import { useProcessAndSendDMOperation } from '../shared/dm-ops/process-dm-ops.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import threadWatcher from '../shared/thread-watcher.js'; import { permissionsAndAuthRelatedRequestTimeout, callIdentityServiceTimeout, } from '../shared/timeouts.js'; import { PeerToPeerContext, usePeerToPeerCommunication, } from '../tunnelbroker/peer-to-peer-context.js'; import type { LegacyLogInInfo, LegacyLogInResult, LegacyRegisterResult, LegacyRegisterInfo, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, ClaimUsernameRequest, ClaimUsernameResponse, LogInRequest, KeyserverAuthResult, KeyserverAuthInfo, ClientKeyserverAuthRequest, ClientLogInResponse, KeyserverLogOutResult, LogOutResult, } from '../types/account-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from '../types/avatar-types.js'; import type { DMChangeThreadSubscriptionOperation } from '../types/dm-ops'; import type { RawEntryInfo, CalendarQuery } from '../types/entry-types.js'; import type { IdentityAuthResult } from '../types/identity-service-types.js'; import type { RawMessageInfo, MessageTruncationStatuses, } from '../types/message-types.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types'; import type { GetOlmSessionInitializationDataResponse } from '../types/olm-session-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from '../types/search-types.js'; import type { PreRequestUserState } from '../types/session-types.js'; import type { SubscriptionUpdateRequest, SubscriptionUpdateResult, } from '../types/subscription-types.js'; import { thickThreadTypes } from '../types/thread-types-enum.js'; import type { RawThreadInfos } from '../types/thread-types.js'; import { userActionsP2PMessageTypes, type DeviceLogoutP2PMessage, type SecondaryDeviceLogoutP2PMessage, } from '../types/tunnelbroker/user-actions-peer-to-peer-message-types.js'; import type { CurrentUserInfo, UserInfo, PasswordUpdate, LoggedOutUserInfo, } from '../types/user-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { getConfig } from '../utils/config.js'; import { getMessageForException } from '../utils/errors.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; import sleep from '../utils/sleep.js'; const loggedOutUserInfo: LoggedOutUserInfo = { anonymous: true, }; export type KeyserverLogOutInput = { +preRequestUserState: PreRequestUserState, +keyserverIDs?: $ReadOnlyArray, }; const logOutActionTypes = Object.freeze({ started: 'LOG_OUT_STARTED', success: 'LOG_OUT_SUCCESS', failed: 'LOG_OUT_FAILED', }); const keyserverLogOut = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: KeyserverLogOutInput) => Promise) => async input => { const { preRequestUserState } = input; const keyserverIDs = input.keyserverIDs ?? allKeyserverIDs; const requests: { [string]: {} } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = {}; } let response = null; try { response = await Promise.race([ callKeyserverEndpoint('log_out', requests), (async () => { await sleep(500); throw new Error('keyserver log_out took more than 500ms'); })(), ]); } catch {} const currentUserInfo = response ? loggedOutUserInfo : null; return { currentUserInfo, preRequestUserState, keyserverIDs }; }; type UseLogOutOptions = { +logOutType?: 'legacy' | 'primary_device' | 'secondary_device', +skipIdentityLogOut?: boolean, +handleUseNewFlowResponse?: () => void, }; function useLogOut( options: UseLogOutOptions = {}, ): (keyserverIDs?: $ReadOnlyArray) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const preRequestUserState = usePreRequestUserState(); const callKeyserverLogOut = useKeyserverCall(keyserverLogOut); const sendLogoutMessage = useSendLogoutMessageToPrimaryDevice(false); const removeAllHolders = useClearAllHolders(); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); const { logOutType, skipIdentityLogOut, handleUseNewFlowResponse } = options; return React.useCallback( async (keyserverIDs?: $ReadOnlyArray) => { const holdersPromise = (async () => { try { await removeAllHolders(); } catch {} })(); const identityPromise = (async () => { if ( skipIdentityLogOut || !usingCommServicesAccessToken || !commServicesAccessToken ) { return; } if (!identityClient) { throw new Error('Identity service client is not initialized'); } let callIdentityClientLogOut; if (logOutType === 'primary_device') { if (!identityClient.logOutPrimaryDevice) { throw new Error( 'logOutPrimaryDevice not defined. ' + 'Are you calling it on non-primary device?', ); } callIdentityClientLogOut = identityClient.logOutPrimaryDevice; } else if (logOutType === 'secondary_device') { callIdentityClientLogOut = identityClient.logOutSecondaryDevice; } else { callIdentityClientLogOut = async () => { try { await identityClient.logOut(); } catch (e) { const errorMessage = getMessageForException(e); if (errorMessage !== 'use_new_flow') { throw e; } // When use_new_flow is returned on legacy V1 login, // we are sure that another device upgraded flows and has become // primary, so this is now a secondary device which still uses // old flow try { await sendLogoutMessage(); } catch (err) { console.log('Failed to send logout message:', err); } handleUseNewFlowResponse?.(); } }; } try { await Promise.race([ callIdentityClientLogOut(), (async () => { await sleep(500); throw new Error('identity log_out took more than 500ms'); })(), ]); } catch {} })(); const [{ keyserverIDs: _, ...result }] = await Promise.all([ callKeyserverLogOut({ preRequestUserState, keyserverIDs, }), holdersPromise, identityPromise, ]); return { ...result, preRequestUserState: { ...result.preRequestUserState, commServicesAccessToken, }, }; }, [ callKeyserverLogOut, commServicesAccessToken, identityClient, logOutType, preRequestUserState, removeAllHolders, skipIdentityLogOut, sendLogoutMessage, handleUseNewFlowResponse, ], ); } function useIdentityLogOut(): () => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const preRequestUserState = usePreRequestUserState(); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); return React.useCallback(async () => { invariant( usingCommServicesAccessToken, 'identityLogOut can only be called when usingCommServicesAccessToken', ); if (!identityClient) { throw new Error('Identity service client is not initialized'); } try { await Promise.race([ identityClient.logOut(), (async () => { await sleep(500); throw new Error('identity log_out took more than 500ms'); })(), ]); } catch {} return { currentUserInfo: null, preRequestUserState: { ...preRequestUserState, commServicesAccessToken, }, }; }, [commServicesAccessToken, identityClient, preRequestUserState]); } function useInvalidCSATLogOut(): () => Promise { const dispatchActionPromise = useDispatchActionPromise(); const preRequestUserState = usePreRequestUserState(); const callKeyserverLogOut = useKeyserverCall(keyserverLogOut); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); return React.useCallback(() => { const keyserverLogOutPromise = async () => { // errors are swallowed inside `keyserverLogOut` above const { keyserverIDs: _, ...result } = await callKeyserverLogOut({ preRequestUserState, }); return { ...result, preRequestUserState: { ...result.preRequestUserState, commServicesAccessToken, }, }; }; return dispatchActionPromise(logOutActionTypes, keyserverLogOutPromise()); }, [ dispatchActionPromise, commServicesAccessToken, callKeyserverLogOut, preRequestUserState, ]); } const primaryDeviceLogOutOptions = Object.freeze({ logOutType: 'primary_device', }); function usePrimaryDeviceLogOut(): () => Promise { const identityContext = React.useContext(IdentityClientContext); if (!identityContext) { throw new Error('Identity service client is not initialized'); } const broadcastDeviceListUpdates = useBroadcastDeviceListUpdates(); const { broadcastEphemeralMessage } = usePeerToPeerCommunication(); const foreignPeerDevices = useSelector(getForeignPeerDeviceIDs); const logOut = useLogOut(primaryDeviceLogOutOptions); return React.useCallback(async () => { const { identityClient, getAuthMetadata } = identityContext; const authMetadata = await getAuthMetadata(); const { userID, deviceID: thisDeviceID } = authMetadata; if (!thisDeviceID || !userID) { throw new Error('No auth metadata'); } const { devices: [primaryDeviceID, ...secondaryDevices], } = await fetchLatestDeviceList(identityClient, userID); if (thisDeviceID !== primaryDeviceID) { throw new Error('Used primary device logout on a non-primary device'); } const messageContents: DeviceLogoutP2PMessage = { type: userActionsP2PMessageTypes.LOG_OUT_DEVICE, }; const recipients = secondaryDevices.map(deviceID => ({ userID, deviceID })); await broadcastEphemeralMessage( JSON.stringify(messageContents), recipients, authMetadata, ); // - logOut() performs device list update by calling Identity RPC // - broadcastDeviceListUpdates asks peers to download it from identity // so we need to call them in this order to make sure peers have latest // device list. // We're relying on Tunnelbroker session stil existing after calling logout // and auth metadata not yet cleared at this point. const logOutResult = await logOut(); await broadcastDeviceListUpdates(foreignPeerDevices); return logOutResult; }, [ broadcastDeviceListUpdates, broadcastEphemeralMessage, foreignPeerDevices, identityContext, logOut, ]); } function useSendLogoutMessageToPrimaryDevice( mandatory?: boolean = true, ): () => Promise { const identityContext = React.useContext(IdentityClientContext); if (!identityContext) { throw new Error('Identity service client is not initialized'); } const peerToPeerContext = React.useContext(PeerToPeerContext); return React.useCallback(async () => { if (!peerToPeerContext) { if (mandatory) { throw new Error('PeerToPeerContext not set'); } return; } const { broadcastEphemeralMessage } = peerToPeerContext; const { identityClient, getAuthMetadata } = identityContext; const authMetadata = await getAuthMetadata(); const { userID, deviceID } = authMetadata; if (!deviceID || !userID) { throw new Error('No auth metadata'); } // get current device list and primary device ID const { devices } = await fetchLatestDeviceList(identityClient, userID); const primaryDeviceID = devices[0]; if (deviceID === primaryDeviceID) { throw new Error('Used secondary device logout on primary device'); } // create and send Olm Tunnelbroker message to primary device const { olmAPI } = getConfig(); await olmAPI.initializeCryptoAccount(); const messageContents: SecondaryDeviceLogoutP2PMessage = { type: userActionsP2PMessageTypes.LOG_OUT_SECONDARY_DEVICE, }; const recipient = { userID, deviceID: primaryDeviceID }; await broadcastEphemeralMessage( JSON.stringify(messageContents), [recipient], authMetadata, ); }, [identityContext, peerToPeerContext, mandatory]); } const secondaryDeviceLogOutOptions = Object.freeze({ logOutType: 'secondary_device', }); function useSecondaryDeviceLogOut(): () => Promise { const logOut = useLogOut(secondaryDeviceLogOutOptions); const sendLogoutMessage = useSendLogoutMessageToPrimaryDevice(); return React.useCallback(async () => { await sendLogoutMessage(); // log out of identity service, keyserver and visually return logOut(); }, [sendLogoutMessage, logOut]); } const claimUsernameActionTypes = Object.freeze({ started: 'CLAIM_USERNAME_STARTED', success: 'CLAIM_USERNAME_SUCCESS', failed: 'CLAIM_USERNAME_FAILED', }); const claimUsername = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( claimUsernameRequest: ClaimUsernameRequest, ) => Promise) => async (claimUsernameRequest: ClaimUsernameRequest) => { const requests = { [authoritativeKeyserverID()]: claimUsernameRequest }; const responses = await callKeyserverEndpoint('claim_username', requests); const response = responses[authoritativeKeyserverID()]; return { message: response.message, signature: response.signature, }; }; function useClaimUsername(): ( claimUsernameRequest: ClaimUsernameRequest, ) => Promise { return useKeyserverCall(claimUsername); } const deleteKeyserverAccountActionTypes = Object.freeze({ started: 'DELETE_KEYSERVER_ACCOUNT_STARTED', success: 'DELETE_KEYSERVER_ACCOUNT_SUCCESS', failed: 'DELETE_KEYSERVER_ACCOUNT_FAILED', }); const deleteKeyserverAccount = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: KeyserverLogOutInput) => Promise) => async input => { const { preRequestUserState } = input; const keyserverIDs = input.keyserverIDs ?? allKeyserverIDs; const requests: { [string]: {} } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = {}; } await callKeyserverEndpoint('delete_account', requests); return { currentUserInfo: loggedOutUserInfo, preRequestUserState, keyserverIDs, }; }; function useDeleteKeyserverAccount(): ( keyserverIDs?: $ReadOnlyArray, ) => Promise { const preRequestUserState = usePreRequestUserState(); const callKeyserverDeleteAccount = useKeyserverCall(deleteKeyserverAccount); return React.useCallback( (keyserverIDs?: $ReadOnlyArray) => callKeyserverDeleteAccount({ preRequestUserState, keyserverIDs }), [callKeyserverDeleteAccount, preRequestUserState], ); } const deleteAccountActionTypes = Object.freeze({ started: 'DELETE_ACCOUNT_STARTED', success: 'DELETE_ACCOUNT_SUCCESS', failed: 'DELETE_ACCOUNT_FAILED', }); const accountDeletionBroadcastOptions = Object.freeze({ includeOwnDevices: true, }); function useDeleteAccount(): (password: ?string) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const broadcastAccountDeletion = useBroadcastAccountDeletion( accountDeletionBroadcastOptions, ); const preRequestUserState = usePreRequestUserState(); const callKeyserverDeleteAccount = useKeyserverCall(deleteKeyserverAccount); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); return React.useCallback( async password => { if (usingCommServicesAccessToken) { if (!identityClient) { throw new Error('Identity service client is not initialized'); } const { deleteWalletUser, deletePasswordUser } = identityClient; if (!deleteWalletUser || !deletePasswordUser) { throw new Error('Delete user method unimplemented'); } await broadcastAccountDeletion(); if (password) { await deletePasswordUser(password); } else { await deleteWalletUser(); } } try { const keyserverResult = await callKeyserverDeleteAccount({ preRequestUserState, }); const { keyserverIDs: _, ...result } = keyserverResult; return { ...result, preRequestUserState: { ...result.preRequestUserState, commServicesAccessToken, }, }; } catch (e) { if (!usingCommServicesAccessToken) { throw e; } console.log( 'Failed to delete account on keyserver:', getMessageForException(e), ); } return { currentUserInfo: null, preRequestUserState: { ...preRequestUserState, commServicesAccessToken, }, }; }, [ broadcastAccountDeletion, callKeyserverDeleteAccount, commServicesAccessToken, identityClient, preRequestUserState, ], ); } // useDeleteDiscardedIdentityAccount is used in a scenario where the user is // visibly logged-out, and it's only used to reset state (eg. Redux, SQLite) to // a logged-out state. function useDeleteDiscardedIdentityAccount(): ( password: ?string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const preRequestUserState = usePreRequestUserState(); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); return React.useCallback( async password => { invariant( usingCommServicesAccessToken, 'deleteDiscardedIdentityAccount can only be called when ' + 'usingCommServicesAccessToken', ); if (!identityClient) { throw new Error('Identity service client is not initialized'); } if ( !identityClient.deleteWalletUser || !identityClient.deletePasswordUser ) { throw new Error('Delete user method unimplemented'); } const deleteUserPromise = password ? identityClient.deletePasswordUser(password) : identityClient.deleteWalletUser(); await Promise.race([ deleteUserPromise, (async () => { await sleep(callIdentityServiceTimeout); throw new Error('identity delete user call took more than 500ms'); })(), ]); return { currentUserInfo: null, preRequestUserState: { ...preRequestUserState, commServicesAccessToken, }, }; }, [commServicesAccessToken, identityClient, preRequestUserState], ); } const legacyKeyserverRegisterActionTypes = Object.freeze({ started: 'LEGACY_KEYSERVER_REGISTER_STARTED', success: 'LEGACY_KEYSERVER_REGISTER_SUCCESS', failed: 'LEGACY_KEYSERVER_REGISTER_FAILED', }); const legacyKeyserverRegisterCallSingleKeyserverEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const legacyKeyserverRegister = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( registerInfo: LegacyRegisterInfo, options?: CallSingleKeyserverEndpointOptions, ) => Promise) => async (registerInfo, options) => { const deviceTokenUpdateRequest = registerInfo.deviceTokenUpdateRequest[authoritativeKeyserverID()]; const { preRequestUserInfo, ...rest } = registerInfo; const response = await callSingleKeyserverEndpoint( 'create_account', { ...rest, deviceTokenUpdateRequest, platformDetails: getConfig().platformDetails, }, { ...legacyKeyserverRegisterCallSingleKeyserverEndpointOptions, ...options, }, ); return { currentUserInfo: response.currentUserInfo, rawMessageInfos: response.rawMessageInfos, threadInfos: response.cookieChange.threadInfos, userInfos: response.cookieChange.userInfos, calendarQuery: registerInfo.calendarQuery, }; }; export type KeyserverAuthInput = $ReadOnly<{ ...KeyserverAuthInfo, +preRequestUserInfo: ?CurrentUserInfo, }>; const keyserverAuthActionTypes = Object.freeze({ started: 'KEYSERVER_AUTH_STARTED', success: 'KEYSERVER_AUTH_SUCCESS', failed: 'KEYSERVER_AUTH_FAILED', }); const keyserverAuthCallSingleKeyserverEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const keyserverAuth = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: KeyserverAuthInput) => Promise) => async keyserverAuthInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { authActionSource, calendarQuery, keyserverData, deviceTokenUpdateInput, preRequestUserInfo, ...restLogInInfo } = keyserverAuthInfo; const keyserverIDs = Object.keys(keyserverData); const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs); const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver( calendarQuery, keyserverIDs, ); const requests: { [string]: ClientKeyserverAuthRequest } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = { ...restLogInInfo, deviceTokenUpdateRequest: deviceTokenUpdateInput[keyserverID], watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], calendarQuery: calendarQueryPerKeyserver[keyserverID], platformDetails: getConfig().platformDetails, initialContentEncryptedMessage: keyserverData[keyserverID].initialContentEncryptedMessage, initialNotificationsEncryptedMessage: keyserverData[keyserverID].initialNotificationsEncryptedMessage, source: authActionSource, }; } const responses: { +[string]: ClientLogInResponse } = await callKeyserverEndpoint( 'keyserver_auth', requests, keyserverAuthCallSingleKeyserverEndpointOptions, ); let threadInfos: RawThreadInfos = {}; const calendarResult: WritableCalendarResult = { calendarQuery: keyserverAuthInfo.calendarQuery, rawEntryInfos: [], }; const messagesResult: WritableGenericMessagesResult = { messageInfos: [], truncationStatus: {}, watchedIDsAtRequestTime: watchedIDs, currentAsOf: {}, }; let updatesCurrentAsOf: { +[string]: number } = {}; for (const keyserverID in responses) { threadInfos = { ...responses[keyserverID].cookieChange.threadInfos, ...threadInfos, }; if (responses[keyserverID].rawEntryInfos) { calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } messagesResult.messageInfos = messagesResult.messageInfos.concat( responses[keyserverID].rawMessageInfos, ); messagesResult.truncationStatus = { ...messagesResult.truncationStatus, ...responses[keyserverID].truncationStatuses, }; messagesResult.currentAsOf = { ...messagesResult.currentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; updatesCurrentAsOf = { ...updatesCurrentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; } const authKeyserverID = authoritativeKeyserverID(); let userInfos: $ReadOnlyArray = []; if (responses[authKeyserverID]) { const userInfosArrays = [ responses[authKeyserverID].userInfos, responses[authKeyserverID].cookieChange.userInfos, ]; userInfos = mergeUserInfos(...userInfosArrays); } return { threadInfos, currentUserInfo: responses[authKeyserverID]?.currentUserInfo, calendarResult, messagesResult, userInfos, updatesCurrentAsOf, authActionSource: keyserverAuthInfo.authActionSource, notAcknowledgedPolicies: responses[authKeyserverID]?.notAcknowledgedPolicies, preRequestUserInfo, }; }; const identityRegisterActionTypes = Object.freeze({ started: 'IDENTITY_REGISTER_STARTED', success: 'IDENTITY_REGISTER_SUCCESS', failed: 'IDENTITY_REGISTER_FAILED', }); function useIdentityPasswordRegister(): ( username: string, password: string, fid: ?string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); if (!identityClient.registerPasswordUser) { throw new Error('Register password user method unimplemented'); } const { registerPasswordUser } = identityClient; const { markPrekeysAsPublished } = getConfig().olmAPI; return React.useCallback( async (username: string, password: string, fid: ?string) => { const response = await registerPasswordUser(username, password, fid); try { await markPrekeysAsPublished(); } catch (e) { console.log( 'Failed to mark prekeys as published:', getMessageForException(e), ); } return response; }, [registerPasswordUser, markPrekeysAsPublished], ); } function useIdentityWalletRegister(): ( walletAddress: string, siweMessage: string, siweSignature: string, fid: ?string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); if (!identityClient.registerWalletUser) { throw new Error('Register wallet user method unimplemented'); } const { registerWalletUser } = identityClient; const { markPrekeysAsPublished } = getConfig().olmAPI; return React.useCallback( async ( walletAddress: string, siweMessage: string, siweSignature: string, fid: ?string, ) => { const response = await registerWalletUser( walletAddress, siweMessage, siweSignature, fid, ); try { await markPrekeysAsPublished(); } catch (e) { console.log( 'Failed to mark prekeys as published:', getMessageForException(e), ); } return response; }, [registerWalletUser, markPrekeysAsPublished], ); } const identityGenerateNonceActionTypes = Object.freeze({ started: 'IDENTITY_GENERATE_NONCE_STARTED', success: 'IDENTITY_GENERATE_NONCE_SUCCESS', failed: 'IDENTITY_GENERATE_NONCE_FAILED', }); function useIdentityGenerateNonce(): () => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); return identityClient.generateNonce; } function mergeUserInfos( ...userInfoArrays: Array<$ReadOnlyArray> ): UserInfo[] { const merged: { [string]: UserInfo } = {}; for (const userInfoArray of userInfoArrays) { for (const userInfo of userInfoArray) { merged[userInfo.id] = userInfo; } } const flattened = []; for (const id in merged) { flattened.push(merged[id]); } return flattened; } type WritableGenericMessagesResult = { messageInfos: RawMessageInfo[], truncationStatus: MessageTruncationStatuses, watchedIDsAtRequestTime: string[], currentAsOf: { [keyserverID: string]: number }, }; type WritableCalendarResult = { rawEntryInfos: RawEntryInfo[], calendarQuery: CalendarQuery, }; const identityLogInActionTypes = Object.freeze({ started: 'IDENTITY_LOG_IN_STARTED', success: 'IDENTITY_LOG_IN_SUCCESS', failed: 'IDENTITY_LOG_IN_FAILED', }); function useIdentityPasswordLogIn(): ( username: string, password: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const preRequestUserState = useSelector(state => state.currentUserInfo); const callClaimUsername = useClaimUsername(); const { markPrekeysAsPublished } = getConfig().olmAPI; return React.useCallback( (username, password) => { if (!identityClient) { throw new Error('Identity service client is not initialized'); } return (async () => { let result; try { result = await identityClient.logInPasswordUser(username, password); } catch (e) { const { registerReservedPasswordUser } = identityClient; if ( !registerReservedPasswordUser || getMessageForException(e) !== 'need_keyserver_message_to_claim_username' ) { throw e; } let message, signature; try { const callClaimUsernameResult = await callClaimUsername({ username, password, }); ({ message, signature } = callClaimUsernameResult); } catch (claimException) { if ( getMessageForException(claimException) === 'invalid_credentials' ) { // This error means the password was incorrect throw new Error('login_failed'); } else { throw claimException; } } result = await registerReservedPasswordUser( username, password, message, signature, ); } try { await markPrekeysAsPublished(); } catch (e) { console.log( 'Failed to mark prekeys as published:', getMessageForException(e), ); } return { ...result, preRequestUserState, }; })(); }, [ identityClient, markPrekeysAsPublished, preRequestUserState, callClaimUsername, ], ); } function useIdentityWalletLogIn(): ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); const { logInWalletUser } = identityClient; const { markPrekeysAsPublished } = getConfig().olmAPI; return React.useCallback( async ( walletAddress: string, siweMessage: string, siweSignature: string, ) => { const response = await logInWalletUser( walletAddress, siweMessage, siweSignature, ); try { await markPrekeysAsPublished(); } catch (e) { console.log( 'Failed to mark prekeys as published:', getMessageForException(e), ); } return response; }, [logInWalletUser, markPrekeysAsPublished], ); } function useIdentitySecondaryDeviceLogIn(): ( userID: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); const { generateNonce, uploadKeysForRegisteredDeviceAndLogIn } = identityClient; const { signMessage, markPrekeysAsPublished } = getConfig().olmAPI; return React.useCallback( async (userID: string) => { const nonce = await generateNonce(); const nonceSignature = await signMessage(nonce); const response = await uploadKeysForRegisteredDeviceAndLogIn(userID, { nonce, nonceSignature, }); try { await markPrekeysAsPublished(); } catch (e) { console.log( 'Failed to mark prekeys as published:', getMessageForException(e), ); } return response; }, [ generateNonce, markPrekeysAsPublished, signMessage, uploadKeysForRegisteredDeviceAndLogIn, ], ); } const legacyLogInActionTypes = Object.freeze({ started: 'LEGACY_LOG_IN_STARTED', success: 'LEGACY_LOG_IN_SUCCESS', failed: 'LEGACY_LOG_IN_FAILED', }); const legacyLogInCallSingleKeyserverEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const legacyLogIn = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: LegacyLogInInfo) => Promise) => async logInInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { authActionSource, calendarQuery, keyserverIDs: inputKeyserverIDs, preRequestUserInfo, ...restLogInInfo } = logInInfo; // Eventually the list of keyservers will be fetched from the // identity service const keyserverIDs = inputKeyserverIDs ?? [authoritativeKeyserverID()]; const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs); const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver( calendarQuery, keyserverIDs, ); const requests: { [string]: LogInRequest } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = { ...restLogInInfo, deviceTokenUpdateRequest: logInInfo.deviceTokenUpdateRequest[keyserverID], source: authActionSource, watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], calendarQuery: calendarQueryPerKeyserver[keyserverID], platformDetails: getConfig().platformDetails, }; } const responses: { +[string]: ClientLogInResponse } = await callKeyserverEndpoint( 'log_in', requests, legacyLogInCallSingleKeyserverEndpointOptions, ); const userInfosArrays = []; let threadInfos: RawThreadInfos = {}; const calendarResult: WritableCalendarResult = { calendarQuery: logInInfo.calendarQuery, rawEntryInfos: [], }; const messagesResult: WritableGenericMessagesResult = { messageInfos: [], truncationStatus: {}, watchedIDsAtRequestTime: watchedIDs, currentAsOf: {}, }; let updatesCurrentAsOf: { +[string]: number } = {}; for (const keyserverID in responses) { threadInfos = { ...responses[keyserverID].cookieChange.threadInfos, ...threadInfos, }; if (responses[keyserverID].rawEntryInfos) { calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } messagesResult.messageInfos = messagesResult.messageInfos.concat( responses[keyserverID].rawMessageInfos, ); messagesResult.truncationStatus = { ...messagesResult.truncationStatus, ...responses[keyserverID].truncationStatuses, }; messagesResult.currentAsOf = { ...messagesResult.currentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; updatesCurrentAsOf = { ...updatesCurrentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; userInfosArrays.push(responses[keyserverID].userInfos); userInfosArrays.push(responses[keyserverID].cookieChange.userInfos); } const userInfos = mergeUserInfos(...userInfosArrays); return { threadInfos, currentUserInfo: responses[authoritativeKeyserverID()].currentUserInfo, calendarResult, messagesResult, userInfos, updatesCurrentAsOf, authActionSource: logInInfo.authActionSource, notAcknowledgedPolicies: responses[authoritativeKeyserverID()].notAcknowledgedPolicies, preRequestUserInfo, }; }; function useLegacyLogIn(): ( input: LegacyLogInInfo, ) => Promise { return useKeyserverCall(legacyLogIn); } const changeKeyserverUserPasswordActionTypes = Object.freeze({ started: 'CHANGE_KEYSERVER_USER_PASSWORD_STARTED', success: 'CHANGE_KEYSERVER_USER_PASSWORD_SUCCESS', failed: 'CHANGE_KEYSERVER_USER_PASSWORD_FAILED', }); const changeKeyserverUserPassword = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((passwordUpdate: PasswordUpdate) => Promise) => async passwordUpdate => { await callSingleKeyserverEndpoint('update_account', passwordUpdate); }; const changeIdentityUserPasswordActionTypes = Object.freeze({ started: 'CHANGE_IDENTITY_USER_PASSWORD_STARTED', success: 'CHANGE_IDENTITY_USER_PASSWORD_SUCCESS', failed: 'CHANGE_IDENTITY_USER_PASSWORD_FAILED', }); function useChangeIdentityUserPassword(): ( oldPassword: string, newPassword: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; return React.useCallback( (oldPassword, newPassword) => { if (!identityClient) { throw new Error('Identity service client is not initialized'); } return identityClient.changePassword(oldPassword, newPassword); }, [identityClient], ); } const searchUsersActionTypes = Object.freeze({ started: 'SEARCH_USERS_STARTED', success: 'SEARCH_USERS_SUCCESS', failed: 'SEARCH_USERS_FAILED', }); const searchUsers = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((usernamePrefix: string) => Promise) => async usernamePrefix => { const response = await callSingleKeyserverEndpoint('search_users', { prefix: usernamePrefix, }); return { userInfos: response.userInfos, }; }; const exactSearchUserActionTypes = Object.freeze({ started: 'EXACT_SEARCH_USER_STARTED', success: 'EXACT_SEARCH_USER_SUCCESS', failed: 'EXACT_SEARCH_USER_FAILED', }); const exactSearchUser = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((username: string) => Promise) => async username => { const response = await callSingleKeyserverEndpoint('exact_search_user', { username, }); return { userInfo: response.userInfo, }; }; const updateSubscriptionActionTypes = Object.freeze({ started: 'UPDATE_SUBSCRIPTION_STARTED', success: 'UPDATE_SUBSCRIPTION_SUCCESS', failed: 'UPDATE_SUBSCRIPTION_FAILED', }); const updateSubscription = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: SubscriptionUpdateRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'update_user_subscription', requests, ); const response = responses[keyserverID]; return { threadID: input.threadID, subscription: response.threadSubscription, }; }; type UseUpdateSubscriptionInput = $ReadOnly< | { +thick: false, ...SubscriptionUpdateRequest, } | { +thick: true, +threadInfo: ThreadInfo, ...SubscriptionUpdateRequest, }, >; function useUpdateSubscription(): ( input: UseUpdateSubscriptionInput, ) => Promise { const processAndSendDMOperation = useProcessAndSendDMOperation(); const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const keyserverCall = useKeyserverCall(updateSubscription); return React.useCallback( async (input: UseUpdateSubscriptionInput) => { if (!input.thick) { const { thick, ...rest } = input; return await keyserverCall({ ...rest }); } invariant(viewerID, 'viewerID must be set'); const { threadInfo, updatedFields } = input; const subscription = { ...threadInfo.currentUser.subscription, ...updatedFields, }; const op: DMChangeThreadSubscriptionOperation = { type: 'change_thread_subscription', time: Date.now(), threadID: threadInfo.id, creatorID: viewerID, subscription, }; const opSpecification: OutboundDMOperationSpecification = { type: dmOperationSpecificationTypes.OUTBOUND, op, recipients: { type: 'all_thread_members', threadID: threadInfo.type === thickThreadTypes.THICK_SIDEBAR && threadInfo.parentThreadID ? threadInfo.parentThreadID : threadInfo.id, }, }; await processAndSendDMOperation(opSpecification); return { threadID: threadInfo.id, subscription }; }, [keyserverCall, processAndSendDMOperation, viewerID], ); } const setUserSettingsActionTypes = Object.freeze({ started: 'SET_USER_SETTINGS_STARTED', success: 'SET_USER_SETTINGS_SUCCESS', failed: 'SET_USER_SETTINGS_FAILED', }); const setUserSettings = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: UpdateUserSettingsRequest) => Promise) => async input => { const requests: { [string]: UpdateUserSettingsRequest } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = input; } await callKeyserverEndpoint('update_user_settings', requests); }; function useSetUserSettings(): ( input: UpdateUserSettingsRequest, ) => Promise { return useKeyserverCall(setUserSettings); } const getOlmSessionInitializationDataActionTypes = Object.freeze({ started: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', success: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', failed: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', }); const getOlmSessionInitializationData = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( options?: ?CallSingleKeyserverEndpointOptions, ) => Promise) => async options => { const olmInitData = await callSingleKeyserverEndpoint( 'get_olm_session_initialization_data', {}, options, ); return { signedIdentityKeysBlob: olmInitData.signedIdentityKeysBlob, contentInitializationInfo: { ...olmInitData.contentInitializationInfo, oneTimeKey: getOneTimeKeyValuesFromBlob( olmInitData.contentInitializationInfo.oneTimeKey, )[0], prekey: getPrekeyValueFromBlob( olmInitData.contentInitializationInfo.prekey, ), }, notifInitializationInfo: { ...olmInitData.notifInitializationInfo, oneTimeKey: getOneTimeKeyValuesFromBlob( olmInitData.notifInitializationInfo.oneTimeKey, )[0], prekey: getPrekeyValueFromBlob( olmInitData.notifInitializationInfo.prekey, ), }, }; }; const policyAcknowledgmentActionTypes = Object.freeze({ started: 'POLICY_ACKNOWLEDGMENT_STARTED', success: 'POLICY_ACKNOWLEDGMENT_SUCCESS', failed: 'POLICY_ACKNOWLEDGMENT_FAILED', }); const policyAcknowledgment = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((policyRequest: PolicyAcknowledgmentRequest) => Promise) => async policyRequest => { await callSingleKeyserverEndpoint('policy_acknowledgment', policyRequest); }; const updateUserAvatarActionTypes = Object.freeze({ started: 'UPDATE_USER_AVATAR_STARTED', success: 'UPDATE_USER_AVATAR_SUCCESS', failed: 'UPDATE_USER_AVATAR_FAILED', }); const updateUserAvatar = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( avatarDBContent: UpdateUserAvatarRequest, ) => Promise) => async avatarDBContent => { const { updates }: UpdateUserAvatarResponse = await callSingleKeyserverEndpoint('update_user_avatar', avatarDBContent); return { updates }; }; const processNewUserIDsActionType = 'PROCESS_NEW_USER_IDS'; const versionSupportedByIdentityActionTypes = Object.freeze({ started: 'VERSION_SUPPORTED_BY_IDENTITY_STARTED', success: 'VERSION_SUPPORTED_BY_IDENTITY_SUCCESS', failed: 'VERSION_SUPPORTED_BY_IDENTITY_FAILED', }); function useVersionSupportedByIdentity(): () => Promise<{ +supported: boolean, }> { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); return async () => { const supported = await identityClient.versionSupported(); return { supported }; }; } +const restoreUserActionTypes = Object.freeze({ + started: 'RESTORE_USER_STARTED', + success: 'RESTORE_USER_SUCCESS', + failed: 'RESTORE_USER_FAILED', +}); + export { changeKeyserverUserPasswordActionTypes, changeKeyserverUserPassword, changeIdentityUserPasswordActionTypes, useChangeIdentityUserPassword, claimUsernameActionTypes, useClaimUsername, useDeleteKeyserverAccount, deleteKeyserverAccountActionTypes, getOlmSessionInitializationDataActionTypes, getOlmSessionInitializationData, mergeUserInfos, legacyLogIn as legacyLogInRawAction, identityLogInActionTypes, useIdentityPasswordLogIn, useIdentityWalletLogIn, useIdentitySecondaryDeviceLogIn, useLegacyLogIn, legacyLogInActionTypes, useLogOut, useIdentityLogOut, usePrimaryDeviceLogOut, useSecondaryDeviceLogOut, logOutActionTypes, legacyKeyserverRegister, legacyKeyserverRegisterActionTypes, searchUsers, searchUsersActionTypes, exactSearchUser, exactSearchUserActionTypes, useSetUserSettings, setUserSettingsActionTypes, useUpdateSubscription, updateSubscriptionActionTypes, policyAcknowledgment, policyAcknowledgmentActionTypes, updateUserAvatarActionTypes, updateUserAvatar, deleteAccountActionTypes, useDeleteAccount, useDeleteDiscardedIdentityAccount, keyserverAuthActionTypes, keyserverAuth as keyserverAuthRawAction, identityRegisterActionTypes, useIdentityPasswordRegister, useIdentityWalletRegister, identityGenerateNonceActionTypes, useIdentityGenerateNonce, processNewUserIDsActionType, versionSupportedByIdentityActionTypes, useVersionSupportedByIdentity, useInvalidCSATLogOut, + restoreUserActionTypes, }; diff --git a/lib/reducers/keyserver-reducer.js b/lib/reducers/keyserver-reducer.js index 611e5841a..61e64d5a4 100644 --- a/lib/reducers/keyserver-reducer.js +++ b/lib/reducers/keyserver-reducer.js @@ -1,749 +1,751 @@ // @flow import { filterThreadIDsInFilterList } from './calendar-filters-reducer.js'; import { unsupervisedBackgroundActionType } from './lifecycle-state-reducer.js'; import { updateActivityActionTypes } from '../actions/activity-actions.js'; import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; import { updateLastCommunicatedPlatformDetailsActionType, setDeviceTokenActionTypes, } from '../actions/device-actions.js'; import { updateCalendarQueryActionTypes } from '../actions/entry-actions.js'; import { addKeyserverActionType, removeKeyserverActionType, } from '../actions/keyserver-actions.js'; import { legacySiweAuthActionTypes } from '../actions/siwe-actions.js'; import { fetchPendingUpdatesActionTypes } from '../actions/update-actions.js'; import { identityLogInActionTypes, identityRegisterActionTypes, keyserverAuthActionTypes, logOutActionTypes, deleteKeyserverAccountActionTypes, deleteAccountActionTypes, legacyKeyserverRegisterActionTypes, legacyLogInActionTypes, + restoreUserActionTypes, } from '../actions/user-actions.js'; import { extractKeyserverIDFromIDOptional } from '../keyserver-conn/keyserver-call-utils.js'; import { setNewSessionActionType, updateConnectionStatusActionType, setLateResponseActionType, updateKeyserverReachabilityActionType, setConnectionIssueActionType, setActiveSessionRecoveryActionType, } from '../keyserver-conn/keyserver-conn-types.js'; import { keyserverStoreOpsHandlers, type ReplaceKeyserverOperation, type RemoveKeyserversOperation, type KeyserverStoreOperation, } from '../ops/keyserver-store-ops.js'; import { nonThreadCalendarFilters } from '../selectors/calendar-filter-selectors.js'; import { queueActivityUpdatesActionType } from '../types/activity-types.js'; import { defaultCalendarQuery } from '../types/entry-types.js'; import type { KeyserverInfos, KeyserverStore, } from '../types/keyserver-types.js'; import type { BaseAction } from '../types/redux-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 } from '../types/update-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { getConfig } from '../utils/config.js'; import { getMessageForException } from '../utils/errors.js'; import { assertObjectsAreEqual } from '../utils/objects.js'; import { setURLPrefix } from '../utils/url-utils.js'; function assertKeyserverStoresAreEqual( processedKeyserverStore: KeyserverInfos, expectedKeyserverStore: KeyserverInfos, location: string, onStateDifference?: (message: string) => mixed, ) { try { assertObjectsAreEqual( processedKeyserverStore, expectedKeyserverStore, `KeyserverInfos - ${location}`, ); } catch (e) { console.log( 'Error processing KeyserverStore ops', processedKeyserverStore, expectedKeyserverStore, ); const message = `Error processing KeyserverStore ops ${ getMessageForException(e) ?? '{no exception message}' }`; onStateDifference?.(message); } } function shouldClearDeviceToken( state: KeyserverStore, payload: | ClientStateSyncIncrementalSocketResult | StateSyncIncrementalActionPayload, ): boolean { const { keyserverID, updatesResult } = payload; for (const update of updatesResult.newUpdates) { if ( update.type === updateTypes.BAD_DEVICE_TOKEN && update.deviceToken === state.keyserverInfos[keyserverID].deviceToken ) { return true; } } return false; } const { processStoreOperations: processStoreOps } = keyserverStoreOpsHandlers; export default function reduceKeyserverStore( state: KeyserverStore, action: BaseAction, onStateDifference?: (message: string) => mixed, ): { keyserverStore: KeyserverStore, keyserverStoreOperations: $ReadOnlyArray, } { if (action.type === addKeyserverActionType) { const replaceOperation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: action.payload.keyserverAdminUserID, keyserverInfo: { ...action.payload.newKeyserverInfo, }, }, }; return { keyserverStore: processStoreOps(state, [replaceOperation]), keyserverStoreOperations: [replaceOperation], }; } else if (action.type === removeKeyserverActionType) { const removeOperation: RemoveKeyserversOperation = { type: 'remove_keyservers', payload: { ids: [action.payload.keyserverAdminUserID], }, }; return { keyserverStore: processStoreOps(state, [removeOperation]), keyserverStoreOperations: [removeOperation], }; } else if (action.type === setNewSessionActionType) { const { keyserverID, sessionChange } = action.payload; const gotUserCookie = sessionChange.cookie?.startsWith('user='); if (!state.keyserverInfos[keyserverID]) { if (gotUserCookie) { console.log( 'received sessionChange with user cookie, ' + `but keyserver ${keyserverID} is not in KeyserverStore!`, ); } return { keyserverStore: state, keyserverStoreOperations: [], }; } let newKeyserverInfo = state.keyserverInfos[keyserverID]; if (sessionChange.cookie !== undefined) { newKeyserverInfo = { ...newKeyserverInfo, cookie: sessionChange.cookie, connection: { ...newKeyserverInfo.connection, activeSessionRecovery: null, }, }; } if (sessionChange.cookieInvalidated) { newKeyserverInfo = { ...newKeyserverInfo, actualizedCalendarQuery: { ...newKeyserverInfo.actualizedCalendarQuery, filters: nonThreadCalendarFilters( newKeyserverInfo.actualizedCalendarQuery.filters, ), }, connection: { ...newKeyserverInfo.connection, queuedActivityUpdates: [], }, }; } if (action.payload.error === 'client_version_unsupported') { newKeyserverInfo = { ...newKeyserverInfo, connection: { ...newKeyserverInfo.connection, connectionIssue: 'client_version_unsupported', }, }; } const operations: ReplaceKeyserverOperation[] = []; if (newKeyserverInfo !== state.keyserverInfos[keyserverID]) { operations.push({ type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: newKeyserverInfo, }, }); } return { keyserverStore: processStoreOps(state, operations), keyserverStoreOperations: operations, }; } else if ( action.type === legacyLogInActionTypes.success || action.type === legacySiweAuthActionTypes.success || action.type === keyserverAuthActionTypes.success ) { const { updatesCurrentAsOf } = action.payload; const operations: ReplaceKeyserverOperation[] = []; for (const keyserverID in updatesCurrentAsOf) { const calendarFilters = filterThreadIDsInFilterList( action.payload.calendarResult.calendarQuery.filters, (threadID: string) => extractKeyserverIDFromIDOptional(threadID) === keyserverID, ); operations.push({ type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], updatesCurrentAsOf: updatesCurrentAsOf[keyserverID], lastCommunicatedPlatformDetails: getConfig().platformDetails, actualizedCalendarQuery: { ...action.payload.calendarResult.calendarQuery, filters: calendarFilters, }, connection: { ...state.keyserverInfos[keyserverID].connection, connectionIssue: null, activeSessionRecovery: null, }, }, }, }); } return { keyserverStore: processStoreOps(state, operations), keyserverStoreOperations: operations, }; } else if (action.type === fullStateSyncActionType) { const { keyserverID } = action.payload; const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], actualizedCalendarQuery: action.payload.calendarQuery, updatesCurrentAsOf: action.payload.updatesCurrentAsOf, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if ( action.type === fetchPendingUpdatesActionTypes.success && action.payload.type === stateSyncPayloadTypes.FULL ) { const { keyserverID } = action.payload; const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], updatesCurrentAsOf: action.payload.updatesCurrentAsOf, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if (action.type === incrementalStateSyncActionType) { const { payload } = action; const { keyserverID } = payload; const deviceToken = shouldClearDeviceToken(state, payload) ? null : state.keyserverInfos[keyserverID].deviceToken; const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], actualizedCalendarQuery: payload.calendarQuery, updatesCurrentAsOf: payload.updatesResult.currentAsOf, deviceToken, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if ( action.type === fetchPendingUpdatesActionTypes.success && action.payload.type === stateSyncPayloadTypes.INCREMENTAL ) { const { payload } = action; const { keyserverID } = payload; const deviceToken = shouldClearDeviceToken(state, payload) ? null : state.keyserverInfos[keyserverID].deviceToken; const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], updatesCurrentAsOf: payload.updatesResult.currentAsOf, deviceToken, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if (action.type === processUpdatesActionType) { const { keyserverID } = action.payload; const updatesCurrentAsOf = Math.max( action.payload.updatesResult.currentAsOf, state.keyserverInfos[keyserverID].updatesCurrentAsOf, ); const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], updatesCurrentAsOf, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if (action.type === setURLPrefix) { const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: authoritativeKeyserverID(), keyserverInfo: { ...state.keyserverInfos[authoritativeKeyserverID()], urlPrefix: action.payload, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if (action.type === updateLastCommunicatedPlatformDetailsActionType) { const { keyserverID, platformDetails } = action.payload; const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], lastCommunicatedPlatformDetails: platformDetails, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if (action.type === legacyKeyserverRegisterActionTypes.success) { const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: authoritativeKeyserverID(), keyserverInfo: { ...state.keyserverInfos[authoritativeKeyserverID()], actualizedCalendarQuery: action.payload.calendarQuery, lastCommunicatedPlatformDetails: getConfig().platformDetails, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if (action.type === updateConnectionStatusActionType) { const { keyserverID, status } = action.payload; if (!state.keyserverInfos[keyserverID]) { return { keyserverStore: state, keyserverStoreOperations: [], }; } const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], connection: { ...state.keyserverInfos[keyserverID].connection, status, lateResponses: [], }, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if (action.type === unsupervisedBackgroundActionType) { const { keyserverID } = action.payload; const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], connection: { ...state.keyserverInfos[keyserverID].connection, status: 'disconnected', lateResponses: [], }, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if (action.type === queueActivityUpdatesActionType) { const { activityUpdates, keyserverID } = action.payload; const oldConnection = state.keyserverInfos[keyserverID].connection; const connection = { ...oldConnection, queuedActivityUpdates: [ ...oldConnection.queuedActivityUpdates.filter(existingUpdate => { for (const activityUpdate of activityUpdates) { if ( ((existingUpdate.focus && activityUpdate.focus) || (existingUpdate.focus === false && activityUpdate.focus !== undefined)) && existingUpdate.threadID === activityUpdate.threadID ) { return false; } } return true; }), ...activityUpdates, ], }; const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], connection, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if (action.type === updateActivityActionTypes.success) { const { activityUpdates } = action.payload; const operations: ReplaceKeyserverOperation[] = []; for (const keyserverID in activityUpdates) { const oldConnection = state.keyserverInfos[keyserverID].connection; const queuedActivityUpdates = oldConnection.queuedActivityUpdates.filter( activityUpdate => !activityUpdates[keyserverID].includes(activityUpdate), ); operations.push({ type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], connection: { ...oldConnection, queuedActivityUpdates }, }, }, }); } return { keyserverStore: processStoreOps(state, operations), keyserverStoreOperations: operations, }; } else if ( action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success || action.type === identityRegisterActionTypes.success || + action.type === restoreUserActionTypes.success || (action.type === identityLogInActionTypes.success && action.payload.userID !== action.payload.preRequestUserState?.id) ) { // We want to remove all keyservers but Ashoat's keyserver const oldKeyserverInfo = state.keyserverInfos[authoritativeKeyserverID()]; const oldConnection = oldKeyserverInfo.connection; const operations: KeyserverStoreOperation[] = [ { type: 'remove_all_keyservers' }, ]; let cookie = oldKeyserverInfo.cookie; if ( action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success || cookie?.startsWith('user=') ) { // To avoid unnecessary rerenders, we won't clear the authoritative // keyserver's cookie unless it's a logout or an account deletion, or if // the cookie belongs to an actual user (as opposed to an anonymous one) cookie = null; } operations.push({ type: 'replace_keyserver', payload: { id: authoritativeKeyserverID(), keyserverInfo: { ...state.keyserverInfos[authoritativeKeyserverID()], actualizedCalendarQuery: defaultCalendarQuery( getConfig().platformDetails.platform, ), connection: { ...oldConnection, connectionIssue: null, activeSessionRecovery: null, queuedActivityUpdates: [], lateResponses: [], }, cookie, }, }, }); return { keyserverStore: processStoreOps(state, operations), keyserverStoreOperations: operations, }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const operations: KeyserverStoreOperation[] = [ { type: 'remove_keyservers', payload: { ids: action.payload.keyserverIDs }, }, ]; if (action.payload.keyserverIDs.includes(authoritativeKeyserverID())) { const oldConnection = state.keyserverInfos[authoritativeKeyserverID()].connection; operations.push({ type: 'replace_keyserver', payload: { id: authoritativeKeyserverID(), keyserverInfo: { ...state.keyserverInfos[authoritativeKeyserverID()], actualizedCalendarQuery: defaultCalendarQuery( getConfig().platformDetails.platform, ), connection: { ...oldConnection, connectionIssue: null, activeSessionRecovery: null, queuedActivityUpdates: [], lateResponses: [], }, cookie: null, }, }, }); } return { keyserverStore: processStoreOps(state, operations), keyserverStoreOperations: operations, }; } else if (action.type === setLateResponseActionType) { const { messageID, isLate, keyserverID } = action.payload; const lateResponsesSet = new Set( state.keyserverInfos[keyserverID].connection.lateResponses, ); if (isLate) { lateResponsesSet.add(messageID); } else { lateResponsesSet.delete(messageID); } const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], connection: { ...state.keyserverInfos[keyserverID].connection, lateResponses: [...lateResponsesSet], }, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if (action.type === updateKeyserverReachabilityActionType) { const { keyserverID } = action.payload; const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], connection: { ...state.keyserverInfos[keyserverID].connection, unreachable: action.payload.visible, }, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if (action.type === setDeviceTokenActionTypes.success) { const { deviceTokens } = action.payload; const operations: ReplaceKeyserverOperation[] = []; for (const keyserverID in deviceTokens) { operations.push({ type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], deviceToken: deviceTokens[keyserverID], }, }, }); } return { keyserverStore: processStoreOps(state, operations), keyserverStoreOperations: operations, }; } else if (action.type === setConnectionIssueActionType) { const { connectionIssue, keyserverID } = action.payload; const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], connection: { ...state.keyserverInfos[keyserverID].connection, connectionIssue, }, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if (action.type === setActiveSessionRecoveryActionType) { const { activeSessionRecovery, keyserverID } = action.payload; const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], connection: { ...state.keyserverInfos[keyserverID].connection, activeSessionRecovery, }, }, }, }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; } else if (action.type === setClientDBStoreActionType) { // Once the functionality is confirmed to work correctly, // we will proceed with returning keyserverInfos from the payload. assertKeyserverStoresAreEqual( action.payload.keyserverInfos ?? {}, state.keyserverInfos, action.type, onStateDifference, ); return { keyserverStore: state, keyserverStoreOperations: [], }; } else if (action.type === updateCalendarQueryActionTypes.success) { const operations: ReplaceKeyserverOperation[] = []; for (const keyserverID of action.payload.keyserverIDs) { operations.push({ type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], actualizedCalendarQuery: { ...action.payload.calendarQuery, filters: filterThreadIDsInFilterList( action.payload.calendarQuery.filters, (threadID: string) => extractKeyserverIDFromIDOptional(threadID) === keyserverID, ), }, }, }, }); } return { keyserverStore: processStoreOps(state, operations), keyserverStoreOperations: operations, }; } return { keyserverStore: state, keyserverStoreOperations: [], }; } diff --git a/lib/reducers/nav-reducer.js b/lib/reducers/nav-reducer.js index 14505b499..0fa518b9a 100644 --- a/lib/reducers/nav-reducer.js +++ b/lib/reducers/nav-reducer.js @@ -1,60 +1,62 @@ // @flow import { updateCalendarQueryActionTypes } from '../actions/entry-actions.js'; import { legacySiweAuthActionTypes } from '../actions/siwe-actions.js'; import { identityRegisterActionTypes, legacyLogInActionTypes, legacyKeyserverRegisterActionTypes, logOutActionTypes, deleteAccountActionTypes, identityLogInActionTypes, + restoreUserActionTypes, } from '../actions/user-actions.js'; import { defaultCalendarQuery } from '../types/entry-types.js'; import type { BaseNavInfo } from '../types/nav-types.js'; import type { BaseAction } from '../types/redux-types.js'; import { fullStateSyncActionType, incrementalStateSyncActionType, } from '../types/socket-types.js'; import { getConfig } from '../utils/config.js'; export default function reduceBaseNavInfo( state: T, action: BaseAction, ): T { if ( action.type === legacyLogInActionTypes.started || action.type === legacySiweAuthActionTypes.started || action.type === legacyKeyserverRegisterActionTypes.started || action.type === fullStateSyncActionType || action.type === incrementalStateSyncActionType ) { const { startDate, endDate } = action.payload.calendarQuery; return { ...state, startDate, endDate }; } else if (action.type === updateCalendarQueryActionTypes.started) { if (action.payload && action.payload.calendarQuery) { const { startDate, endDate } = action.payload.calendarQuery; return { ...state, startDate, endDate }; } } else if ( action.type === updateCalendarQueryActionTypes.success && !action.payload.calendarQueryAlreadyUpdated ) { const { startDate, endDate } = action.payload.calendarQuery; return { ...state, startDate, endDate }; } else if ( action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success || action.type === identityRegisterActionTypes.success || + action.type === restoreUserActionTypes.success || (action.type === identityLogInActionTypes.success && action.payload.userID !== action.payload.preRequestUserState?.id) ) { const { startDate, endDate } = defaultCalendarQuery( getConfig().platformDetails.platform, ); return { ...state, startDate, endDate }; } return state; } diff --git a/lib/reducers/user-reducer.js b/lib/reducers/user-reducer.js index bbd115e55..27c0b1cab 100644 --- a/lib/reducers/user-reducer.js +++ b/lib/reducers/user-reducer.js @@ -1,631 +1,633 @@ // @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 { findUserIdentitiesActionTypes } from '../actions/find-user-identities-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 { processNewUserIDsActionType, identityLogInActionTypes, identityRegisterActionTypes, deleteAccountActionTypes, keyserverAuthActionTypes, logOutActionTypes, legacyLogInActionTypes, legacyKeyserverRegisterActionTypes, setUserSettingsActionTypes, updateUserAvatarActionTypes, + restoreUserActionTypes, } 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 === restoreUserActionTypes.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(); } function discardKeyserverUsernames( newUserInfos: UserInfos, stateUserInfos: UserInfos, ): UserInfos { if (!usingCommServicesAccessToken) { return newUserInfos; } let result: UserInfos = {}; for (const id in newUserInfos) { const username = stateUserInfos[id] ? stateUserInfos[id].username : null; result = { ...result, [id]: { ...newUserInfos[id], username, }, }; } return result; } type ReduceUserInfosResult = [ UserStore, $ReadOnlyArray, $ReadOnlyArray, ]; function handleUserInfoUpdates( state: UserStore, payload: | ClientStateSyncIncrementalSocketResult | StateSyncIncrementalActionPayload | ClientUpdatesResultWithUserInfos, ): ReduceUserInfosResult { if (payload.keyserverID !== authoritativeKeyserverID()) { return [state, [], []]; } const keyserverUserInfos = _keyBy(userInfo => userInfo.id)(payload.userInfos); const newUserInfos = discardKeyserverUsernames( keyserverUserInfos, state.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 keyserverUserInfos: UserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const newUserInfos = discardKeyserverUsernames( keyserverUserInfos, state.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) => { const existingUserInfo = state.userInfos[userInfo.id]; if (!existingUserInfo) { return acc; } return { ...acc, [userInfo.id]: { ...existingUserInfo, 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 keyserverUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const newUserInfos = discardKeyserverUsernames( keyserverUserInfos, state.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 keyserverUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const newUserInfos = discardKeyserverUsernames( keyserverUserInfos, state.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 keyserverUserInfos = _keyBy(userInfo => userInfo.id)(userInfos); const newUserInfos = discardKeyserverUsernames( keyserverUserInfos, state.userInfos, ); const userStoreOps: UserStoreOperation[] = [ ...convertUserInfosToReplaceUserOps(newUserInfos), ]; 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) { const userIDs = action.payload.updatesResult.newUpdates.map( update => update.deletedUserID, ); const userStoreOps: UserStoreOperation[] = [ { type: 'remove_users', payload: { ids: userIDs }, }, ]; return [ { userInfos: processUserStoreOps(state.userInfos, userStoreOps), }, [], userStoreOps, ]; } else if (action.type === updateUserAvatarActionTypes.success) { const keyserverUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.updates.userInfos, ); const newUserInfos = discardKeyserverUsernames( keyserverUserInfos, state.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/types/redux-types.js b/lib/types/redux-types.js index 4ddf274b3..525c6ad8c 100644 --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -1,1697 +1,1713 @@ // @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, SetMissingDeviceListsPayload, } from './aux-user-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from './avatar-types.js'; import type { LocalLatestBackupInfo } from './backup-types.js'; import type { CommunityStore, AddCommunityPayload, FetchCommunityInfosResponse, ClientFetchAllCommunityInfosWithNamesResponse, CreateOrUpdateFarcasterChannelTagResponse, DeleteFarcasterChannelTagPayload, } from './community-types.js'; import type { DBOpsStore } from './db-ops-types.js'; import type { GetVersionActionPayload, LastCommunicatedPlatformDetails, } from './device-types.js'; import type { ProcessDMOpsPayload, QueuedDMOperations, QueueDMOpsPayload, PruneDMOpsQueuePayload, ClearQueuedThreadDMOpsPayload, ClearQueuedMessageDMOpsPayload, ClearQueuedEntryDMOpsPayload, ClearQueuedMembershipDMOpsPayload, } 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 { HolderStore, BlobHashAndHolder } from './holder-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 { GetOlmSessionInitializationDataResponse } from './olm-session-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 } 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 { ProcessHoldersStartedPayload, ProcessHoldersFailedPayload, ProcessHoldersFinishedPayload, } from '../actions/holder-actions.js'; import type { UpdateConnectionStatusPayload, SetLateResponsePayload, UpdateKeyserverReachabilityPayload, } from '../keyserver-conn/keyserver-conn-types.js'; import type { SendMessageError } from '../utils/errors.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, +holderStore: HolderStore, +initialStateLoaded: boolean, ... }; 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<{ +dispatchMetadata?: DispatchMetadata, ... | { +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: SendMessageError, +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: SendMessageError, +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: SendMessageError, +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: 'FETCH_ALL_COMMUNITY_INFOS_WITH_NAMES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_ALL_COMMUNITY_INFOS_WITH_NAMES_SUCCESS', +payload: ClientFetchAllCommunityInfosWithNamesResponse, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_ALL_COMMUNITY_INFOS_WITH_NAMES_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: '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, } | { +type: 'CLEAR_QUEUED_MESSAGE_DM_OPS', +payload: ClearQueuedMessageDMOpsPayload, } | { +type: 'CLEAR_QUEUED_ENTRY_DM_OPS', +payload: ClearQueuedEntryDMOpsPayload, } | { +type: 'CLEAR_QUEUED_MEMBERSHIP_DM_OPS', +payload: ClearQueuedMembershipDMOpsPayload, } | { +type: 'STORE_ESTABLISHED_HOLDER', +payload: BlobHashAndHolder, } | { +type: 'PROCESS_HOLDERS_STARTED', +payload: ProcessHoldersStartedPayload, +loadingInfo: LoadingInfo, } | { +type: 'PROCESS_HOLDERS_FAILED', +error: true, +payload: Error & ProcessHoldersFailedPayload, +loadingInfo: LoadingInfo, } | { +type: 'PROCESS_HOLDERS_SUCCESS', +payload: ProcessHoldersFinishedPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_MISSING_DEVICE_LISTS', +payload: SetMissingDeviceListsPayload, } | { +type: 'CREATE_USER_KEYS_BACKUP_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'CREATE_USER_KEYS_BACKUP_SUCCESS', +payload: LocalLatestBackupInfo, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_USER_KEYS_BACKUP_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, + } + | { + +type: 'RESTORE_USER_STARTED', + +loadingInfo: LoadingInfo, + +payload?: void, + } + | { + +type: 'RESTORE_USER_FAILED', + +error: true, + +payload: Error, + +loadingInfo: LoadingInfo, + } + | { + +type: 'RESTORE_USER_SUCCESS', + +payload: IdentityAuthResult, + +loadingInfo: LoadingInfo, }, }>; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); export type DispatchSource = 'tunnelbroker' | 'tab-sync'; // Data added when dispatching action as a result of message received // from other peer. It is used to send processing confirmation. export type InboundActionMetadata = { +messageID: string, +senderDeviceID: string, }; // Data added when dispatching action triggered locally, used to resolve // promise associated with sending messages and track failed messages. export type OutboundActionMetadata = { +dmOpID: string, }; export type DispatchMetadata = InboundActionMetadata | OutboundActionMetadata; export type SuperAction = { +type: string, +payload?: ActionPayload, +loadingInfo?: LoadingInfo, +error?: boolean, +dispatchSource?: DispatchSource, +dispatchMetadata?: DispatchMetadata, }; 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'; diff --git a/web/redux/services-access-token-reducer.js b/web/redux/services-access-token-reducer.js index 881fbb8db..668993808 100644 --- a/web/redux/services-access-token-reducer.js +++ b/web/redux/services-access-token-reducer.js @@ -1,20 +1,22 @@ // @flow import { identityLogInActionTypes, identityRegisterActionTypes, + restoreUserActionTypes, } from 'lib/actions/user-actions.js'; import type { Action } from './redux-setup.js'; function reduceServicesAccessToken(state: ?string, action: Action): ?string { if ( action.type === identityLogInActionTypes.success || + action.type === restoreUserActionTypes.success || action.type === identityRegisterActionTypes.success ) { return action.payload.accessToken; } return state; } export { reduceServicesAccessToken };