diff --git a/lib/shared/dm-ops/dm-op-utils.js b/lib/shared/dm-ops/dm-op-utils.js index c6a08118a..9c5405dde 100644 --- a/lib/shared/dm-ops/dm-op-utils.js +++ b/lib/shared/dm-ops/dm-op-utils.js @@ -1,266 +1,359 @@ // @flow import invariant from 'invariant'; +import _groupBy from 'lodash/fp/groupBy.js'; import * as React from 'react'; import uuid from 'uuid'; +import { type ProcessDMOperationUtilities } from './dm-op-spec.js'; import { dmOpSpecs } from './dm-op-specs.js'; import { useProcessAndSendDMOperation } from './process-dm-ops.js'; +import { mergeUpdatesWithMessageInfos } from '../../reducers/message-reducer.js'; import type { CreateThickRawThreadInfoInput, DMAddMembersOperation, DMAddViewerToThreadMembersOperation, DMOperation, ComposableDMOperation, } from '../../types/dm-ops.js'; +import type { RawMessageInfo } from '../../types/message-types.js'; import type { + RawThreadInfo, ThickRawThreadInfo, ThreadInfo, } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { InboundActionMetadata } from '../../types/redux-types.js'; import { outboundP2PMessageStatuses, type OutboundP2PMessage, } from '../../types/sqlite-types.js'; import { assertThickThreadType, thickThreadTypes, } from '../../types/thread-types-enum.js'; -import type { RawThreadInfos } from '../../types/thread-types.js'; +import type { LegacyRawThreadInfo } from '../../types/thread-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 { updateTypes } from '../../types/update-types-enum.js'; +import type { ClientUpdateInfo } from '../../types/update-types.js'; import { getContentSigningKey } from '../../utils/crypto-utils.js'; import { useSelector } from '../../utils/redux-utils.js'; +import { messageSpecs } from '../messages/message-specs.js'; +import { updateSpecs } from '../updates/update-specs.js'; function generateMessagesToPeers( message: DMOperation, peers: $ReadOnlyArray<{ +userID: string, +deviceID: string, }>, ): $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: dmOpSpecs[message.type].supportsAutoRetry, }; outboundP2PMessages.push(messageToPeer); } return outboundP2PMessages; } export const dmOperationSpecificationTypes = Object.freeze({ OUTBOUND: 'OutboundDMOperationSpecification', INBOUND: 'InboundDMOperationSpecification', }); type OutboundDMOperationSpecificationRecipients = | { +type: 'all_peer_devices' | 'self_devices' } | { +type: 'some_users', +userIDs: $ReadOnlyArray } | { +type: 'all_thread_members', +threadID: string } | { +type: 'some_devices', +deviceIDs: $ReadOnlyArray }; // The operation generated on the sending client, causes changes to // the state and broadcasting information to peers. export type OutboundDMOperationSpecification = { +type: 'OutboundDMOperationSpecification', +op: DMOperation, +recipients: OutboundDMOperationSpecificationRecipients, +sendOnly?: boolean, }; export type OutboundComposableDMOperationSpecification = { +type: 'OutboundDMOperationSpecification', +op: ComposableDMOperation, +recipients: OutboundDMOperationSpecificationRecipients, // Composable DM Ops are created only to be sent, locally we use // dedicated mechanism for updating the store. +sendOnly: true, +composableMessageID: string, }; // 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: ?InboundActionMetadata, }; export type DMOperationSpecification = | OutboundDMOperationSpecification | InboundDMOperationSpecification; async function createMessagesToPeersFromDMOp( operation: DMOperation, recipients: OutboundDMOperationSpecificationRecipients, allPeerUserIDAndDeviceIDs: $ReadOnlyArray<{ +userID: string, +deviceID: string, }>, - currentUserInfo: ?CurrentUserInfo, - threadInfos: RawThreadInfos, + utilities: ProcessDMOperationUtilities, ): Promise<$ReadOnlyArray> { - if (!currentUserInfo?.id) { + const { viewerID, threadInfos } = utilities; + if (!viewerID) { return []; } let peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs; if (recipients.type === 'self_devices') { peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter( - peer => peer.userID === currentUserInfo.id, + peer => peer.userID === viewerID, ); } else if (recipients.type === 'some_users') { const userIDs = new Set(recipients.userIDs); peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter(peer => userIDs.has(peer.userID), ); } else if (recipients.type === 'all_thread_members') { const members = threadInfos[recipients.threadID]?.members ?? []; const memberIDs = members.map(member => member.id); const userIDs = new Set(memberIDs); peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter(peer => userIDs.has(peer.userID), ); } else if (recipients.type === 'some_devices') { const deviceIDs = new Set(recipients.deviceIDs); peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter(peer => deviceIDs.has(peer.deviceID), ); } const thisDeviceID = await getContentSigningKey(); const targetPeers = peerUserIDAndDeviceIDs.filter( peer => peer.deviceID !== thisDeviceID, ); return generateMessagesToPeers(operation, targetPeers); } function getCreateThickRawThreadInfoInputFromThreadInfo( threadInfo: ThickRawThreadInfo, ): CreateThickRawThreadInfoInput { const roleID = Object.keys(threadInfo.roles).pop(); const thickThreadType = assertThickThreadType(threadInfo.type); return { threadID: threadInfo.id, threadType: thickThreadType, creationTime: threadInfo.creationTime, parentThreadID: threadInfo.parentThreadID, allMemberIDsWithSubscriptions: threadInfo.members.map( ({ id, subscription }) => ({ id, subscription, }), ), roleID, unread: !!threadInfo.currentUser.unread, name: threadInfo.name, avatar: threadInfo.avatar, description: threadInfo.description, color: threadInfo.color, containingThreadID: threadInfo.containingThreadID, sourceMessageID: threadInfo.sourceMessageID, repliesCount: threadInfo.repliesCount, pinnedCount: threadInfo.pinnedCount, timestamps: threadInfo.timestamps, }; } function useAddDMThreadMembers(): ( newMemberIDs: $ReadOnlyArray, threadInfo: ThreadInfo, ) => Promise { const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const processAndSendDMOperation = useProcessAndSendDMOperation(); const threadInfos = useSelector(state => state.threadStore.threadInfos); return React.useCallback( async (newMemberIDs: $ReadOnlyArray, threadInfo: ThreadInfo) => { const rawThreadInfo = threadInfos[threadInfo.id]; invariant(rawThreadInfo.thick, 'thread should be thick'); const existingThreadDetails = getCreateThickRawThreadInfoInputFromThreadInfo(rawThreadInfo); invariant(viewerID, 'viewerID should be set'); const addViewerToThreadMembersOperation: DMAddViewerToThreadMembersOperation = { type: 'add_viewer_to_thread_members', existingThreadDetails, editorID: viewerID, time: Date.now(), messageID: uuid.v4(), addedUserIDs: newMemberIDs, }; const viewerOperationSpecification: OutboundDMOperationSpecification = { type: dmOperationSpecificationTypes.OUTBOUND, op: addViewerToThreadMembersOperation, recipients: { type: 'some_users', userIDs: newMemberIDs, }, sendOnly: true, }; invariant(viewerID, 'viewerID should be set'); const addMembersOperation: DMAddMembersOperation = { type: 'add_members', threadID: threadInfo.id, editorID: viewerID, time: Date.now(), messageID: uuid.v4(), addedUserIDs: newMemberIDs, }; const newMemberIDsSet = new Set(newMemberIDs); const recipientsThreadID = threadInfo.type === thickThreadTypes.THICK_SIDEBAR && threadInfo.parentThreadID ? threadInfo.parentThreadID : threadInfo.id; const existingMembers = threadInfos[recipientsThreadID]?.members ?.map(member => member.id) ?.filter(memberID => !newMemberIDsSet.has(memberID)) ?? []; const addMembersOperationSpecification: OutboundDMOperationSpecification = { type: dmOperationSpecificationTypes.OUTBOUND, op: addMembersOperation, recipients: { type: 'some_users', userIDs: existingMembers, }, }; await Promise.all([ processAndSendDMOperation(viewerOperationSpecification), processAndSendDMOperation(addMembersOperationSpecification), ]); }, [processAndSendDMOperation, threadInfos, viewerID], ); } +function getThreadUpdatesForNewMessages( + rawMessageInfos: $ReadOnlyArray, + updateInfos: $ReadOnlyArray, + utilities: ProcessDMOperationUtilities, +): Array { + const { threadInfos, 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) { + updatedThreadInfosByThreadID[updatedThreadInfo.id] = updatedThreadInfo; + } + } + + const newUpdateInfos: Array = []; + for (const threadID in messagesByThreadID) { + const repliesCountIncreasingMessages = messagesByThreadID[threadID].filter( + message => messageSpecs[message.type].includedInRepliesCount, + ); + + let threadInfo = updatedThreadInfosByThreadID[threadID]; + + if (repliesCountIncreasingMessages.length > 0) { + const repliesCountIncreaseTime = Math.max( + repliesCountIncreasingMessages.map(message => message.time), + ); + const newThreadInfo = { + ...threadInfo, + repliesCount: + threadInfo.repliesCount + repliesCountIncreasingMessages.length, + }; + newUpdateInfos.push({ + type: updateTypes.UPDATE_THREAD, + id: uuid.v4(), + time: repliesCountIncreaseTime, + threadInfo: newThreadInfo, + }); + threadInfo = newThreadInfo; + } + + const messagesFromOtherPeers = messagesByThreadID[threadID].filter( + message => message.creatorID !== viewerID, + ); + if (messagesFromOtherPeers.length === 0) { + continue; + } + // We take the most recent timestamp to make sure that + // change_thread_read_status operation older + // than it won't flip the status to read. + const time = Math.max(messagesFromOtherPeers.map(message => message.time)); + invariant(threadInfo.thick, 'Thread should be thick'); + + // We aren't checking if the unread timestamp is lower than the time. + // We're doing this because we want to flip the thread to unread after + // any new message from a non-viewer. + newUpdateInfos.push({ + type: updateTypes.UPDATE_THREAD_READ_STATUS, + id: uuid.v4(), + time, + threadID: threadInfo.id, + unread: true, + }); + } + + return newUpdateInfos; +} + export { createMessagesToPeersFromDMOp, useAddDMThreadMembers, getCreateThickRawThreadInfoInputFromThreadInfo, + getThreadUpdatesForNewMessages, }; diff --git a/lib/shared/dm-ops/process-dm-ops.js b/lib/shared/dm-ops/process-dm-ops.js index 089d55e40..64cc55eb8 100644 --- a/lib/shared/dm-ops/process-dm-ops.js +++ b/lib/shared/dm-ops/process-dm-ops.js @@ -1,474 +1,394 @@ // @flow import invariant from 'invariant'; -import _groupBy from 'lodash/fp/groupBy.js'; import * as React from 'react'; -import uuid from 'uuid'; import type { ProcessDMOperationUtilities } from './dm-op-spec.js'; import { dmOpSpecs } from './dm-op-specs.js'; import { type OutboundDMOperationSpecification, type DMOperationSpecification, createMessagesToPeersFromDMOp, dmOperationSpecificationTypes, type OutboundComposableDMOperationSpecification, + getThreadUpdatesForNewMessages, } from './dm-op-utils.js'; import { useProcessBlobHolders } from '../../actions/holder-actions.js'; import { processNewUserIDsActionType, useFindUserIdentities, } from '../../actions/user-actions.js'; import { useLoggedInUserInfo } from '../../hooks/account-hooks.js'; import { useGetLatestMessageEdit } from '../../hooks/latest-message-edit.js'; import { useDispatchWithMetadata } from '../../hooks/ops-hooks.js'; -import { mergeUpdatesWithMessageInfos } from '../../reducers/message-reducer.js'; import { getAllPeerUserIDAndDeviceIDs } from '../../selectors/user-selectors.js'; import { usePeerToPeerCommunication, type ProcessOutboundP2PMessagesResult, } from '../../tunnelbroker/peer-to-peer-context.js'; import { processDMOpsActionType, queueDMOpsActionType, dmOperationValidator, } from '../../types/dm-ops.js'; -import type { RawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { NotificationsCreationData } from '../../types/notif-types.js'; import type { DispatchMetadata } from '../../types/redux-types.js'; import type { OutboundP2PMessage } from '../../types/sqlite-types.js'; -import type { LegacyRawThreadInfo } from '../../types/thread-types.js'; -import { updateTypes } from '../../types/update-types-enum.js'; import { extractUserIDsFromPayload } from '../../utils/conversion-utils.js'; import { useSelector, useDispatch } from '../../utils/redux-utils.js'; -import { messageSpecs } from '../messages/message-specs.js'; -import { updateSpecs } from '../updates/update-specs.js'; function useSendDMOperationUtils() { const fetchMessage = useGetLatestMessageEdit(); const threadInfos = useSelector(state => state.threadStore.threadInfos); const entryInfos = useSelector(state => state.entryStore.entryInfos); const findUserIdentities = useFindUserIdentities(); const loggedInUserInfo = useLoggedInUserInfo(); const viewerID = loggedInUserInfo?.id; return React.useMemo( () => ({ viewerID, fetchMessage, threadInfos, entryInfos, findUserIdentities, }), [viewerID, fetchMessage, threadInfos, entryInfos, findUserIdentities], ); } function useProcessDMOperation(): ( dmOperationSpecification: DMOperationSpecification, dmOpID: ?string, ) => Promise { - const threadInfos = useSelector(state => state.threadStore.threadInfos); const baseUtilities = useSendDMOperationUtils(); const dispatchWithMetadata = useDispatchWithMetadata(); const allPeerUserIDAndDeviceIDs = useSelector(getAllPeerUserIDAndDeviceIDs); - const currentUserInfo = useSelector(state => state.currentUserInfo); const processBlobHolders = useProcessBlobHolders(); const dispatch = useDispatch(); return React.useCallback( async ( dmOperationSpecification: DMOperationSpecification, dmOpID: ?string, ) => { const { viewerID, ...restUtilities } = baseUtilities; if (!viewerID) { console.log('ignored DMOperation because logged out'); return; } const utilities: ProcessDMOperationUtilities = { ...restUtilities, viewerID, }; const { op: dmOp } = dmOperationSpecification; let outboundP2PMessages: ?$ReadOnlyArray = null; if ( dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND ) { outboundP2PMessages = await createMessagesToPeersFromDMOp( dmOp, dmOperationSpecification.recipients, allPeerUserIDAndDeviceIDs, - currentUserInfo, - threadInfos, + utilities, ); } let dispatchMetadata: ?DispatchMetadata = null; if ( dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND && dmOpID ) { dispatchMetadata = { dmOpID, }; } else if ( dmOperationSpecification.type === dmOperationSpecificationTypes.INBOUND ) { dispatchMetadata = dmOperationSpecification.metadata; } let composableMessageID: ?string = null; if ( dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND && !dmOpSpecs[dmOp.type].supportsAutoRetry ) { composableMessageID = dmOp.messageID; } if ( dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND && dmOperationSpecification.sendOnly ) { const notificationsCreationData = await dmOpSpecs[ dmOp.type ].notificationsCreationData?.(dmOp, utilities); dispatchWithMetadata( { type: processDMOpsActionType, payload: { rawMessageInfos: [], updateInfos: [], outboundP2PMessages, composableMessageID, notificationsCreationData, }, }, dispatchMetadata, ); return; } const processingCheckResult = await dmOpSpecs[dmOp.type].canBeProcessed( dmOp, utilities, ); if (!processingCheckResult.isProcessingPossible) { if (processingCheckResult.reason.type === 'invalid') { return; } let condition; if (processingCheckResult.reason.type === 'missing_thread') { condition = { type: 'thread', threadID: processingCheckResult.reason.threadID, }; } else if (processingCheckResult.reason.type === 'missing_entry') { condition = { type: 'entry', entryID: processingCheckResult.reason.entryID, }; } else if (processingCheckResult.reason.type === 'missing_message') { condition = { type: 'message', messageID: processingCheckResult.reason.messageID, }; } else if (processingCheckResult.reason.type === 'missing_membership') { condition = { type: 'membership', threadID: processingCheckResult.reason.threadID, userID: processingCheckResult.reason.userID, }; } dispatchWithMetadata( { type: queueDMOpsActionType, payload: { operation: dmOp, timestamp: Date.now(), condition, }, }, dispatchMetadata, ); return; } const newUserIDs = extractUserIDsFromPayload(dmOperationValidator, dmOp); if (newUserIDs.length > 0) { dispatch({ type: processNewUserIDsActionType, payload: { userIDs: newUserIDs }, }); } const dmOpSpec = dmOpSpecs[dmOp.type]; const notificationsCreationDataPromise: Promise = (async () => { if ( dmOperationSpecification.type === dmOperationSpecificationTypes.INBOUND || !dmOpSpec.notificationsCreationData ) { return null; } return await dmOpSpec.notificationsCreationData(dmOp, utilities); })(); const [ { rawMessageInfos, updateInfos, blobOps }, notificationsCreationData, ] = await Promise.all([ dmOpSpec.processDMOperation(dmOp, utilities), notificationsCreationDataPromise, ]); const holderOps = blobOps .map(({ dmOpType, ...holderOp }) => { if ( (dmOpType === 'inbound_only' && dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND) || (dmOpType === 'outbound_only' && dmOperationSpecification.type === dmOperationSpecificationTypes.INBOUND) ) { return null; } return holderOp; }) .filter(Boolean); void processBlobHolders(holderOps); - const { rawMessageInfos: allNewMessageInfos } = - mergeUpdatesWithMessageInfos(rawMessageInfos, updateInfos); - const messagesByThreadID = _groupBy(message => message.threadID)( - allNewMessageInfos, + const newUpdateInfos = getThreadUpdatesForNewMessages( + rawMessageInfos, + updateInfos, + utilities, ); - 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) { - updatedThreadInfosByThreadID[updatedThreadInfo.id] = - updatedThreadInfo; - } - } - - for (const threadID in messagesByThreadID) { - const repliesCountIncreasingMessages = messagesByThreadID[ - threadID - ].filter(message => messageSpecs[message.type].includedInRepliesCount); - - const threadInfo = updatedThreadInfosByThreadID[threadID]; - - if (repliesCountIncreasingMessages.length > 0) { - 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 - // change_thread_read_status operation older - // than it won't flip the status to read. - const time = Math.max( - messagesFromOtherPeers.map(message => message.time), - ); - invariant(threadInfo.thick, 'Thread should be thick'); - - // We aren't checking if the unread timestamp is lower than the time. - // We're doing this because we want to flip the thread to unread after - // any new message from a non-viewer. - updateInfos.push({ - type: updateTypes.UPDATE_THREAD_READ_STATUS, - id: uuid.v4(), - time, - threadID: threadInfo.id, - unread: true, - }); - } + updateInfos.push(...newUpdateInfos); dispatchWithMetadata( { type: processDMOpsActionType, payload: { rawMessageInfos, updateInfos, outboundP2PMessages, composableMessageID, notificationsCreationData, }, }, dispatchMetadata, ); }, [ baseUtilities, dispatchWithMetadata, allPeerUserIDAndDeviceIDs, - currentUserInfo, - threadInfos, dispatch, processBlobHolders, ], ); } function useProcessAndSendDMOperation(): ( dmOperationSpecification: OutboundDMOperationSpecification, ) => Promise { const processDMOps = useProcessDMOperation(); const { getDMOpsSendingPromise } = usePeerToPeerCommunication(); return React.useCallback( async (dmOperationSpecification: OutboundDMOperationSpecification) => { const { promise, dmOpID } = getDMOpsSendingPromise(); await processDMOps(dmOperationSpecification, dmOpID); await promise; }, [getDMOpsSendingPromise, processDMOps], ); } function useSendComposableDMOperation(): ( dmOperationSpecification: OutboundComposableDMOperationSpecification, ) => Promise { - const threadInfos = useSelector(state => state.threadStore.threadInfos); const { getDMOpsSendingPromise } = usePeerToPeerCommunication(); const dispatchWithMetadata = useDispatchWithMetadata(); const allPeerUserIDAndDeviceIDs = useSelector(getAllPeerUserIDAndDeviceIDs); - const currentUserInfo = useSelector(state => state.currentUserInfo); const baseUtilities = useSendDMOperationUtils(); const { processOutboundMessages } = usePeerToPeerCommunication(); const localMessageInfos = useSelector(state => state.messageStore.local); return React.useCallback( async ( dmOperationSpecification: OutboundComposableDMOperationSpecification, ): Promise => { const { viewerID, ...restUtilities } = baseUtilities; if (!viewerID) { console.log('ignored DMOperation because logged out'); return { result: 'failure', failedMessageIDs: [], }; } const utilities: ProcessDMOperationUtilities = { ...restUtilities, viewerID, }; const { promise, dmOpID } = getDMOpsSendingPromise(); const { op, composableMessageID, recipients } = dmOperationSpecification; const localMessageInfo = localMessageInfos[composableMessageID]; if ( localMessageInfo?.outboundP2PMessageIDs && localMessageInfo.outboundP2PMessageIDs.length > 0 ) { processOutboundMessages(localMessageInfo.outboundP2PMessageIDs, dmOpID); try { // This code should never throw. return await promise; } catch (e) { invariant( localMessageInfo.outboundP2PMessageIDs, 'outboundP2PMessageIDs should be defined', ); return { result: 'failure', failedMessageIDs: localMessageInfo.outboundP2PMessageIDs, }; } } const outboundP2PMessages = await createMessagesToPeersFromDMOp( op, recipients, allPeerUserIDAndDeviceIDs, - currentUserInfo, - threadInfos, + utilities, ); const notificationsCreationData = await dmOpSpecs[ op.type ].notificationsCreationData?.(op, utilities); dispatchWithMetadata( { type: processDMOpsActionType, payload: { rawMessageInfos: [], updateInfos: [], outboundP2PMessages, composableMessageID, notificationsCreationData, }, }, { dmOpID, }, ); try { // This code should never throw. return await promise; } catch (e) { return { result: 'failure', failedMessageIDs: outboundP2PMessages.map( message => message.messageID, ), }; } }, [ allPeerUserIDAndDeviceIDs, - currentUserInfo, dispatchWithMetadata, getDMOpsSendingPromise, localMessageInfos, processOutboundMessages, - threadInfos, baseUtilities, ], ); } export { useProcessDMOperation, useProcessAndSendDMOperation, useSendComposableDMOperation, };