diff --git a/lib/handlers/db-ops-handler.react.js b/lib/handlers/db-ops-handler.react.js index 2f31634a9..ea8148b86 100644 --- a/lib/handlers/db-ops-handler.react.js +++ b/lib/handlers/db-ops-handler.react.js @@ -1,83 +1,86 @@ // @flow import * as React from 'react'; import { opsProcessingFinishedActionType } from '../actions/db-ops-actions.js'; import { usePeerToPeerCommunication } from '../tunnelbroker/peer-to-peer-context.js'; import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import type { DBOpsEntry } from '../types/db-ops-types.js'; import { type MessageProcessed, peerToPeerMessageTypes, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; import { getConfig } from '../utils/config.js'; import { getContentSigningKey } from '../utils/crypto-utils.js'; import { getMessageForException } from '../utils/errors.js'; import { useDispatch, useSelector } from '../utils/redux-utils.js'; function DBOpsHandler(): React.Node { const { sqliteAPI } = getConfig(); const { processDBStoreOperations } = sqliteAPI; const queueFront = useSelector(state => state.dbOpsStore.queuedOps[0]); const prevQueueFront = React.useRef(null); const { sendMessageToDevice } = useTunnelbroker(); const { processOutboundMessages } = usePeerToPeerCommunication(); const dispatch = useDispatch(); React.useEffect(() => { if (!queueFront || prevQueueFront.current === queueFront) { return; } prevQueueFront.current = queueFront; - const { ops, messageSourceMetadata, dmOpID } = queueFront; + const { ops, dispatchMetadata } = queueFront; void (async () => { if (ops) { await processDBStoreOperations(ops); if (ops.outboundP2PMessages && ops.outboundP2PMessages.length > 0) { const messageIDs = ops.outboundP2PMessages.map( message => message.messageID, ); - processOutboundMessages(messageIDs, dmOpID); + processOutboundMessages(messageIDs, dispatchMetadata?.dmOpID); } } dispatch({ type: opsProcessingFinishedActionType, }); - if (messageSourceMetadata) { + if (dispatchMetadata) { try { - const { messageID, senderDeviceID } = messageSourceMetadata; const deviceID = await getContentSigningKey(); + const { messageID, senderDeviceID } = dispatchMetadata; + if (!messageID || !senderDeviceID) { + return; + } const message: MessageProcessed = { type: peerToPeerMessageTypes.MESSAGE_PROCESSED, messageID, deviceID, }; await sendMessageToDevice({ deviceID: senderDeviceID, payload: JSON.stringify(message), }); await sqliteAPI.removeInboundP2PMessages([messageID]); } catch (e) { console.log( `Error while sending confirmation: ${ getMessageForException(e) ?? 'unknown error' }`, ); } } })(); }, [ queueFront, dispatch, processDBStoreOperations, sendMessageToDevice, sqliteAPI, processOutboundMessages, ]); return null; } export { DBOpsHandler }; diff --git a/lib/hooks/ops-hooks.js b/lib/hooks/ops-hooks.js index 3c8c35353..986b1869d 100644 --- a/lib/hooks/ops-hooks.js +++ b/lib/hooks/ops-hooks.js @@ -1,26 +1,25 @@ // @flow import * as React from 'react'; -import type { MessageSourceMetadata } from '../types/db-ops-types.js'; -import type { SuperAction } from '../types/redux-types.js'; +import type { DispatchMetadata, SuperAction } from '../types/redux-types.js'; import { useDispatch } from '../utils/redux-utils.js'; -function useDispatchWithMessageSource(): ( +function useDispatchWithMetadata(): ( action: SuperAction, - messageSourceMetadata: ?MessageSourceMetadata, + dispatchMetadata: ?DispatchMetadata, ) => mixed { const dispatch = useDispatch(); return React.useCallback( - (action: SuperAction, messageSourceMetadata: ?MessageSourceMetadata) => { - if (messageSourceMetadata) { - dispatch({ ...action, messageSourceMetadata }); + (action: SuperAction, dispatchMetadata: ?DispatchMetadata) => { + if (dispatchMetadata) { + dispatch({ ...action, dispatchMetadata }); } else { dispatch(action); } }, [dispatch], ); } -export { useDispatchWithMessageSource }; +export { useDispatchWithMetadata }; diff --git a/lib/reducers/db-ops-reducer.js b/lib/reducers/db-ops-reducer.js index 96d87454e..d0dd5dfe6 100644 --- a/lib/reducers/db-ops-reducer.js +++ b/lib/reducers/db-ops-reducer.js @@ -1,128 +1,113 @@ // @flow import { opsProcessingFinishedActionType } from '../actions/db-ops-actions.js'; import { extractKeyserverIDFromIDOptional } from '../keyserver-conn/keyserver-call-utils.js'; import type { MessageSearchStoreOperation } from '../message-search-types.js'; import type { MessageStoreOperation } from '../ops/message-store-ops.js'; -import type { - MessageSourceMetadata, - DBOpsStore, -} from '../types/db-ops-types.js'; -import { scheduleP2PMessagesActionType } from '../types/dm-ops.js'; +import type { DBOpsStore } from '../types/db-ops-types.js'; import { messageTypes } from '../types/message-types-enum.js'; -import type { BaseAction } from '../types/redux-types.js'; +import type { BaseAction, DispatchMetadata } from '../types/redux-types.js'; import type { StoreOperations } from '../types/store-ops-types.js'; import { values } from '../utils/objects.js'; function reduceDBOpsStore(store: DBOpsStore, action: BaseAction): DBOpsStore { if (action.type === opsProcessingFinishedActionType) { const [, ...rest] = store.queuedOps; return { ...store, queuedOps: rest, }; - } else if (action.type === scheduleP2PMessagesActionType) { - const newEntry = { - ops: { - outboundP2PMessages: action.payload.messages, - }, - dmOpID: action.payload.dmOpID, - }; - return { - ...store, - queuedOps: [...store.queuedOps, newEntry], - }; } return store; } function getMessageSearchStoreOps( messageStoreOps: ?$ReadOnlyArray, ): $ReadOnlyArray { if (!messageStoreOps) { return []; } const messageSearchStoreOps: MessageSearchStoreOperation[] = []; for (const messageOp of messageStoreOps) { if (messageOp.type === 'replace') { // We only create search index for thick threads, // and for non-local messages const { messageInfo } = messageOp.payload; if ( extractKeyserverIDFromIDOptional(messageInfo.threadID) || !messageInfo.id ) { continue; } if (messageInfo.type === messageTypes.TEXT) { messageSearchStoreOps.push({ type: 'update_search_messages', payload: { originalMessageID: messageInfo.id, messageID: messageInfo.id, content: messageInfo.text, }, }); } else if (messageInfo.type === messageTypes.EDIT_MESSAGE) { messageSearchStoreOps.push({ type: 'update_search_messages', payload: { originalMessageID: messageInfo.targetMessageID, messageID: messageInfo.id, content: messageInfo.text, }, }); } } } return messageSearchStoreOps; } function queueDBOps( store: DBOpsStore, - messageSourceMetadata: ?MessageSourceMetadata, + dispatchMetadata: ?DispatchMetadata, inputOps: StoreOperations, ): DBOpsStore { const areNewOpsPresent = values(inputOps).some( opsArray => opsArray.length > 0, ); let newEntry = null; const { messageStoreOperations } = inputOps; const messageSearchStoreOperations = getMessageSearchStoreOps( messageStoreOperations, ); const ops = { ...inputOps, messageSearchStoreOperations, }; - if (messageSourceMetadata && areNewOpsPresent) { + if (dispatchMetadata && areNewOpsPresent) { newEntry = { - messageSourceMetadata, + dispatchMetadata, ops, }; } else if (areNewOpsPresent) { newEntry = { ops, }; - } else if (messageSourceMetadata) { + } else if (dispatchMetadata) { newEntry = { - messageSourceMetadata, + dispatchMetadata, }; } if (!newEntry) { return store; } return { ...store, queuedOps: [...store.queuedOps, newEntry], }; } export { reduceDBOpsStore, queueDBOps, getMessageSearchStoreOps }; diff --git a/lib/reducers/db-ops-reducer.test.js b/lib/reducers/db-ops-reducer.test.js index 6c97ed778..ee70ce05b 100644 --- a/lib/reducers/db-ops-reducer.test.js +++ b/lib/reducers/db-ops-reducer.test.js @@ -1,43 +1,43 @@ // @flow import { reduceDBOpsStore } from './db-ops-reducer.js'; import { opsProcessingFinishedActionType } from '../actions/db-ops-actions.js'; import type { DBOpsStore } from '../types/db-ops-types.js'; const emptyOps = { draftStoreOperations: [], threadStoreOperations: [], messageStoreOperations: [], reportStoreOperations: [], userStoreOperations: [], keyserverStoreOperations: [], communityStoreOperations: [], integrityStoreOperations: [], syncedMetadataStoreOperations: [], auxUserStoreOperations: [], threadActivityStoreOperations: [], }; describe('DB ops reducer', () => { const store: DBOpsStore = { queuedOps: [ { ops: emptyOps, - messageSourceMetadata: { messageID: '5', senderDeviceID: '' }, + dispatchMetadata: { messageID: '5', senderDeviceID: '' }, }, { ops: emptyOps }, { ops: null, - messageSourceMetadata: { messageID: '7', senderDeviceID: '' }, + dispatchMetadata: { messageID: '7', senderDeviceID: '' }, }, ], }; it('should remove the first entry', () => { const newState = reduceDBOpsStore(store, { type: opsProcessingFinishedActionType, }); expect(newState.queuedOps.length).toEqual(2); - expect(newState.queuedOps[1].messageSourceMetadata?.messageID).toEqual('7'); + expect(newState.queuedOps[1].dispatchMetadata?.messageID).toEqual('7'); }); }); diff --git a/lib/shared/dm-ops/dm-op-utils.js b/lib/shared/dm-ops/dm-op-utils.js index d3a03b7a7..0a94a8370 100644 --- a/lib/shared/dm-ops/dm-op-utils.js +++ b/lib/shared/dm-ops/dm-op-utils.js @@ -1,111 +1,111 @@ // @flow import uuid from 'uuid'; -import type { MessageSourceMetadata } from '../../types/db-ops-types.js'; import type { DMOperation } from '../../types/dm-ops.js'; +import type { InboundActionMetadata } from '../../types/redux-types.js'; import { outboundP2PMessageStatuses, type OutboundP2PMessage, } from '../../types/sqlite-types.js'; import { type DMOperationP2PMessage, userActionsP2PMessageTypes, } from '../../types/tunnelbroker/user-actions-peer-to-peer-message-types.js'; import type { CurrentUserInfo } from '../../types/user-types.js'; import { getContentSigningKey } from '../../utils/crypto-utils.js'; function generateMessagesToPeers( message: DMOperation, peers: $ReadOnlyArray<{ +userID: string, +deviceID: string, }>, supportsAutoRetry: boolean, ): $ReadOnlyArray { const opMessage: DMOperationP2PMessage = { type: userActionsP2PMessageTypes.DM_OPERATION, op: message, }; const plaintext = JSON.stringify(opMessage); const outboundP2PMessages = []; for (const peer of peers) { const messageToPeer: OutboundP2PMessage = { messageID: uuid.v4(), deviceID: peer.deviceID, userID: peer.userID, timestamp: new Date().getTime().toString(), plaintext, ciphertext: '', status: outboundP2PMessageStatuses.persisted, supportsAutoRetry, }; outboundP2PMessages.push(messageToPeer); } return outboundP2PMessages; } export const dmOperationSpecificationTypes = Object.freeze({ OUTBOUND: 'OutboundDMOperationSpecification', INBOUND: 'InboundDMOperationSpecification', }); // The operation generated on the sending client, causes changes to // the state and broadcasting information to peers. export type OutboundDMOperationSpecification = { +type: 'OutboundDMOperationSpecification', +op: DMOperation, +supportsAutoRetry: boolean, +recipients: | { +type: 'all_peer_devices' | 'self_devices' } | { +type: 'some_users', +userIDs: $ReadOnlyArray }, }; // The operation received from other peers, causes changes to // the state and after processing, sends confirmation to the sender. export type InboundDMOperationSpecification = { +type: 'InboundDMOperationSpecification', +op: DMOperation, - +metadata: ?MessageSourceMetadata, + +metadata: ?InboundActionMetadata, }; export type DMOperationSpecification = | OutboundDMOperationSpecification | InboundDMOperationSpecification; async function createMessagesToPeersFromDMOp( operation: OutboundDMOperationSpecification, allPeerUserIDAndDeviceIDs: $ReadOnlyArray<{ +userID: string, +deviceID: string, }>, currentUserInfo: ?CurrentUserInfo, ): Promise<$ReadOnlyArray> { if (!currentUserInfo?.id) { return []; } let peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs; if (operation.recipients.type === 'self_devices') { peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter( peer => peer.userID === currentUserInfo.id, ); } else if (operation.recipients.type === 'some_users') { const userIDs = new Set(operation.recipients.userIDs); peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter(peer => userIDs.has(peer.userID), ); } const thisDeviceID = await getContentSigningKey(); const targetPeers = peerUserIDAndDeviceIDs.filter( peer => peer.deviceID !== thisDeviceID, ); return generateMessagesToPeers( operation.op, targetPeers, operation.supportsAutoRetry, ); } export { createMessagesToPeersFromDMOp }; diff --git a/lib/shared/dm-ops/process-dm-ops.js b/lib/shared/dm-ops/process-dm-ops.js index f9d4f59e9..5a4d3cb62 100644 --- a/lib/shared/dm-ops/process-dm-ops.js +++ b/lib/shared/dm-ops/process-dm-ops.js @@ -1,200 +1,200 @@ // @flow import _groupBy from 'lodash/fp/groupBy.js'; import * as React from 'react'; import uuid from 'uuid'; import { dmOpSpecs } from './dm-op-specs.js'; import type { OutboundDMOperationSpecification, DMOperationSpecification, } from './dm-op-utils.js'; import { createMessagesToPeersFromDMOp, dmOperationSpecificationTypes, } from './dm-op-utils.js'; import { useLoggedInUserInfo } from '../../hooks/account-hooks.js'; import { useGetLatestMessageEdit } from '../../hooks/latest-message-edit.js'; -import { useDispatchWithMessageSource } from '../../hooks/ops-hooks.js'; +import { useDispatchWithMetadata } from '../../hooks/ops-hooks.js'; import { mergeUpdatesWithMessageInfos } from '../../reducers/message-reducer.js'; import { getAllPeerUserIDAndDeviceIDs } from '../../selectors/user-selectors.js'; import { processDMOpsActionType, queueDMOpsActionType, } from '../../types/dm-ops.js'; import type { RawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { OutboundP2PMessage } from '../../types/sqlite-types.js'; import { threadTypes } from '../../types/thread-types-enum.js'; import type { LegacyRawThreadInfo } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { useSelector } from '../../utils/redux-utils.js'; import { messageSpecs } from '../messages/message-specs.js'; import { updateSpecs } from '../updates/update-specs.js'; function useProcessDMOperation(): ( dmOperationSpecification: DMOperationSpecification, ) => Promise { const fetchMessage = useGetLatestMessageEdit(); const threadInfos = useSelector(state => state.threadStore.threadInfos); const utilities = React.useMemo( () => ({ fetchMessage, threadInfos, }), [fetchMessage, threadInfos], ); - const dispatchWithMessageSource = useDispatchWithMessageSource(); + const dispatchWithMetadata = useDispatchWithMetadata(); const loggedInUserInfo = useLoggedInUserInfo(); const viewerID = loggedInUserInfo?.id; const allPeerUserIDAndDeviceIDs = useSelector(getAllPeerUserIDAndDeviceIDs); const currentUserInfo = useSelector(state => state.currentUserInfo); return React.useCallback( async (dmOperationSpecification: DMOperationSpecification) => { if (!viewerID) { console.log('ignored DMOperation because logged out'); return; } let outboundP2PMessages: ?$ReadOnlyArray = null; if ( dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND ) { outboundP2PMessages = await createMessagesToPeersFromDMOp( dmOperationSpecification, allPeerUserIDAndDeviceIDs, currentUserInfo, ); } const { op: dmOp, metadata } = dmOperationSpecification; const processingCheckResult = dmOpSpecs[dmOp.type].canBeProcessed( dmOp, viewerID, utilities, ); if (!processingCheckResult.isProcessingPossible) { if (processingCheckResult.reason.type === 'missing_thread') { - dispatchWithMessageSource( + dispatchWithMetadata( { type: queueDMOpsActionType, payload: { operation: dmOp, threadID: processingCheckResult.reason.threadID, timestamp: Date.now(), }, }, metadata, ); } return; } const { rawMessageInfos, updateInfos } = await dmOpSpecs[ dmOp.type ].processDMOperation(dmOp, viewerID, utilities); const { rawMessageInfos: allNewMessageInfos } = mergeUpdatesWithMessageInfos(rawMessageInfos, updateInfos); const messagesByThreadID = _groupBy(message => message.threadID)( allNewMessageInfos, ); const updatedThreadInfosByThreadID: { [string]: RawThreadInfo | LegacyRawThreadInfo, } = {}; for (const threadID in messagesByThreadID) { updatedThreadInfosByThreadID[threadID] = threadInfos[threadID]; } for (const update of updateInfos) { const updatedThreadInfo = updateSpecs[ update.type ].getUpdatedThreadInfo?.(update, updatedThreadInfosByThreadID); if ( updatedThreadInfo && updatedThreadInfo?.type === threadTypes.THICK_SIDEBAR ) { updatedThreadInfosByThreadID[updatedThreadInfo.id] = updatedThreadInfo; } } for (const threadID in messagesByThreadID) { const repliesCountIncreasingMessages = messagesByThreadID[ threadID ].filter(message => messageSpecs[message.type].includedInRepliesCount); if (repliesCountIncreasingMessages.length > 0) { const threadInfo = updatedThreadInfosByThreadID[threadID]; const repliesCountIncreaseTime = Math.max( repliesCountIncreasingMessages.map(message => message.time), ); updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time: repliesCountIncreaseTime, threadInfo: { ...threadInfo, repliesCount: threadInfo.repliesCount + repliesCountIncreasingMessages.length, }, }); } const messagesFromOtherPeers = messagesByThreadID[threadID].filter( message => message.creatorID !== viewerID, ); if (messagesFromOtherPeers.length === 0) { continue; } // We take the most recent timestamp to make sure that updates older // than it won't flip the status to read. const time = Math.max( messagesFromOtherPeers.map(message => message.time), ); updateInfos.push({ type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID, unread: true, }); } - dispatchWithMessageSource( + dispatchWithMetadata( { type: processDMOpsActionType, payload: { rawMessageInfos, updateInfos, outboundP2PMessages, }, }, metadata, ); }, [ viewerID, utilities, - dispatchWithMessageSource, + dispatchWithMetadata, allPeerUserIDAndDeviceIDs, currentUserInfo, threadInfos, ], ); } function useProcessAndSendDMOperation(): ( dmOperationSpecification: OutboundDMOperationSpecification, ) => Promise { const processDMOps = useProcessDMOperation(); return React.useCallback( async (dmOperationSpecification: OutboundDMOperationSpecification) => { await processDMOps(dmOperationSpecification); }, [processDMOps], ); } export { useProcessDMOperation, useProcessAndSendDMOperation }; diff --git a/lib/types/db-ops-types.js b/lib/types/db-ops-types.js index acead3265..99683f007 100644 --- a/lib/types/db-ops-types.js +++ b/lib/types/db-ops-types.js @@ -1,22 +1,17 @@ // @flow +import { type DispatchMetadata } from './redux-types.js'; import type { StoreOperations } from './store-ops-types.js'; -export type MessageSourceMetadata = { - +messageID: string, - +senderDeviceID: string, -}; - export type DBOpsEntry = | { - +messageSourceMetadata: MessageSourceMetadata, + +dispatchMetadata: DispatchMetadata, +ops?: ?StoreOperations, } | { +ops: StoreOperations, - +dmOpID?: string, }; export type DBOpsStore = { +queuedOps: $ReadOnlyArray, }; diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js index f88cb8033..166478689 100644 --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -1,387 +1,381 @@ // @flow import t, { type TInterface, type TUnion, type TStructProps } from 'tcomb'; import { clientAvatarValidator, type ClientAvatar } from './avatar-types.js'; import type { RawMessageInfo } from './message-types.js'; import type { OutboundP2PMessage } from './sqlite-types.js'; import { type NonSidebarThickThreadType, nonSidebarThickThreadTypes, type ThickThreadType, thickThreadTypeValidator, } from './thread-types-enum.js'; import type { ClientUpdateInfo } from './update-types.js'; import { values } from '../utils/objects.js'; import { tColor, tShape, tString, tUserID } from '../utils/validation-utils.js'; export const dmOperationTypes = Object.freeze({ CREATE_THREAD: 'create_thread', CREATE_SIDEBAR: 'create_sidebar', SEND_TEXT_MESSAGE: 'send_text_message', SEND_REACTION_MESSAGE: 'send_reaction_message', SEND_EDIT_MESSAGE: 'send_edit_message', ADD_MEMBERS: 'add_members', ADD_VIEWER_TO_THREAD_MEMBERS: 'add_viewer_to_thread_members', JOIN_THREAD: 'join_thread', LEAVE_THREAD: 'leave_thread', REMOVE_MEMBERS: 'remove_members', CHANGE_THREAD_SETTINGS: 'change_thread_settings', CHANGE_THREAD_SETTINGS_AND_ADD_VIEWER: 'change_thread_settings_and_add_viewer', }); export type DMOperationType = $Values; export type CreateThickRawThreadInfoInput = { +threadID: string, +threadType: ThickThreadType, +creationTime: number, +parentThreadID?: ?string, +allMemberIDs: $ReadOnlyArray, +roleID: string, +creatorID: string, +name?: ?string, +avatar?: ?ClientAvatar, +description?: ?string, +color?: ?string, +containingThreadID?: ?string, +sourceMessageID?: ?string, +repliesCount?: ?number, +pinnedCount?: ?number, }; export const createThickRawThreadInfoInputValidator: TInterface = tShape({ threadID: t.String, threadType: thickThreadTypeValidator, creationTime: t.Number, parentThreadID: t.maybe(t.String), allMemberIDs: t.list(tUserID), roleID: t.String, creatorID: tUserID, name: t.maybe(t.String), avatar: t.maybe(clientAvatarValidator), description: t.maybe(t.String), color: t.maybe(t.String), containingThreadID: t.maybe(t.String), sourceMessageID: t.maybe(t.String), repliesCount: t.maybe(t.Number), pinnedCount: t.maybe(t.Number), }); export type DMCreateThreadOperation = { +type: 'create_thread', +threadID: string, +creatorID: string, +time: number, +threadType: NonSidebarThickThreadType, +memberIDs: $ReadOnlyArray, +roleID: string, +newMessageID: string, }; export const dmCreateThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CREATE_THREAD), threadID: t.String, creatorID: tUserID, time: t.Number, threadType: t.enums.of(values(nonSidebarThickThreadTypes)), memberIDs: t.list(tUserID), roleID: t.String, newMessageID: t.String, }); export type DMCreateSidebarOperation = { +type: 'create_sidebar', +threadID: string, +creatorID: string, +time: number, +parentThreadID: string, +memberIDs: $ReadOnlyArray, +sourceMessageID: string, +roleID: string, +newSidebarSourceMessageID: string, +newCreateSidebarMessageID: string, }; export const dmCreateSidebarOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CREATE_SIDEBAR), threadID: t.String, creatorID: tUserID, time: t.Number, parentThreadID: t.String, memberIDs: t.list(tUserID), sourceMessageID: t.String, roleID: t.String, newSidebarSourceMessageID: t.String, newCreateSidebarMessageID: t.String, }); export type DMSendTextMessageOperation = { +type: 'send_text_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +text: string, }; export const dmSendTextMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_TEXT_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, text: t.String, }); export type DMSendReactionMessageOperation = { +type: 'send_reaction_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +targetMessageID: string, +reaction: string, +action: 'add_reaction' | 'remove_reaction', }; export const dmSendReactionMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_REACTION_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, targetMessageID: t.String, reaction: t.String, action: t.enums.of(['add_reaction', 'remove_reaction']), }); export type DMSendEditMessageOperation = { +type: 'send_edit_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +targetMessageID: string, +text: string, }; export const dmSendEditMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_EDIT_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, targetMessageID: t.String, text: t.String, }); type DMAddMembersBase = { +editorID: string, +time: number, +messageID: string, +addedUserIDs: $ReadOnlyArray, }; const dmAddMembersBaseValidatorShape = { editorID: tUserID, time: t.Number, messageID: t.String, addedUserIDs: t.list(tUserID), }; export type DMAddMembersOperation = $ReadOnly<{ +type: 'add_members', +threadID: string, ...DMAddMembersBase, }>; export const dmAddMembersOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_MEMBERS), threadID: t.String, ...dmAddMembersBaseValidatorShape, }); export type DMAddViewerToThreadMembersOperation = $ReadOnly<{ +type: 'add_viewer_to_thread_members', +existingThreadDetails: CreateThickRawThreadInfoInput, ...DMAddMembersBase, }>; export const dmAddViewerToThreadMembersValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_VIEWER_TO_THREAD_MEMBERS), existingThreadDetails: createThickRawThreadInfoInputValidator, ...dmAddMembersBaseValidatorShape, }); export type DMJoinThreadOperation = { +type: 'join_thread', +joinerID: string, +time: number, +messageID: string, +existingThreadDetails: CreateThickRawThreadInfoInput, }; export const dmJoinThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.JOIN_THREAD), joinerID: tUserID, time: t.Number, messageID: t.String, existingThreadDetails: createThickRawThreadInfoInputValidator, }); export type DMLeaveThreadOperation = { +type: 'leave_thread', +editorID: string, +time: number, +messageID: string, +threadID: string, }; export const dmLeaveThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.LEAVE_THREAD), editorID: tUserID, time: t.Number, messageID: t.String, threadID: t.String, }); export type DMRemoveMembersOperation = { +type: 'remove_members', +editorID: string, +time: number, +messageID: string, +threadID: string, +removedUserIDs: $ReadOnlyArray, }; export const dmRemoveMembersOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.REMOVE_MEMBERS), editorID: tUserID, time: t.Number, messageID: t.String, threadID: t.String, removedUserIDs: t.list(tUserID), }); type DMChangeThreadSettingsBase = { +editorID: string, +time: number, +changes: { +name?: string, +description?: string, +color?: string, +newMemberIDs?: $ReadOnlyArray, +avatar?: ClientAvatar, }, +messageIDsPrefix: string, }; const dmChangeThreadSettingsBaseValidatorShape: TStructProps = { editorID: tUserID, time: t.Number, changes: tShape({ name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), newMemberIDs: t.maybe(t.list(tUserID)), avatar: t.maybe(clientAvatarValidator), }), messageIDsPrefix: t.String, }; export type DMChangeThreadSettingsOperation = $ReadOnly<{ +type: 'change_thread_settings', +threadID: string, ...DMChangeThreadSettingsBase, }>; export const dmChangeThreadSettingsOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS), threadID: t.String, ...dmChangeThreadSettingsBaseValidatorShape, }); export type DMChangeThreadSettingsAndAddViewerOperation = $ReadOnly<{ +type: 'change_thread_settings_and_add_viewer', +existingThreadDetails: CreateThickRawThreadInfoInput, ...DMChangeThreadSettingsBase, }>; export const dmChangeThreadSettingsAndAddViewerOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS_AND_ADD_VIEWER), existingThreadDetails: createThickRawThreadInfoInputValidator, ...dmChangeThreadSettingsBaseValidatorShape, }); export type DMOperation = | DMCreateThreadOperation | DMCreateSidebarOperation | DMSendTextMessageOperation | DMSendReactionMessageOperation | DMSendEditMessageOperation | DMAddMembersOperation | DMAddViewerToThreadMembersOperation | DMJoinThreadOperation | DMLeaveThreadOperation | DMRemoveMembersOperation | DMChangeThreadSettingsOperation | DMChangeThreadSettingsAndAddViewerOperation; export const dmOperationValidator: TUnion = t.union([ dmCreateThreadOperationValidator, dmCreateSidebarOperationValidator, dmSendTextMessageOperationValidator, dmSendReactionMessageOperationValidator, dmSendEditMessageOperationValidator, dmAddMembersOperationValidator, dmAddViewerToThreadMembersValidator, dmJoinThreadOperationValidator, dmLeaveThreadOperationValidator, dmRemoveMembersOperationValidator, dmChangeThreadSettingsOperationValidator, dmChangeThreadSettingsAndAddViewerOperationValidator, ]); export type DMOperationResult = { rawMessageInfos: Array, updateInfos: Array, }; export const processDMOpsActionType = 'PROCESS_DM_OPS'; export type ProcessDMOpsPayload = { +rawMessageInfos: $ReadOnlyArray, +updateInfos: $ReadOnlyArray, +outboundP2PMessages: ?$ReadOnlyArray, }; export const queueDMOpsActionType = 'QUEUE_DM_OPS'; export type QueueDMOpsPayload = { +operation: DMOperation, +threadID: string, +timestamp: number, }; export const pruneDMOpsQueueActionType = 'PRUNE_DM_OPS_QUEUE'; export type PruneDMOpsQueuePayload = { +pruneMaxTimestamp: number, }; -export const scheduleP2PMessagesActionType = 'SCHEDULE_P2P_MESSAGES'; -export type ScheduleP2PMessagesPayload = { - +dmOpID: string, - +messages: $ReadOnlyArray, -}; - export const clearQueuedThreadDMOpsActionType = 'CLEAR_QUEUED_THREAD_DM_OPS'; export type ClearQueuedThreadDMOpsPayload = { +threadID: string, }; export type QueuedDMOperations = { +operations: { +[threadID: string]: $ReadOnlyArray<{ +operation: DMOperation, +timestamp: number, }>, }, }; diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js index 8035f6f79..f13b0b085 100644 --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -1,1616 +1,1626 @@ // @flow import type { PersistState } from 'redux-persist/es/types'; import type { LogOutResult, KeyserverLogOutResult, LegacyLogInStartingPayload, LegacyLogInResult, LegacyRegisterResult, DefaultNotificationPayload, ClaimUsernameResponse, KeyserverAuthResult, } from './account-types.js'; import type { ActivityUpdateSuccessPayload, QueueActivityUpdatesPayload, SetThreadUnreadStatusPayload, } from './activity-types.js'; import type { AlertStore, RecordAlertActionPayload } from './alert-types.js'; import type { AuxUserStore, SetAuxUserFIDsPayload, AddAuxUserFIDsPayload, RemovePeerUsersPayload, SetPeerDeviceListsPayload, } from './aux-user-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from './avatar-types.js'; import type { CommunityStore, AddCommunityPayload, FetchCommunityInfosResponse, CreateOrUpdateFarcasterChannelTagResponse, DeleteFarcasterChannelTagPayload, } from './community-types.js'; -import type { MessageSourceMetadata, DBOpsStore } from './db-ops-types.js'; +import type { DBOpsStore } from './db-ops-types.js'; import type { GetVersionActionPayload, LastCommunicatedPlatformDetails, } from './device-types.js'; import type { - ScheduleP2PMessagesPayload, ProcessDMOpsPayload, QueuedDMOperations, QueueDMOpsPayload, PruneDMOpsQueuePayload, ClearQueuedThreadDMOpsPayload, } from './dm-ops.js'; import type { DraftStore } from './draft-types.js'; import type { EnabledApps, SupportedApps } from './enabled-apps.js'; import type { RawEntryInfo, EntryStore, SaveEntryPayload, CreateEntryPayload, DeleteEntryResult, RestoreEntryPayload, FetchEntryInfosResult, CalendarQueryUpdateResult, CalendarQueryUpdateStartingPayload, FetchRevisionsForEntryPayload, } from './entry-types.js'; import type { CalendarFilter, CalendarThreadFilter, SetCalendarDeletedFilterPayload, } from './filter-types.js'; import type { IdentityAuthResult } from './identity-service-types'; import type { IntegrityStore } from './integrity-types.js'; import type { KeyserverStore, AddKeyserverPayload, RemoveKeyserverPayload, } from './keyserver-types.js'; import type { LifecycleState } from './lifecycle-state-types.js'; import type { FetchInviteLinksResponse, InviteLink, InviteLinksStore, InviteLinkVerificationResponse, DisableInviteLinkPayload, } from './link-types.js'; import type { LoadingStatus, LoadingInfo } from './loading-types.js'; import type { UpdateMultimediaMessageMediaPayload } from './media-types.js'; import type { MessageReportCreationResult } from './message-report-types.js'; import type { MessageStore, RawMultimediaMessageInfo, FetchMessageInfosPayload, SendMessagePayload, EditMessagePayload, SaveMessagesPayload, NewMessagesPayload, MessageStorePrunePayload, LocallyComposedMessageInfo, SimpleMessagesPayload, FetchPinnedMessagesResult, SearchMessagesResponse, } from './message-types.js'; import type { RawReactionMessageInfo } from './messages/reaction.js'; import type { RawTextMessageInfo } from './messages/text.js'; import type { BaseNavInfo, WebNavInfo } from './nav-types.js'; import type { 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 { UpdateConnectionStatusPayload, SetLateResponsePayload, UpdateKeyserverReachabilityPayload, } from '../keyserver-conn/keyserver-conn-types.js'; export type BaseAppState = { +navInfo: NavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +alertStore: AlertStore, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +dataLoaded: boolean, +userPolicies: UserPolicies, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +integrityStore: IntegrityStore, +globalThemeInfo: GlobalThemeInfo, +customServer: ?string, +communityStore: CommunityStore, +dbOpsStore: DBOpsStore, +syncedMetadataStore: SyncedMetadataStore, +auxUserStore: AuxUserStore, +tunnelbrokerDeviceToken: TunnelbrokerDeviceToken, +_persist: ?PersistState, +queuedDMOperations: QueuedDMOperations, ... }; export type NativeAppState = BaseAppState<>; export type WebAppState = BaseAppState<> & { +pushApiPublicKey: ?string, ... }; export type AppState = NativeAppState | WebAppState; export type ClientWebInitialReduxStateResponse = { +navInfo: WebNavInfo, +currentUserInfo: CurrentUserInfo, +entryStore: EntryStore, +threadStore: ThreadStore, +userInfos: UserInfos, +messageStore: MessageStore, +pushApiPublicKey: ?string, +inviteLinksStore: InviteLinksStore, +keyserverInfo: WebInitialKeyserverInfo, }; export type ServerWebInitialReduxStateResponse = { +navInfo: WebNavInfo, +currentUserInfo: CurrentUserInfo, +entryStore: EntryStore, +threadStore: LegacyThreadStore, +userInfos: UserInfos, +messageStore: MessageStore, +pushApiPublicKey: ?string, +inviteLinksStore: InviteLinksStore, +keyserverInfo: WebInitialKeyserverInfo, }; export type WebInitialKeyserverInfo = { +sessionID: ?string, +updatesCurrentAsOf: number, }; export type BaseAction = $ReadOnly<{ - +messageSourceMetadata?: MessageSourceMetadata, + +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: Error & { +localID: string, +threadID: string, }, +loadingInfo?: LoadingInfo, } | { +type: 'SEND_TEXT_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawMultimediaMessageInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, }, +loadingInfo?: LoadingInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REACTION_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawReactionMessageInfo, } | { +type: 'SEND_REACTION_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, +targetMessageID: string, +reaction: string, +action: string, }, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REACTION_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_SUCCESS', +payload: UserSearchResult, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_SUCCESS', +payload: ExactUserSearchResult, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_DRAFT', +payload: { +key: string, +text: string, }, } | { +type: 'MOVE_DRAFT', +payload: { +oldKey: string, +newKey: string, }, } | { +type: 'SET_CLIENT_DB_STORE', +payload: ClientStore, } | { +type: 'UPDATE_ACTIVITY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_ACTIVITY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_ACTIVITY_SUCCESS', +payload: ActivityUpdateSuccessPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_STARTED', +payload: SetDeviceTokenStartedPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_SUCCESS', +payload: SetDeviceTokenActionPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_SUCCESS', +payload?: ClearDeliveredReportsPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_SUCCESS', +payload?: ClearDeliveredReportsPayload, +loadingInfo: LoadingInfo, } | { +type: 'QUEUE_REPORTS', +payload: QueueReportsPayload, } | { +type: 'SET_URL_PREFIX', +payload: string, } | { +type: 'SAVE_MESSAGES', +payload: SaveMessagesPayload, } | { +type: 'UPDATE_CALENDAR_THREAD_FILTER', +payload: CalendarThreadFilter, } | { +type: 'CLEAR_CALENDAR_THREAD_FILTER', +payload?: void, } | { +type: 'SET_CALENDAR_DELETED_FILTER', +payload: SetCalendarDeletedFilterPayload, } | { +type: 'UPDATE_SUBSCRIPTION_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_SUBSCRIPTION_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_SUBSCRIPTION_SUCCESS', +payload: SubscriptionUpdateResult, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_QUERY_STARTED', +loadingInfo: LoadingInfo, +payload?: CalendarQueryUpdateStartingPayload, } | { +type: 'UPDATE_CALENDAR_QUERY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_QUERY_SUCCESS', +payload: CalendarQueryUpdateResult, +loadingInfo: LoadingInfo, } | { +type: 'FULL_STATE_SYNC', +payload: StateSyncFullActionPayload, } | { +type: 'INCREMENTAL_STATE_SYNC', +payload: StateSyncIncrementalActionPayload, } | ProcessServerRequestAction | { +type: 'UPDATE_CONNECTION_STATUS', +payload: UpdateConnectionStatusPayload, } | { +type: 'QUEUE_ACTIVITY_UPDATES', +payload: QueueActivityUpdatesPayload, } | { +type: 'UNSUPERVISED_BACKGROUND', +payload: { +keyserverID: string }, } | { +type: 'UPDATE_LIFECYCLE_STATE', +payload: LifecycleState, } | { +type: 'ENABLE_APP', +payload: SupportedApps, } | { +type: 'DISABLE_APP', +payload: SupportedApps, } | { +type: 'UPDATE_REPORTS_ENABLED', +payload: Partial, } | { +type: 'PROCESS_UPDATES', +payload: ClientUpdatesResultWithUserInfos, } | { +type: 'PROCESS_MESSAGES', +payload: NewMessagesPayload, } | { +type: 'MESSAGE_STORE_PRUNE', +payload: MessageStorePrunePayload, } | { +type: 'SET_LATE_RESPONSE', +payload: SetLateResponsePayload, } | { +type: 'UPDATE_KEYSERVER_REACHABILITY', +payload: UpdateKeyserverReachabilityPayload, } | { +type: 'REQUEST_ACCESS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'REQUEST_ACCESS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'REQUEST_ACCESS_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_MULTIMEDIA_MESSAGE_MEDIA', +payload: UpdateMultimediaMessageMediaPayload, } | { +type: 'CREATE_LOCAL_MESSAGE', +payload: LocallyComposedMessageInfo, } | { +type: 'UPDATE_RELATIONSHIPS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_RELATIONSHIPS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_RELATIONSHIPS_SUCCESS', +payload: RelationshipErrors, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_STARTED', +payload: { +threadID: string, +unread: boolean, }, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_SUCCESS', +payload: SetThreadUnreadStatusPayload, } | { +type: 'SET_USER_SETTINGS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SET_USER_SETTINGS_SUCCESS', +payload: DefaultNotificationPayload, } | { +type: 'SET_USER_SETTINGS_FAILED', +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_SUCCESS', +payload: MessageReportCreationResult, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FORCE_POLICY_ACKNOWLEDGMENT', +payload: ForcePolicyAcknowledgmentPayload, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_SUCCESS', +payload: PolicyAcknowledgmentPayload, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_SIWE_AUTH_STARTED', +payload: LegacyLogInStartingPayload, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_SIWE_AUTH_SUCCESS', +payload: LegacyLogInResult, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_SIWE_AUTH_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'RECORD_ALERT', +payload: RecordAlertActionPayload, } | { +type: 'UPDATE_USER_AVATAR_STARTED', +payload: UpdateUserAvatarRequest, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_USER_AVATAR_SUCCESS', +payload: UpdateUserAvatarResponse, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_USER_AVATAR_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_EDIT_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'SEND_EDIT_MESSAGE_SUCCESS', +payload: EditMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_EDIT_MESSAGE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'TOGGLE_MESSAGE_PIN_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'TOGGLE_MESSAGE_PIN_SUCCESS', +payload: ToggleMessagePinResult, +loadingInfo: LoadingInfo, } | { +type: 'TOGGLE_MESSAGE_PIN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PINNED_MESSAGES_SUCCESS', +payload: FetchPinnedMessagesResult, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'VERIFY_INVITE_LINK_SUCCESS', +payload: InviteLinkVerificationResponse, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_SUCCESS', +payload: FetchInviteLinksResponse, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CALENDAR_COMMUNITY_FILTER', +payload: void, } | { +type: 'UPDATE_CHAT_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CHAT_COMMUNITY_FILTER', +payload: void, } | { +type: 'SEARCH_MESSAGES_STARTED', +payload: void, +loadingInfo?: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_SUCCESS', +payload: SearchMessagesResponse, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_SUCCESS', +payload: InviteLink, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DISABLE_INVITE_LINK_SUCCESS', +payload: DisableInviteLinkPayload, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', +payload: GetOlmSessionInitializationDataResponse, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DATA_LOADED', +payload: { +dataLoaded: boolean, }, } | { +type: 'GET_VERSION_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_VERSION_SUCCESS', +payload: GetVersionActionPayload, +loadingInfo: LoadingInfo, } | { +type: 'GET_VERSION_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_LAST_COMMUNICATED_PLATFORM_DETAILS', +payload: LastCommunicatedPlatformDetails, } | { +type: 'RESET_USER_STATE', +payload?: void } | { +type: 'MODIFY_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'MODIFY_COMMUNITY_ROLE_SUCCESS', +payload: RoleModificationPayload, +loadingInfo: LoadingInfo, } | { +type: 'MODIFY_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DELETE_COMMUNITY_ROLE_SUCCESS', +payload: RoleDeletionPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_THREAD_LAST_NAVIGATED', +payload: { +threadID: string, +time: number }, } | { +type: 'UPDATE_INTEGRITY_STORE', +payload: { +threadIDsToHash?: $ReadOnlyArray, +threadHashingStatus?: 'starting' | 'running' | 'completed', }, } | { +type: 'UPDATE_THEME_INFO', +payload: Partial, } | { +type: 'ADD_KEYSERVER', +payload: AddKeyserverPayload, } | { +type: 'REMOVE_KEYSERVER', +payload: RemoveKeyserverPayload, } | { +type: 'SET_CUSTOM_SERVER', +payload: string, } | { +type: 'SET_CONNECTION_ISSUE', +payload: { +connectionIssue: ?ConnectionIssue, +keyserverID: string }, } | { +type: 'ADD_COMMUNITY', +payload: AddCommunityPayload, } | { +type: 'SET_SYNCED_METADATA_ENTRY', +payload: SetSyncedMetadataEntryPayload, } | { +type: 'CLEAR_SYNCED_METADATA_ENTRY', +payload: ClearSyncedMetadataEntryPayload, } | { +type: 'SET_ACTIVE_SESSION_RECOVERY', +payload: SetActiveSessionRecoveryPayload, } | { +type: 'SET_AUX_USER_FIDS', +payload: SetAuxUserFIDsPayload, } | { +type: 'ADD_AUX_USER_FIDS', +payload: AddAuxUserFIDsPayload, } | { +type: 'CLEAR_AUX_USER_FIDS', +payload?: void, } | { +type: 'REMOVE_PEER_USERS', +payload: RemovePeerUsersPayload, } | { +type: 'SET_PEER_DEVICE_LISTS', +payload: SetPeerDeviceListsPayload, } | { +type: 'OPS_PROCESSING_FINISHED_ACTION_TYPE', +payload?: void, } | { +type: 'FETCH_COMMUNITY_INFOS_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_COMMUNITY_INFOS_SUCCESS', +payload: FetchCommunityInfosResponse, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_COMMUNITY_INFOS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_SUCCESS', +payload: CreateOrUpdateFarcasterChannelTagResponse, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_FARCASTER_CHANNEL_TAG_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DELETE_FARCASTER_CHANNEL_TAG_SUCCESS', +payload: DeleteFarcasterChannelTagPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_FARCASTER_CHANNEL_TAG_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'PROCESS_NEW_USER_IDS', +payload: { +userIDs: $ReadOnlyArray, }, +loadingInfo: LoadingInfo, } | { +type: 'FIND_USER_IDENTITIES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FIND_USER_IDENTITIES_SUCCESS', +payload: { +userInfos: $ReadOnlyArray, }, +loadingInfo: LoadingInfo, } | { +type: 'FIND_USER_IDENTITIES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'VERSION_SUPPORTED_BY_IDENTITY_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'VERSION_SUPPORTED_BY_IDENTITY_SUCCESS', +payload: { +supported: boolean, }, +loadingInfo: LoadingInfo, } | { +type: 'VERSION_SUPPORTED_BY_IDENTITY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PENDING_UPDATES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PENDING_UPDATES_SUCCESS', +payload: ClientStateSyncSocketResult, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PENDING_UPDATES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_TB_DEVICE_TOKEN_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'SET_TB_DEVICE_TOKEN_SUCCESS', +payload: { +deviceToken: string, }, +loadingInfo: LoadingInfo, } | { +type: 'SET_TB_DEVICE_TOKEN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'PROCESS_DM_OPS', +payload: ProcessDMOpsPayload, } - | { +type: 'SCHEDULE_P2P_MESSAGES', +payload: ScheduleP2PMessagesPayload } | { +type: 'INVALIDATE_TUNNELBROKER_DEVICE_TOKEN', +payload: { +deviceToken: string, }, } | { +type: 'QUEUE_DM_OPS', +payload: QueueDMOpsPayload } | { +type: 'PRUNE_DM_OPS_QUEUE', +payload: PruneDMOpsQueuePayload } | { +type: 'CLEAR_QUEUED_THREAD_DM_OPS', +payload: ClearQueuedThreadDMOpsPayload, }, }>; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); export type DispatchSource = 'tunnelbroker' | 'tab-sync'; +// 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, - +messageSourceMetadata?: MessageSourceMetadata, + +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/lib/types/request-types.js b/lib/types/request-types.js index 053790894..eaaa3ef81 100644 --- a/lib/types/request-types.js +++ b/lib/types/request-types.js @@ -1,337 +1,337 @@ // @flow import invariant from 'invariant'; import t, { type TUnion, type TInterface } from 'tcomb'; import { type ActivityUpdate, activityUpdateValidator, } from './activity-types.js'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; import { signedIdentityKeysBlobValidator } from './crypto-types.js'; -import type { MessageSourceMetadata } from './db-ops-types.js'; import type { Platform, PlatformDetails } from './device-types.js'; import { type RawEntryInfo, type CalendarQuery, rawEntryInfoValidator, } from './entry-types.js'; import type { RawThreadInfo } from './minimally-encoded-thread-permissions-types'; +import type { DispatchMetadata } from './redux-types.js'; import { type ThreadInconsistencyReportShape, type EntryInconsistencyReportShape, type ClientThreadInconsistencyReportShape, type ClientEntryInconsistencyReportShape, threadInconsistencyReportValidatorShape, entryInconsistencyReportValidatorShape, } from './report-types.js'; import type { LegacyRawThreadInfo } from './thread-types.js'; import { type CurrentUserInfo, currentUserInfoValidator, type AccountUserInfo, accountUserInfoValidator, } from './user-types.js'; import { mixedRawThreadInfoValidator } from '../permissions/minimally-encoded-raw-thread-info-validators.js'; import { tNumber, tShape, tID, tUserID, tPlatform, tPlatformDetails, } from '../utils/validation-utils.js'; // "Server requests" are requests for information that the server delivers to // clients. Clients then respond to those requests with a "client response". export const serverRequestTypes = Object.freeze({ PLATFORM: 0, //DEVICE_TOKEN: 1, (DEPRECATED) THREAD_INCONSISTENCY: 2, PLATFORM_DETAILS: 3, //INITIAL_ACTIVITY_UPDATE: 4, (DEPRECATED) ENTRY_INCONSISTENCY: 5, CHECK_STATE: 6, INITIAL_ACTIVITY_UPDATES: 7, // MORE_ONE_TIME_KEYS: 8, (DEPRECATED) SIGNED_IDENTITY_KEYS_BLOB: 9, INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE: 10, }); type ServerRequestType = $Values; export function assertServerRequestType( serverRequestType: number, ): ServerRequestType { invariant( serverRequestType === 0 || serverRequestType === 2 || serverRequestType === 3 || serverRequestType === 5 || serverRequestType === 6 || serverRequestType === 7 || serverRequestType === 9 || serverRequestType === 10, 'number is not ServerRequestType enum', ); return serverRequestType; } type PlatformServerRequest = { +type: 0, }; const platformServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.PLATFORM), }); type PlatformClientResponse = { +type: 0, +platform: Platform, }; const platformClientResponseValidator: TInterface = tShape({ type: tNumber(serverRequestTypes.PLATFORM), platform: tPlatform, }); export type ThreadInconsistencyClientResponse = { ...ThreadInconsistencyReportShape, +type: 2, }; const threadInconsistencyClientResponseValidator: TInterface = tShape({ ...threadInconsistencyReportValidatorShape, type: tNumber(serverRequestTypes.THREAD_INCONSISTENCY), }); type PlatformDetailsServerRequest = { type: 3, }; const platformDetailsServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.PLATFORM_DETAILS), }); type PlatformDetailsClientResponse = { type: 3, platformDetails: PlatformDetails, }; const platformDetailsClientResponseValidator: TInterface = tShape({ type: tNumber(serverRequestTypes.PLATFORM_DETAILS), platformDetails: tPlatformDetails, }); export type EntryInconsistencyClientResponse = { type: 5, ...EntryInconsistencyReportShape, }; const entryInconsistencyClientResponseValidator: TInterface = tShape({ ...entryInconsistencyReportValidatorShape, type: tNumber(serverRequestTypes.ENTRY_INCONSISTENCY), }); type FailUnmentioned = Partial<{ +threadInfos: boolean, +entryInfos: boolean, +userInfos: boolean, }>; type StateChanges = Partial<{ +rawThreadInfos: LegacyRawThreadInfo[] | RawThreadInfo[], +rawEntryInfos: RawEntryInfo[], +currentUserInfo: CurrentUserInfo, +userInfos: AccountUserInfo[], +deleteThreadIDs: string[], +deleteEntryIDs: string[], +deleteUserInfoIDs: string[], }>; export type ServerCheckStateServerRequest = { +type: 6, +hashesToCheck: { +[key: string]: number }, +failUnmentioned?: FailUnmentioned, +stateChanges?: StateChanges, }; const serverCheckStateServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.CHECK_STATE), hashesToCheck: t.dict(t.String, t.Number), failUnmentioned: t.maybe( tShape({ threadInfos: t.maybe(t.Boolean), entryInfos: t.maybe(t.Boolean), userInfos: t.maybe(t.Boolean), }), ), stateChanges: t.maybe( tShape({ rawThreadInfos: t.maybe(t.list(mixedRawThreadInfoValidator)), rawEntryInfos: t.maybe(t.list(rawEntryInfoValidator)), currentUserInfo: t.maybe(currentUserInfoValidator), userInfos: t.maybe(t.list(accountUserInfoValidator)), deleteThreadIDs: t.maybe(t.list(tID)), deleteEntryIDs: t.maybe(t.list(tID)), deleteUserInfoIDs: t.maybe(t.list(tUserID)), }), ), }); type CheckStateClientResponse = { +type: 6, +hashResults: { +[key: string]: boolean }, }; const checkStateClientResponseValidator: TInterface = tShape({ type: tNumber(serverRequestTypes.CHECK_STATE), hashResults: t.dict(t.String, t.Boolean), }); type InitialActivityUpdatesClientResponse = { +type: 7, +activityUpdates: $ReadOnlyArray, }; const initialActivityUpdatesClientResponseValidator: TInterface = tShape({ type: tNumber(serverRequestTypes.INITIAL_ACTIVITY_UPDATES), activityUpdates: t.list(activityUpdateValidator), }); type MoreOneTimeKeysClientResponse = { +type: 8, +keys: $ReadOnlyArray, }; type SignedIdentityKeysBlobServerRequest = { +type: 9, }; const signedIdentityKeysBlobServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB), }); type SignedIdentityKeysBlobClientResponse = { +type: 9, +signedIdentityKeysBlob: SignedIdentityKeysBlob, }; const signedIdentityKeysBlobClientResponseValidator: TInterface = tShape({ type: tNumber(serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB), signedIdentityKeysBlob: signedIdentityKeysBlobValidator, }); type InitialNotificationsEncryptedMessageServerRequest = { +type: 10, }; const initialNotificationsEncryptedMessageServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE), }); type InitialNotificationsEncryptedMessageClientResponse = { +type: 10, +initialNotificationsEncryptedMessage: string, }; const initialNotificationsEncryptedMessageClientResponseValidator: TInterface = tShape({ type: tNumber(serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE), initialNotificationsEncryptedMessage: t.String, }); export type ServerServerRequest = | PlatformServerRequest | PlatformDetailsServerRequest | ServerCheckStateServerRequest | SignedIdentityKeysBlobServerRequest | InitialNotificationsEncryptedMessageServerRequest; export const serverServerRequestValidator: TUnion = t.union([ platformServerRequestValidator, platformDetailsServerRequestValidator, serverCheckStateServerRequestValidator, signedIdentityKeysBlobServerRequestValidator, initialNotificationsEncryptedMessageServerRequestValidator, ]); export type ClientResponse = | PlatformClientResponse | ThreadInconsistencyClientResponse | PlatformDetailsClientResponse | EntryInconsistencyClientResponse | CheckStateClientResponse | InitialActivityUpdatesClientResponse | MoreOneTimeKeysClientResponse | SignedIdentityKeysBlobClientResponse | InitialNotificationsEncryptedMessageClientResponse; export type ClientCheckStateServerRequest = { +type: 6, +hashesToCheck: { +[key: string]: number }, +failUnmentioned?: Partial<{ +threadInfos: boolean, +entryInfos: boolean, +userInfos: boolean, }>, +stateChanges?: Partial<{ +rawThreadInfos: RawThreadInfo[], +rawEntryInfos: RawEntryInfo[], +currentUserInfo: CurrentUserInfo, +userInfos: AccountUserInfo[], +deleteThreadIDs: string[], +deleteEntryIDs: string[], +deleteUserInfoIDs: string[], }>, }; export type ClientServerRequest = | PlatformServerRequest | PlatformDetailsServerRequest | ClientCheckStateServerRequest | SignedIdentityKeysBlobServerRequest | InitialNotificationsEncryptedMessageServerRequest; // This is just the client variant of ClientResponse. The server needs to handle // multiple client versions so the type supports old versions of certain client // responses, but the client variant only need to support the latest version. type ClientThreadInconsistencyClientResponse = { ...ClientThreadInconsistencyReportShape, +type: 2, }; type ClientEntryInconsistencyClientResponse = { +type: 5, ...ClientEntryInconsistencyReportShape, }; export type ClientClientResponse = | PlatformClientResponse | ClientThreadInconsistencyClientResponse | PlatformDetailsClientResponse | ClientEntryInconsistencyClientResponse | CheckStateClientResponse | InitialActivityUpdatesClientResponse | MoreOneTimeKeysClientResponse | SignedIdentityKeysBlobClientResponse | InitialNotificationsEncryptedMessageClientResponse; export type ClientInconsistencyResponse = | ClientThreadInconsistencyClientResponse | ClientEntryInconsistencyClientResponse; export const processServerRequestsActionType = 'PROCESS_SERVER_REQUESTS'; export type ProcessServerRequestsPayload = { +serverRequests: $ReadOnlyArray, +calendarQuery: CalendarQuery, +keyserverID: string, }; export type ProcessServerRequestAction = { - +messageSourceMetadata?: MessageSourceMetadata, + +dispatchMetadata?: DispatchMetadata, +type: 'PROCESS_SERVER_REQUESTS', +payload: ProcessServerRequestsPayload, }; export const clientResponseInputValidator: TUnion = t.union([ platformClientResponseValidator, threadInconsistencyClientResponseValidator, entryInconsistencyClientResponseValidator, platformDetailsClientResponseValidator, checkStateClientResponseValidator, initialActivityUpdatesClientResponseValidator, signedIdentityKeysBlobClientResponseValidator, initialNotificationsEncryptedMessageClientResponseValidator, ]); diff --git a/native/redux/action-types.js b/native/redux/action-types.js index 2a7f6ee21..6ad5e05f2 100644 --- a/native/redux/action-types.js +++ b/native/redux/action-types.js @@ -1,63 +1,62 @@ // @flow import type { Orientations } from 'react-native-orientation-locker'; import { saveMessagesActionType } from 'lib/actions/message-actions.js'; -import type { MessageSourceMetadata } from 'lib/types/db-ops-types.js'; -import type { BaseAction } from 'lib/types/redux-types.js'; +import type { BaseAction, DispatchMetadata } from 'lib/types/redux-types.js'; import type { DimensionsInfo } from './dimensions-updater.react.js'; import type { AppState } from './state-types.js'; import type { DeviceCameraInfo } from '../types/camera.js'; import type { ConnectivityInfo } from '../types/connectivity.js'; import type { LocalSettings } from '../types/local-settings-types.js'; export const updateDimensionsActiveType = 'UPDATE_DIMENSIONS'; export const updateConnectivityActiveType = 'UPDATE_CONNECTIVITY'; export const updateDeviceCameraInfoActionType = 'UPDATE_DEVICE_CAMERA_INFO'; export const updateDeviceOrientationActionType = 'UPDATE_DEVICE_ORIENTATION'; export const setStoreLoadedActionType = 'SET_STORE_LOADED'; export const setReduxStateActionType = 'SET_REDUX_STATE'; export const setLocalSettingsActionType = 'SET_LOCAL_SETTINGS'; export const setAccessTokenActionType = 'SET_ACCESS_TOKEN'; export const backgroundActionTypes: Set = new Set([ saveMessagesActionType, ]); export type Action = $ReadOnly< | BaseAction | { - +messageSourceMetadata?: MessageSourceMetadata, + +dispatchMetadata?: DispatchMetadata, ... | { +type: 'SET_REDUX_STATE', +payload: { +state: AppState, +hideFromMonitor: boolean }, } | { +type: 'UPDATE_DIMENSIONS', +payload: Partial, } | { +type: 'UPDATE_CONNECTIVITY', +payload: ConnectivityInfo, } | { +type: 'UPDATE_DEVICE_CAMERA_INFO', +payload: Partial, } | { +type: 'UPDATE_DEVICE_ORIENTATION', +payload: Orientations, } | { +type: 'UPDATE_THREAD_LAST_NAVIGATED', +payload: { +threadID: string, +time: number }, } | { +type: 'SET_STORE_LOADED', } | { +type: 'SET_LOCAL_SETTINGS', +payload: LocalSettings } | { +type: 'SET_ACCESS_TOKEN', +payload: ?string }, }, >; diff --git a/native/redux/redux-setup.js b/native/redux/redux-setup.js index 23b99e5fd..b8012c342 100644 --- a/native/redux/redux-setup.js +++ b/native/redux/redux-setup.js @@ -1,451 +1,451 @@ // @flow import { AppState as NativeAppState, Alert } from 'react-native'; import { createStore, applyMiddleware, type Store, compose } from 'redux'; import { persistStore, persistReducer } from 'redux-persist'; import thunk from 'redux-thunk'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { legacySiweAuthActionTypes } from 'lib/actions/siwe-actions.js'; import { logOutActionTypes, deleteAccountActionTypes, legacyLogInActionTypes, keyserverAuthActionTypes, deleteKeyserverAccountActionTypes, } from 'lib/actions/user-actions.js'; import { setNewSessionActionType } from 'lib/keyserver-conn/keyserver-conn-types.js'; import type { ThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { queueDBOps } from 'lib/reducers/db-ops-reducer.js'; import { reduceLoadingStatuses } from 'lib/reducers/loading-reducer.js'; import baseReducer from 'lib/reducers/master-reducer.js'; import { reduceCurrentUserInfo } from 'lib/reducers/user-reducer.js'; import { shouldClearData } from 'lib/shared/data-utils.js'; import { invalidSessionDowngrade, invalidSessionRecovery, identityInvalidSessionDowngrade, } from 'lib/shared/session-utils.js'; import { isStaff } from 'lib/shared/staff-utils.js'; import type { Dispatch, BaseAction } from 'lib/types/redux-types.js'; import { rehydrateActionType } from 'lib/types/redux-types.js'; import type { SetSessionPayload } from 'lib/types/session-types.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import { resetUserSpecificState } from 'lib/utils/reducers-utils.js'; import { updateDimensionsActiveType, updateConnectivityActiveType, updateDeviceCameraInfoActionType, updateDeviceOrientationActionType, backgroundActionTypes, setReduxStateActionType, setStoreLoadedActionType, type Action, setLocalSettingsActionType, } from './action-types.js'; import { setAccessTokenActionType } from './action-types.js'; import { defaultState } from './default-state.js'; import { remoteReduxDevServerConfig } from './dev-tools.js'; import { persistConfig, setPersistor } from './persist.js'; import { onStateDifference } from './redux-debug-utils.js'; import type { AppState } from './state-types.js'; import { nonUserSpecificFieldsNative } from './state-types.js'; import { getGlobalNavContext } from '../navigation/icky-global.js'; import { activeMessageListSelector } from '../navigation/nav-selectors.js'; import reactotron from '../reactotron.js'; import { appOutOfDateAlertDetails } from '../utils/alert-messages.js'; import { isStaffRelease } from '../utils/staff-utils.js'; import { getDevServerHostname } from '../utils/url-utils.js'; function reducer(state: AppState = defaultState, inputAction: Action) { let action = inputAction; if (action.type === setReduxStateActionType) { return action.payload.state; } // We want to alert staff/developers if there's a difference between the keys // we expect to see REHYDRATED and the keys that are actually REHYDRATED. // Context: https://linear.app/comm/issue/ENG-2127/ if ( action.type === rehydrateActionType && (__DEV__ || isStaffRelease || (state.currentUserInfo && state.currentUserInfo.id && isStaff(state.currentUserInfo.id))) ) { // 1. Construct set of keys expected to be REHYDRATED const defaultKeys: $ReadOnlyArray = Object.keys(defaultState); const expectedKeys = defaultKeys.filter( each => !persistConfig.blacklist.includes(each), ); const expectedKeysSet = new Set(expectedKeys); // 2. Construct set of keys actually REHYDRATED const rehydratedKeys: $ReadOnlyArray = Object.keys( action.payload ?? {}, ); const rehydratedKeysSet = new Set(rehydratedKeys); // 3. Determine the difference between the two sets const expectedKeysNotRehydrated = expectedKeys.filter( each => !rehydratedKeysSet.has(each), ); const rehydratedKeysNotExpected = rehydratedKeys.filter( each => !expectedKeysSet.has(each), ); // 4. Display alerts with the differences between the two sets if (expectedKeysNotRehydrated.length > 0) { Alert.alert( `EXPECTED KEYS NOT REHYDRATED: ${JSON.stringify( expectedKeysNotRehydrated, )}`, ); } if (rehydratedKeysNotExpected.length > 0) { Alert.alert( `REHYDRATED KEYS NOT EXPECTED: ${JSON.stringify( rehydratedKeysNotExpected, )}`, ); } } if ( (action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success) && identityInvalidSessionDowngrade( state, action.payload.currentUserInfo, action.payload.preRequestUserState, ) ) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } if ( action.type === setNewSessionActionType && invalidSessionDowngrade( state, action.payload.sessionChange.currentUserInfo, action.payload.preRequestUserState, action.payload.keyserverID, ) ) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const { currentUserInfo, preRequestUserState } = action.payload; const newKeyserverIDs = []; for (const keyserverID of action.payload.keyserverIDs) { if ( invalidSessionDowngrade( state, currentUserInfo, preRequestUserState, keyserverID, ) ) { continue; } newKeyserverIDs.push(keyserverID); } if (newKeyserverIDs.length === 0) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } action = { ...action, payload: { ...action.payload, keyserverIDs: newKeyserverIDs, }, }; } if ( (action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && invalidSessionRecovery( state, action.payload.preRequestUserState?.currentUserInfo, action.payload.authActionSource, )) || ((action.type === legacyLogInActionTypes.success || action.type === legacySiweAuthActionTypes.success) && invalidSessionRecovery( state, action.payload.preRequestUserInfo, action.payload.authActionSource, )) || (action.type === keyserverAuthActionTypes.success && invalidSessionRecovery( state, action.payload.preRequestUserInfo, action.payload.authActionSource, )) ) { return state; } if (action.type === updateDimensionsActiveType) { return { ...state, dimensions: { ...state.dimensions, ...action.payload, }, }; } else if (action.type === updateConnectivityActiveType) { return { ...state, connectivity: action.payload, }; } else if (action.type === updateDeviceCameraInfoActionType) { return { ...state, deviceCameraInfo: { ...state.deviceCameraInfo, ...action.payload, }, }; } else if (action.type === updateDeviceOrientationActionType) { return { ...state, deviceOrientation: action.payload, }; } else if (action.type === setLocalSettingsActionType) { return { ...state, localSettings: { ...state.localSettings, ...action.payload }, }; } else if (action.type === setAccessTokenActionType) { return { ...state, commServicesAccessToken: action.payload }; } if (action.type === setNewSessionActionType) { sessionInvalidationAlert(action.payload); } if (action.type === setStoreLoadedActionType) { return { ...state, storeLoaded: true, }; } if (action.type === setClientDBStoreActionType) { state = { ...state, storeLoaded: true, }; const currentLoggedInUserID = state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id; const actionCurrentLoggedInUserID = action.payload.currentUserID; if ( !currentLoggedInUserID || !actionCurrentLoggedInUserID || actionCurrentLoggedInUserID !== currentLoggedInUserID ) { // If user is logged out now, was logged out at the time action was // dispatched or their ID changed between action dispatch and a // call to reducer we ignore the SQLite data since it is not valid return state; } } // We're calling this reducer twice: here and in the baseReducer. This call // is only used to determine the new current user ID. We don't want to use // the remaining part of the current user info, because it is possible that // the reducer returned a modified ID without cleared remaining parts of // the current user info - this would be a bug, but we want to be extra // careful when clearing the state. // When newCurrentUserInfo has the same ID as state.currentUserInfo the state // won't be cleared and the current user info determined in baseReducer will // be equal to the newCurrentUserInfo. // When newCurrentUserInfo has different ID than state.currentUserInfo, we // reset the state and pass it to the baseReducer. Then, in baseReducer, // reduceCurrentUserInfo acts on the cleared state and may return a different // result than newCurrentUserInfo. // Overall, the solution is a little wasteful, but makes us sure that we never // keep the info of the user when the current user ID changes. const newCurrentUserInfo = reduceCurrentUserInfo( state.currentUserInfo, action, ); if (shouldClearData(state.currentUserInfo?.id, newCurrentUserInfo?.id)) { state = resetUserSpecificState( state, defaultState, nonUserSpecificFieldsNative, ); } const baseReducerResult = baseReducer( state, (action: BaseAction), onStateDifference, ); state = baseReducerResult.state; const { storeOperations } = baseReducerResult; const fixUnreadActiveThreadResult = fixUnreadActiveThread(state, action); state = fixUnreadActiveThreadResult.state; const threadStoreOperationsWithUnreadFix = [ ...(storeOperations.threadStoreOperations ?? []), ...fixUnreadActiveThreadResult.threadStoreOperations, ]; const ops = { ...storeOperations, threadStoreOperations: threadStoreOperationsWithUnreadFix, }; state = { ...state, - dbOpsStore: queueDBOps(state.dbOpsStore, action.messageSourceMetadata, ops), + dbOpsStore: queueDBOps(state.dbOpsStore, action.dispatchMetadata, ops), }; return state; } function sessionInvalidationAlert(payload: SetSessionPayload) { if ( !payload.sessionChange.cookieInvalidated || !payload.preRequestUserState || !payload.preRequestUserState.currentUserInfo || payload.preRequestUserState.currentUserInfo.anonymous ) { return; } if (payload.error === 'client_version_unsupported') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK' }], { cancelable: true, }, ); } else { Alert.alert( 'Session invalidated', 'We’re sorry, but your session was invalidated by the server. ' + 'Please log in again.', [{ text: 'OK' }], { cancelable: true }, ); } } // Makes sure a currently focused thread is never unread. Note that we consider // a backgrounded NativeAppState to actually be active if it last changed to // inactive more than 10 seconds ago. This is because there is a delay when // NativeAppState is updating in response to a foreground, and actions don't get // processed more than 10 seconds after a backgrounding anyways. However we // don't consider this for action types that can be expected to happen while the // app is backgrounded. type FixUnreadActiveThreadResult = { +state: AppState, +threadStoreOperations: $ReadOnlyArray, }; function fixUnreadActiveThread( state: AppState, action: *, ): FixUnreadActiveThreadResult { const navContext = getGlobalNavContext(); const activeThread = activeMessageListSelector(navContext); if ( !activeThread || !state.threadStore.threadInfos[activeThread]?.currentUser.unread || (NativeAppState.currentState !== 'active' && (appLastBecameInactive + 10000 >= Date.now() || backgroundActionTypes.has(action.type))) ) { return { state, threadStoreOperations: [] }; } const activeThreadInfo = state.threadStore.threadInfos[activeThread]; const updatedActiveThreadInfo = { ...activeThreadInfo, currentUser: { ...activeThreadInfo.currentUser, unread: false, }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: activeThread, threadInfo: updatedActiveThreadInfo, }, }, ]; const updatedThreadStore = threadStoreOpsHandlers.processStoreOperations( state.threadStore, threadStoreOperations, ); return { state: { ...state, threadStore: updatedThreadStore }, threadStoreOperations, }; } let appLastBecameInactive = 0; function appBecameInactive() { appLastBecameInactive = Date.now(); } const middleware = applyMiddleware(thunk, reduxLoggerMiddleware); let composeFunc = compose; if (__DEV__ && global.HermesInternal) { const { composeWithDevTools } = require('remote-redux-devtools/src/index.js'); composeFunc = composeWithDevTools({ maxAge: 200, name: 'Redux', hostname: getDevServerHostname(), ...remoteReduxDevServerConfig, }); } else if (global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { composeFunc = global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'Redux', }); } let enhancers; if (reactotron) { enhancers = composeFunc(middleware, reactotron.createEnhancer()); } else { enhancers = composeFunc(middleware); } const store: Store = createStore( persistReducer(persistConfig, reducer), defaultState, enhancers, ); const persistor = persistStore(store); setPersistor(persistor); const unsafeDispatch: any = store.dispatch; const dispatch: Dispatch = unsafeDispatch; export { store, dispatch, appBecameInactive }; diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js index 9e46427ad..39748fcb8 100644 --- a/web/redux/redux-setup.js +++ b/web/redux/redux-setup.js @@ -1,555 +1,552 @@ // @flow import invariant from 'invariant'; import type { PersistState } from 'redux-persist/es/types.js'; import { logOutActionTypes, deleteKeyserverAccountActionTypes, deleteAccountActionTypes, keyserverAuthActionTypes, } from 'lib/actions/user-actions.js'; import { setNewSessionActionType } from 'lib/keyserver-conn/keyserver-conn-types.js'; import { type ReplaceKeyserverOperation, keyserverStoreOpsHandlers, } from 'lib/ops/keyserver-store-ops.js'; import { type ReplaceThreadActivityEntryOperation, threadActivityStoreOpsHandlers, } from 'lib/ops/thread-activity-store-ops.js'; import { type ThreadStoreOperation, threadStoreOpsHandlers, } from 'lib/ops/thread-store-ops.js'; import { queueDBOps } from 'lib/reducers/db-ops-reducer.js'; import { reduceLoadingStatuses } from 'lib/reducers/loading-reducer.js'; import baseReducer from 'lib/reducers/master-reducer.js'; import { reduceCurrentUserInfo } from 'lib/reducers/user-reducer.js'; import { mostRecentlyReadThreadSelector } from 'lib/selectors/thread-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { shouldClearData } from 'lib/shared/data-utils.js'; import { invalidSessionDowngrade, identityInvalidSessionDowngrade, invalidSessionRecovery, } from 'lib/shared/session-utils.js'; import type { AlertStore } from 'lib/types/alert-types.js'; import type { AuxUserStore } from 'lib/types/aux-user-types.js'; import type { CommunityStore } from 'lib/types/community-types.js'; -import type { - MessageSourceMetadata, - DBOpsStore, -} from 'lib/types/db-ops-types.js'; +import type { DBOpsStore } from 'lib/types/db-ops-types.js'; import type { QueuedDMOperations } from 'lib/types/dm-ops.js'; import type { DraftStore } from 'lib/types/draft-types.js'; import type { EnabledApps } from 'lib/types/enabled-apps.js'; import type { EntryStore } from 'lib/types/entry-types.js'; import { type CalendarFilter } from 'lib/types/filter-types.js'; import type { IntegrityStore } from 'lib/types/integrity-types.js'; import type { KeyserverStore } from 'lib/types/keyserver-types.js'; import type { LifecycleState } from 'lib/types/lifecycle-state-types.js'; import type { InviteLinksStore } from 'lib/types/link-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { MessageStore } from 'lib/types/message-types.js'; import type { WebNavInfo } from 'lib/types/nav-types.js'; import type { UserPolicies } from 'lib/types/policy-types.js'; -import type { BaseAction } from 'lib/types/redux-types.js'; +import type { BaseAction, DispatchMetadata } from 'lib/types/redux-types.js'; import type { ReportStore } from 'lib/types/report-types.js'; import type { StoreOperations } from 'lib/types/store-ops-types.js'; import type { SyncedMetadataStore } from 'lib/types/synced-metadata-types.js'; import type { GlobalThemeInfo } from 'lib/types/theme-types.js'; import type { ThreadActivityStore } from 'lib/types/thread-activity-types'; import type { ThreadStore } from 'lib/types/thread-types.js'; import type { TunnelbrokerDeviceToken } from 'lib/types/tunnelbroker-device-token-types.js'; import type { CurrentUserInfo, UserStore } from 'lib/types/user-types.js'; import { resetUserSpecificState } from 'lib/utils/reducers-utils.js'; import { updateWindowActiveActionType, updateNavInfoActionType, updateWindowDimensionsActionType, setInitialReduxState, } from './action-types.js'; import { reduceCommunityPickerStore } from './community-picker-reducer.js'; import { defaultWebState } from './default-state.js'; import reduceNavInfo from './nav-reducer.js'; import { onStateDifference } from './redux-debug-utils.js'; import { reduceServicesAccessToken } from './services-access-token-reducer.js'; import { getVisibility } from './visibility.js'; import { activeThreadSelector } from '../selectors/nav-selectors.js'; import type { InitialReduxStateActionPayload } from '../types/redux-types.js'; export type WindowDimensions = { width: number, height: number }; export type CommunityPickerStore = { +chat: ?string, +calendar: ?string, }; const nonUserSpecificFieldsWeb = [ 'loadingStatuses', 'windowDimensions', 'lifecycleState', 'windowActive', 'pushApiPublicKey', 'keyserverStore', 'initialStateLoaded', '_persist', 'customServer', ]; export type AppState = { +navInfo: WebNavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +communityPickerStore: CommunityPickerStore, +windowDimensions: WindowDimensions, +alertStore: AlertStore, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +dataLoaded: boolean, +windowActive: boolean, +userPolicies: UserPolicies, +pushApiPublicKey: ?string, +_persist: ?PersistState, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +initialStateLoaded: boolean, +integrityStore: IntegrityStore, +globalThemeInfo: GlobalThemeInfo, +customServer: ?string, +communityStore: CommunityStore, +dbOpsStore: DBOpsStore, +syncedMetadataStore: SyncedMetadataStore, +auxUserStore: AuxUserStore, +tunnelbrokerDeviceToken: TunnelbrokerDeviceToken, +queuedDMOperations: QueuedDMOperations, }; export type Action = $ReadOnly< | BaseAction | { - +messageSourceMetadata?: MessageSourceMetadata, + +dispatchMetadata?: DispatchMetadata, ... | { +type: 'UPDATE_NAV_INFO', +payload: Partial } | { +type: 'UPDATE_WINDOW_DIMENSIONS', +payload: WindowDimensions, } | { +type: 'UPDATE_WINDOW_ACTIVE', +payload: boolean, } | { +type: 'SET_INITIAL_REDUX_STATE', +payload: InitialReduxStateActionPayload, }, }, >; function reducer(oldState: AppState | void, action: Action): AppState { invariant(oldState, 'should be set'); let state = oldState; let storeOperations: StoreOperations = {}; if ( (action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && invalidSessionRecovery( state, action.payload.preRequestUserState?.currentUserInfo, action.payload.authActionSource, )) || (action.type === keyserverAuthActionTypes.success && invalidSessionRecovery( state, action.payload.preRequestUserInfo, action.payload.authActionSource, )) ) { return state; } if ( (action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success) && identityInvalidSessionDowngrade( oldState, action.payload.currentUserInfo, action.payload.preRequestUserState, ) ) { return { ...oldState, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } if (action.type === setInitialReduxState) { const { userInfos, keyserverInfos, actualizedCalendarQuery, ...rest } = action.payload; const replaceOperations: ReplaceKeyserverOperation[] = []; for (const keyserverID in keyserverInfos) { replaceOperations.push({ type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverStore.keyserverInfos[keyserverID], ...keyserverInfos[keyserverID], actualizedCalendarQuery, }, }, }); } let newState = { ...state, ...rest, keyserverStore: keyserverStoreOpsHandlers.processStoreOperations( state.keyserverStore, replaceOperations, ), initialStateLoaded: true, }; if (userInfos) { newState = { ...newState, userStore: { userInfos } }; } return validateStateAndQueueOpsProcessing(action, oldState, newState, { ...storeOperations, keyserverStoreOperations: [ ...(storeOperations.keyserverStoreOperations ?? []), ...replaceOperations, ], }); } else if (action.type === updateWindowDimensionsActionType) { return validateStateAndQueueOpsProcessing( action, oldState, { ...state, windowDimensions: action.payload, }, storeOperations, ); } else if (action.type === updateWindowActiveActionType) { return validateStateAndQueueOpsProcessing( action, oldState, { ...state, windowActive: action.payload, }, storeOperations, ); } else if (action.type === setNewSessionActionType) { const { keyserverID, sessionChange } = action.payload; if (!state.keyserverStore.keyserverInfos[keyserverID]) { if (sessionChange.cookie?.startsWith('user=')) { console.log( 'received sessionChange with user cookie, ' + `but keyserver ${keyserverID} is not in KeyserverStore!`, ); } return state; } if ( invalidSessionDowngrade( oldState, sessionChange.currentUserInfo, action.payload.preRequestUserState, keyserverID, ) ) { return { ...oldState, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } const replaceOperation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverStore.keyserverInfos[keyserverID], sessionID: sessionChange.sessionID, }, }, }; state = { ...state, keyserverStore: keyserverStoreOpsHandlers.processStoreOperations( state.keyserverStore, [replaceOperation], ), }; storeOperations = { ...storeOperations, keyserverStoreOperations: [ ...(storeOperations.keyserverStoreOperations ?? []), replaceOperation, ], }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const { currentUserInfo, preRequestUserState } = action.payload; const newKeyserverIDs = []; for (const keyserverID of action.payload.keyserverIDs) { if ( invalidSessionDowngrade( state, currentUserInfo, preRequestUserState, keyserverID, ) ) { continue; } newKeyserverIDs.push(keyserverID); } if (newKeyserverIDs.length === 0) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } action = { ...action, payload: { ...action.payload, keyserverIDs: newKeyserverIDs, }, }; } if (action.type !== updateNavInfoActionType) { // We're calling this reducer twice: here and in the baseReducer. This call // is only used to determine the new current user ID. We don't want to use // the remaining part of the current user info, because it is possible that // the reducer returned a modified ID without cleared remaining parts of // the current user info - this would be a bug, but we want to be extra // careful when clearing the state. // When newCurrentUserInfo has the same ID as state.currentUserInfo // the state won't be cleared and the current user info determined in // baseReducer will be equal to the newCurrentUserInfo. // When newCurrentUserInfo has different ID than state.currentUserInfo, we // reset the state and pass it to the baseReducer. Then, in baseReducer, // reduceCurrentUserInfo acts on the cleared state and may return // a different result than newCurrentUserInfo. // Overall, the solution is a little wasteful, but makes us sure that we // never keep the info of the user when the current user ID changes. const newCurrentUserInfo = reduceCurrentUserInfo( state.currentUserInfo, action, ); if (shouldClearData(state.currentUserInfo?.id, newCurrentUserInfo?.id)) { state = resetUserSpecificState( state, defaultWebState, nonUserSpecificFieldsWeb, ); } const baseReducerResult = baseReducer(state, action, onStateDifference); state = baseReducerResult.state; storeOperations = { ...baseReducerResult.storeOperations, keyserverStoreOperations: [ ...(storeOperations.keyserverStoreOperations ?? []), ...(baseReducerResult.storeOperations.keyserverStoreOperations ?? []), ], }; } const communityPickerStore = reduceCommunityPickerStore( state.communityPickerStore, action, ); state = { ...state, navInfo: reduceNavInfo( state.navInfo, action, state.threadStore.threadInfos, ), communityPickerStore, commServicesAccessToken: reduceServicesAccessToken( state.commServicesAccessToken, action, ), }; return validateStateAndQueueOpsProcessing( action, oldState, state, storeOperations, ); } function validateStateAndQueueOpsProcessing( action: Action, oldState: AppState, state: AppState, storeOperations: StoreOperations, ): AppState { const updateActiveThreadOps: ThreadStoreOperation[] = []; if ( (state.navInfo.activeChatThreadID && !state.navInfo.pendingThread && !state.threadStore.threadInfos[state.navInfo.activeChatThreadID]) || (!state.navInfo.activeChatThreadID && isLoggedIn(state)) ) { // Makes sure the active thread always exists state = { ...state, navInfo: { ...state.navInfo, activeChatThreadID: mostRecentlyReadThreadSelector(state), }, }; } const activeThread = activeThreadSelector(state); if ( activeThread && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread && getVisibility().hidden() ) { console.warn( `thread ${activeThread} is active and unread, ` + 'but visibilityjs reports the window is not visible', ); } if ( activeThread && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread && typeof document !== 'undefined' && document && 'hasFocus' in document && !document.hasFocus() ) { console.warn( `thread ${activeThread} is active and unread, ` + 'but document.hasFocus() is false', ); } if ( activeThread && !getVisibility().hidden() && typeof document !== 'undefined' && document && 'hasFocus' in document && document.hasFocus() && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread ) { // Makes sure a currently focused thread is never unread const activeThreadInfo = state.threadStore.threadInfos[activeThread]; updateActiveThreadOps.push({ type: 'replace', payload: { id: activeThread, threadInfo: { ...activeThreadInfo, currentUser: { ...activeThreadInfo.currentUser, unread: false, }, }, }, }); } const oldActiveThread = activeThreadSelector(oldState); if ( activeThread && oldActiveThread !== activeThread && state.messageStore.threads[activeThread] ) { const now = Date.now(); const replaceOperation: ReplaceThreadActivityEntryOperation = { type: 'replace_thread_activity_entry', payload: { id: activeThread, threadActivityStoreEntry: { ...state[activeThread], lastNavigatedTo: now, }, }, }; const threadActivityStore = threadActivityStoreOpsHandlers.processStoreOperations( state.threadActivityStore, [replaceOperation], ); state = { ...state, threadActivityStore, }; storeOperations = { ...storeOperations, threadActivityStoreOperations: [ ...(storeOperations.threadActivityStoreOperations ?? []), replaceOperation, ], }; } if (updateActiveThreadOps.length > 0) { state = { ...state, threadStore: threadStoreOpsHandlers.processStoreOperations( state.threadStore, updateActiveThreadOps, ), }; storeOperations = { ...storeOperations, threadStoreOperations: [ ...(storeOperations.threadStoreOperations ?? []), ...updateActiveThreadOps, ], }; } // The operations were already dispatched from the main tab // For now the `dispatchSource` field is not included in any of the // redux actions and this causes flow to throw an error. // As soon as one of the actions is updated, this fix (and the corresponding // one in tab-synchronization.js) can be removed. // $FlowFixMe if (action.dispatchSource === 'tab-sync') { return state; } return { ...state, dbOpsStore: queueDBOps( state.dbOpsStore, - action.messageSourceMetadata, + action.dispatchMetadata, storeOperations, ), }; } export { nonUserSpecificFieldsWeb, reducer };