diff --git a/lib/shared/dm-ops/dm-op-utils.js b/lib/shared/dm-ops/dm-op-utils.js --- a/lib/shared/dm-ops/dm-op-utils.js +++ b/lib/shared/dm-ops/dm-op-utils.js @@ -1,20 +1,32 @@ // @flow +import invariant from 'invariant'; +import * as React from 'react'; import uuid from 'uuid'; import { dmOpSpecs } from './dm-op-specs.js'; -import type { DMOperation } from '../../types/dm-ops.js'; +import { useProcessAndSendDMOperation } from './process-dm-ops.js'; +import type { + CreateThickRawThreadInfoInput, + DMAddMembersOperation, + DMAddViewerToThreadMembersOperation, + DMOperation, +} from '../../types/dm-ops.js'; +import type { 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 } from '../../types/thread-types-enum.js'; +import type { RawThreadInfos } 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 { getContentSigningKey } from '../../utils/crypto-utils.js'; +import { useSelector } from '../../utils/redux-utils.js'; function generateMessagesToPeers( message: DMOperation, @@ -57,7 +69,8 @@ +op: DMOperation, +recipients: | { +type: 'all_peer_devices' | 'self_devices' } - | { +type: 'some_users', +userIDs: $ReadOnlyArray }, + | { +type: 'some_users', +userIDs: $ReadOnlyArray } + | { +type: 'all_thread_members', +threadID: string }, +sendOnly?: boolean, }; @@ -80,6 +93,7 @@ +deviceID: string, }>, currentUserInfo: ?CurrentUserInfo, + threadInfos: RawThreadInfos, ): Promise<$ReadOnlyArray> { if (!currentUserInfo?.id) { return []; @@ -95,6 +109,14 @@ peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter(peer => userIDs.has(peer.userID), ); + } else if (operation.recipients.type === 'all_thread_members') { + const members = threadInfos[operation.recipients.threadID]?.members ?? []; + const memberIDs = members.map(member => member.id); + + const userIDs = new Set(memberIDs); + peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter(peer => + userIDs.has(peer.userID), + ); } const thisDeviceID = await getContentSigningKey(); @@ -104,4 +126,93 @@ return generateMessagesToPeers(operation.op, targetPeers); } -export { createMessagesToPeersFromDMOp }; +function getCreateThickRawThreadInfoInputFromThreadInfo( + threadInfo: ThreadInfo, +): 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, + allMemberIDs: threadInfo.members.map(member => member.id), + 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, + }; +} + +function useAddDMThreadMembers(): ( + newMemberIDs: $ReadOnlyArray, + threadInfo: ThreadInfo, +) => Promise { + const viewerID = useSelector( + state => state.currentUserInfo && state.currentUserInfo.id, + ); + const processAndSendDMOperation = useProcessAndSendDMOperation(); + + return React.useCallback( + async (newMemberIDs: $ReadOnlyArray, threadInfo: ThreadInfo) => { + const existingThreadDetails = + getCreateThickRawThreadInfoInputFromThreadInfo(threadInfo); + + invariant(viewerID, 'viewerID should be set'); + const viewerToThreadMembersOperation: 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: viewerToThreadMembersOperation, + 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 addMembersOperationSpecification: OutboundDMOperationSpecification = + { + type: dmOperationSpecificationTypes.OUTBOUND, + op: addMembersOperation, + recipients: { + type: 'some_users', + userIDs: threadInfo.members + .map(member => member.id) + .filter(memberID => !newMemberIDsSet.has(memberID)), + }, + }; + + void Promise.all([ + processAndSendDMOperation(viewerOperationSpecification), + processAndSendDMOperation(addMembersOperationSpecification), + ]); + }, + [processAndSendDMOperation, viewerID], + ); +} + +export { createMessagesToPeersFromDMOp, useAddDMThreadMembers }; diff --git a/lib/shared/dm-ops/process-dm-ops.js b/lib/shared/dm-ops/process-dm-ops.js --- a/lib/shared/dm-ops/process-dm-ops.js +++ b/lib/shared/dm-ops/process-dm-ops.js @@ -78,6 +78,7 @@ dmOperationSpecification, allPeerUserIDAndDeviceIDs, currentUserInfo, + threadInfos, ); }