diff --git a/lib/shared/dm-ops/add-members-spec.js b/lib/shared/dm-ops/add-members-spec.js --- a/lib/shared/dm-ops/add-members-spec.js +++ b/lib/shared/dm-ops/add-members-spec.js @@ -3,10 +3,7 @@ import invariant from 'invariant'; import uuid from 'uuid'; -import { - createRoleAndPermissionForThickThreads, - createThickRawThreadInfo, -} from './create-thread-spec.js'; +import { createRoleAndPermissionForThickThreads } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, @@ -14,10 +11,7 @@ import { createRepliesCountUpdate } from './dm-op-utils.js'; import type { DMAddMembersOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; -import { - messageTruncationStatus, - type RawMessageInfo, -} from '../../types/message-types.js'; +import { type RawMessageInfo } from '../../types/message-types.js'; import { minimallyEncodeMemberInfo, type ThickRawThreadInfo, @@ -38,104 +32,62 @@ +updateInfos: Array, +threadInfo: ?ThickRawThreadInfo, } { - const { editorID, time, messageID, addedUserIDs, existingThreadDetails } = - dmOperation; + const { editorID, time, messageID, addedUserIDs, threadID } = dmOperation; const addMembersMessage = { type: messageTypes.ADD_MEMBERS, id: messageID, - threadID: existingThreadDetails.threadID, + threadID, creatorID: editorID, time, addedUserIDs: [...addedUserIDs], }; - const viewerIsAdded = addedUserIDs.includes(viewerID); - const updateInfos: Array = []; - const rawMessageInfos: Array = []; - let resultThreadInfo: ?ThickRawThreadInfo; - if (viewerIsAdded) { - const newThread = createThickRawThreadInfo( - { - ...existingThreadDetails, - allMemberIDs: [...existingThreadDetails.allMemberIDs, ...addedUserIDs], - }, - viewerID, - ); - resultThreadInfo = newThread; - updateInfos.push({ - type: updateTypes.JOIN_THREAD, - id: uuid.v4(), - time, - threadInfo: newThread, - rawMessageInfos: [addMembersMessage], - truncationStatus: messageTruncationStatus.EXHAUSTIVE, - rawEntryInfos: [], - }); - const repliesCountUpdate = createRepliesCountUpdate(newThread, [ - addMembersMessage, - ]); - if ( - repliesCountUpdate && - repliesCountUpdate.type === updateTypes.UPDATE_THREAD - ) { - updateInfos.push(repliesCountUpdate); - resultThreadInfo.repliesCount = - repliesCountUpdate.threadInfo.repliesCount; - } - } else { - const currentThreadInfoOptional = - utilities.threadInfos[existingThreadDetails.threadID]; - if (!currentThreadInfoOptional || !currentThreadInfoOptional.thick) { - // We can't perform this operation now. It should be queued for later. - return { - rawMessageInfos: [], - updateInfos: [], - threadInfo: null, - }; - } - const currentThreadInfo: ThickRawThreadInfo = currentThreadInfoOptional; - const defaultRoleID = values(currentThreadInfo.roles).find(role => - roleIsDefaultRole(role), - )?.id; - invariant(defaultRoleID, 'Default role ID must exist'); - const { membershipPermissions } = createRoleAndPermissionForThickThreads( - currentThreadInfo.type, - currentThreadInfo.id, - defaultRoleID, - ); - const newMembers = addedUserIDs - .filter(userID => !userIsMember(currentThreadInfo, userID)) - .map(userID => - minimallyEncodeMemberInfo({ - id: userID, - role: defaultRoleID, - permissions: membershipPermissions, - isSender: editorID === viewerID, - subscription: joinThreadSubscription, - }), - ); - - const newThreadInfo = { - ...currentThreadInfo, - members: [...currentThreadInfo.members, ...newMembers], + const currentThreadInfo = utilities.threadInfos[threadID]; + if (!currentThreadInfo.thick) { + return { + rawMessageInfos: [], + updateInfos: [], + threadInfo: null, }; - resultThreadInfo = newThreadInfo; - const updateWithRepliesCount = createRepliesCountUpdate(newThreadInfo, [ - addMembersMessage, - ]); - updateInfos.push( - updateWithRepliesCount ?? { - type: updateTypes.UPDATE_THREAD, - id: uuid.v4(), - time, - threadInfo: newThreadInfo, - }, - ); - rawMessageInfos.push(addMembersMessage); } + const defaultRoleID = values(currentThreadInfo.roles).find(role => + roleIsDefaultRole(role), + )?.id; + invariant(defaultRoleID, 'Default role ID must exist'); + const { membershipPermissions } = createRoleAndPermissionForThickThreads( + currentThreadInfo.type, + currentThreadInfo.id, + defaultRoleID, + ); + const newMembers = addedUserIDs + .filter(userID => !userIsMember(currentThreadInfo, userID)) + .map(userID => + minimallyEncodeMemberInfo({ + id: userID, + role: defaultRoleID, + permissions: membershipPermissions, + isSender: editorID === viewerID, + subscription: joinThreadSubscription, + }), + ); + + const resultThreadInfo = { + ...currentThreadInfo, + members: [...currentThreadInfo.members, ...newMembers], + }; + const updateWithRepliesCount = createRepliesCountUpdate(resultThreadInfo, [ + addMembersMessage, + ]); + const update = updateWithRepliesCount ?? { + type: updateTypes.UPDATE_THREAD, + id: uuid.v4(), + time, + threadInfo: resultThreadInfo, + }; + return { - rawMessageInfos, - updateInfos, + rawMessageInfos: [addMembersMessage], + updateInfos: [update], threadInfo: resultThreadInfo, }; } @@ -158,17 +110,14 @@ viewerID: string, utilities: ProcessDMOperationUtilities, ) { - if ( - utilities.threadInfos[dmOperation.existingThreadDetails.threadID] || - dmOperation.addedUserIDs.includes(viewerID) - ) { + if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', - threadID: dmOperation.existingThreadDetails.threadID, + threadID: dmOperation.threadID, }, }; }, diff --git a/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js b/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js new file mode 100644 --- /dev/null +++ b/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js @@ -0,0 +1,99 @@ +// @flow + +import uuid from 'uuid'; + +import { createThickRawThreadInfo } from './create-thread-spec.js'; +import type { DMOperationSpec } from './dm-op-spec.js'; +import { createRepliesCountUpdate } from './dm-op-utils.js'; +import type { DMAddViewerToThreadMembersOperation } from '../../types/dm-ops.js'; +import { messageTypes } from '../../types/message-types-enum.js'; +import { + messageTruncationStatus, + type RawMessageInfo, +} from '../../types/message-types.js'; +import { type ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; +import { updateTypes } from '../../types/update-types-enum.js'; +import type { ClientUpdateInfo } from '../../types/update-types.js'; + +function createAddViewerToThreadMembersResults( + dmOperation: DMAddViewerToThreadMembersOperation, + viewerID: string, +): { + +rawMessageInfos: Array, + +updateInfos: Array, + +threadInfo: ?ThickRawThreadInfo, +} { + const { editorID, time, messageID, addedUserIDs, existingThreadDetails } = + dmOperation; + const addMembersMessage = { + type: messageTypes.ADD_MEMBERS, + id: messageID, + threadID: existingThreadDetails.threadID, + creatorID: editorID, + time, + addedUserIDs: [...addedUserIDs], + }; + + const updateInfos: Array = []; + + const resultThreadInfo = createThickRawThreadInfo( + { + ...existingThreadDetails, + allMemberIDs: [...existingThreadDetails.allMemberIDs, ...addedUserIDs], + }, + viewerID, + ); + updateInfos.push({ + type: updateTypes.JOIN_THREAD, + id: uuid.v4(), + time, + threadInfo: resultThreadInfo, + rawMessageInfos: [addMembersMessage], + truncationStatus: messageTruncationStatus.EXHAUSTIVE, + rawEntryInfos: [], + }); + const repliesCountUpdate = createRepliesCountUpdate(resultThreadInfo, [ + addMembersMessage, + ]); + if ( + repliesCountUpdate && + repliesCountUpdate.type === updateTypes.UPDATE_THREAD + ) { + updateInfos.push(repliesCountUpdate); + resultThreadInfo.repliesCount = repliesCountUpdate.threadInfo.repliesCount; + } + return { + rawMessageInfos: [], + updateInfos, + threadInfo: resultThreadInfo, + }; +} + +const addViewerToThreadMembersSpec: DMOperationSpec = + Object.freeze({ + processDMOperation: async ( + dmOperation: DMAddViewerToThreadMembersOperation, + viewerID: string, + ) => { + const { rawMessageInfos, updateInfos } = + createAddViewerToThreadMembersResults(dmOperation, viewerID); + return { rawMessageInfos, updateInfos }; + }, + canBeProcessed( + dmOperation: DMAddViewerToThreadMembersOperation, + viewerID: string, + ) { + if (dmOperation.addedUserIDs.includes(viewerID)) { + return { isProcessingPossible: true }; + } + console.log('Invalid DM operation', dmOperation); + return { + isProcessingPossible: false, + reason: { + type: 'invalid', + }, + }; + }, + }); + +export { addViewerToThreadMembersSpec, createAddViewerToThreadMembersResults }; diff --git a/lib/shared/dm-ops/change-thread-settings-spec.js b/lib/shared/dm-ops/change-thread-settings-spec.js --- a/lib/shared/dm-ops/change-thread-settings-spec.js +++ b/lib/shared/dm-ops/change-thread-settings-spec.js @@ -3,10 +3,11 @@ import invariant from 'invariant'; import uuid from 'uuid'; +import { createAddNewMembersResults } from './add-members-spec.js'; import { - addMembersSpec, - createAddNewMembersResults, -} from './add-members-spec.js'; + addViewerToThreadMembersSpec, + createAddViewerToThreadMembersResults, +} from './add-viewer-to-thread-members-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, @@ -23,6 +24,7 @@ function createAddMembersOperation( dmOperation: DMChangeThreadSettingsOperation, + viewerID: string, ) { const { editorID, time, messageIDsPrefix, changes, existingThreadDetails } = dmOperation; @@ -30,16 +32,39 @@ changes.newMemberIDs && changes.newMemberIDs.length > 0 ? [...new Set(changes.newMemberIDs)] : []; + if (newMemberIDs.includes(viewerID)) { + return { + type: 'add_viewer_to_thread_members', + editorID, + time, + messageID: `${messageIDsPrefix}/add_members`, + addedUserIDs: newMemberIDs, + existingThreadDetails, + }; + } return { type: 'add_members', editorID, time, messageID: `${messageIDsPrefix}/add_members`, addedUserIDs: newMemberIDs, - existingThreadDetails, + threadID: existingThreadDetails.threadID, }; } +function processAddMembersOperation( + dmOperation: DMChangeThreadSettingsOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, +) { + const operation = createAddMembersOperation(dmOperation, viewerID); + if (operation.type === 'add_viewer_to_thread_members') { + return createAddViewerToThreadMembersResults(operation, viewerID); + } else { + return createAddNewMembersResults(operation, viewerID, utilities); + } +} + const changeThreadSettingsSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( @@ -63,9 +88,8 @@ const rawMessageInfos: Array = []; if (changes.newMemberIDs && changes.newMemberIDs.length > 0) { - const addMembersOperation = createAddMembersOperation(dmOperation); - const addMembersResult = createAddNewMembersResults( - addMembersOperation, + const addMembersResult = processAddMembersOperation( + dmOperation, viewerID, utilities, ); @@ -150,11 +174,25 @@ viewerID: string, utilities: ProcessDMOperationUtilities, ) { - return addMembersSpec.canBeProcessed( - createAddMembersOperation(dmOperation), - viewerID, - utilities, - ); + const operation = createAddMembersOperation(dmOperation, viewerID); + if (operation.type === 'add_viewer_to_thread_members') { + return addViewerToThreadMembersSpec.canBeProcessed( + operation, + viewerID, + utilities, + ); + } else if ( + utilities.threadInfos[dmOperation.existingThreadDetails.threadID] + ) { + return { isProcessingPossible: true }; + } + return { + isProcessingPossible: false, + reason: { + type: 'missing_thread', + threadID: dmOperation.existingThreadDetails.threadID, + }, + }; }, }); diff --git a/lib/shared/dm-ops/dm-op-spec.js b/lib/shared/dm-ops/dm-op-spec.js --- a/lib/shared/dm-ops/dm-op-spec.js +++ b/lib/shared/dm-ops/dm-op-spec.js @@ -24,6 +24,8 @@ | { +isProcessingPossible: true } | { +isProcessingPossible: false, - +reason: { +type: 'missing_thread', +threadID: string }, + +reason: + | { +type: 'missing_thread', +threadID: string } + | { +type: 'invalid' }, }, }; diff --git a/lib/shared/dm-ops/dm-op-specs.js b/lib/shared/dm-ops/dm-op-specs.js --- a/lib/shared/dm-ops/dm-op-specs.js +++ b/lib/shared/dm-ops/dm-op-specs.js @@ -1,6 +1,7 @@ // @flow import { addMembersSpec } from './add-members-spec.js'; +import { addViewerToThreadMembersSpec } from './add-viewer-to-thread-members-spec.js'; import { changeThreadSettingsSpec } from './change-thread-settings-spec.js'; import { createSidebarSpec } from './create-sidebar-spec.js'; import { createThreadSpec } from './create-thread-spec.js'; @@ -22,6 +23,7 @@ [dmOperationTypes.SEND_REACTION_MESSAGE]: sendReactionMessageSpec, [dmOperationTypes.SEND_EDIT_MESSAGE]: sendEditMessageSpec, [dmOperationTypes.ADD_MEMBERS]: addMembersSpec, + [dmOperationTypes.ADD_VIEWER_TO_THREAD_MEMBERS]: addViewerToThreadMembersSpec, [dmOperationTypes.JOIN_THREAD]: joinThreadSpec, [dmOperationTypes.LEAVE_THREAD]: leaveThreadSpec, [dmOperationTypes.REMOVE_MEMBERS]: removeMembersSpec, diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -22,6 +22,7 @@ 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', @@ -173,22 +174,41 @@ text: t.String, }); -export type DMAddMembersOperation = { - +type: 'add_members', +type DMAddMembersBase = { +editorID: string, +time: number, +messageID: string, +addedUserIDs: $ReadOnlyArray, - +existingThreadDetails: CreateThickRawThreadInfoInput, }; +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), - editorID: tUserID, - time: t.Number, - messageID: t.String, - addedUserIDs: t.list(tUserID), + 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 = { @@ -278,6 +298,7 @@ | DMSendReactionMessageOperation | DMSendEditMessageOperation | DMAddMembersOperation + | DMAddViewerToThreadMembersOperation | DMJoinThreadOperation | DMLeaveThreadOperation | DMRemoveMembersOperation @@ -289,6 +310,7 @@ dmSendReactionMessageOperationValidator, dmSendEditMessageOperationValidator, dmAddMembersOperationValidator, + dmAddViewerToThreadMembersValidator, dmJoinThreadOperationValidator, dmLeaveThreadOperationValidator, dmRemoveMembersOperationValidator,