diff --git a/lib/shared/dm-ops/add-members-spec.js b/lib/shared/dm-ops/add-members-spec.js index b8e0a9178..2dd0cc3d2 100644 --- a/lib/shared/dm-ops/add-members-spec.js +++ b/lib/shared/dm-ops/add-members-spec.js @@ -1,175 +1,194 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import { createRoleAndPermissionForThickThreads, createThickRawThreadInfo, } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { createUpdateUnreadCountUpdate } 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 { minimallyEncodeMemberInfo, type ThickRawThreadInfo, } from '../../types/minimally-encoded-thread-permissions-types.js'; import { joinThreadSubscription } from '../../types/subscription-types.js'; import type { ThickMemberInfo } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { values } from '../../utils/objects.js'; import { roleIsDefaultRole, userIsMember } from '../thread-utils.js'; function createAddNewMembersResults( dmOperation: DMAddMembersOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ): { +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 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: [], }, { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID: existingThreadDetails.threadID, unread: true, }, ); const repliesCountUpdate = createUpdateUnreadCountUpdate(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], }; resultThreadInfo = newThreadInfo; const updateWithRepliesCount = createUpdateUnreadCountUpdate( newThreadInfo, [addMembersMessage], ); updateInfos.push( updateWithRepliesCount ?? { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: newThreadInfo, }, { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID: existingThreadDetails.threadID, unread: true, }, ); rawMessageInfos.push(addMembersMessage); } return { rawMessageInfos, updateInfos, threadInfo: resultThreadInfo, }; } const addMembersSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMAddMembersOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { rawMessageInfos, updateInfos } = createAddNewMembersResults( dmOperation, viewerID, utilities, ); return { rawMessageInfos, updateInfos }; }, + canBeProcessed( + dmOperation: DMAddMembersOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) { + if ( + utilities.threadInfos[dmOperation.existingThreadDetails.threadID] || + dmOperation.addedUserIDs.includes(viewerID) + ) { + return { isProcessingPossible: true }; + } + return { + isProcessingPossible: false, + reason: { + type: 'missing_thread', + threadID: dmOperation.existingThreadDetails.threadID, + }, + }; + }, }); export { addMembersSpec, createAddNewMembersResults }; diff --git a/lib/shared/dm-ops/change-thread-settings-spec.js b/lib/shared/dm-ops/change-thread-settings-spec.js index de2fe9d19..3aa9b68d5 100644 --- a/lib/shared/dm-ops/change-thread-settings-spec.js +++ b/lib/shared/dm-ops/change-thread-settings-spec.js @@ -1,162 +1,171 @@ // @flow +import invariant from 'invariant'; import uuid from 'uuid'; -import { createAddNewMembersResults } from './add-members-spec.js'; +import { + addMembersSpec, + createAddNewMembersResults, +} from './add-members-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { createUpdateUnreadCountUpdate } from './dm-op-utils.js'; import type { DMChangeThreadSettingsOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { RawMessageInfo } from '../../types/message-types.js'; import type { RawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { LegacyRawThreadInfo } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { values } from '../../utils/objects.js'; +function createAddMembersOperation( + dmOperation: DMChangeThreadSettingsOperation, +) { + const { editorID, time, messageIDsPrefix, changes, existingThreadDetails } = + dmOperation; + const newMemberIDs = + changes.newMemberIDs && changes.newMemberIDs.length > 0 + ? [...new Set(changes.newMemberIDs)] + : []; + return { + type: 'add_members', + editorID, + time, + messageID: `${messageIDsPrefix}/add_members`, + addedUserIDs: newMemberIDs, + existingThreadDetails, + }; +} + const changeThreadSettingsSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMChangeThreadSettingsOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { editorID, time, changes, messageIDsPrefix, existingThreadDetails, } = dmOperation; const { name, description, color, avatar } = changes; const threadID = existingThreadDetails.threadID; - const newMemberIDs = - changes.newMemberIDs && changes.newMemberIDs.length > 0 - ? [...new Set(changes.newMemberIDs)] - : null; - let threadInfoToUpdate: ?(RawThreadInfo | LegacyRawThreadInfo) = utilities.threadInfos[threadID]; - if (!threadInfoToUpdate && !newMemberIDs?.includes(viewerID)) { - // We can't perform this operation now. It should be queued for later. - return { - rawMessageInfos: [], - updateInfos: [], - }; - } - const updateInfos: Array = []; const rawMessageInfos: Array = []; - if (newMemberIDs) { + if (changes.newMemberIDs && changes.newMemberIDs.length > 0) { + const addMembersOperation = createAddMembersOperation(dmOperation); const addMembersResult = createAddNewMembersResults( - { - type: 'add_members', - editorID, - time, - messageID: `${messageIDsPrefix}/add_members`, - addedUserIDs: newMemberIDs, - existingThreadDetails, - }, + addMembersOperation, viewerID, utilities, ); if (addMembersResult.threadInfo) { threadInfoToUpdate = addMembersResult.threadInfo; } updateInfos.push(...addMembersResult.updateInfos); rawMessageInfos.push(...addMembersResult.rawMessageInfos); } - if (!threadInfoToUpdate || !threadInfoToUpdate.thick) { - // We can't perform this operation now. It should be queued for later. - return { - rawMessageInfos: [], - updateInfos: [], - }; - } + invariant(threadInfoToUpdate?.thick, 'Thread should be thick'); const changedFields: { [string]: string | number } = {}; if (name !== undefined && name !== null) { changedFields.name = name; threadInfoToUpdate = { ...threadInfoToUpdate, name, }; } if (description !== undefined && description !== null) { changedFields.description = description; threadInfoToUpdate = { ...threadInfoToUpdate, description, }; } if (color) { changedFields.color = color; threadInfoToUpdate = { ...threadInfoToUpdate, color, }; } if (avatar) { changedFields.avatar = JSON.stringify(avatar); threadInfoToUpdate = { ...threadInfoToUpdate, avatar, }; } for (const fieldName in changedFields) { const newValue = changedFields[fieldName]; rawMessageInfos.push({ type: messageTypes.CHANGE_SETTINGS, threadID, creatorID: editorID, time, field: fieldName, value: newValue, id: `${messageIDsPrefix}/${fieldName}`, }); } const repliesCountUpdate = createUpdateUnreadCountUpdate( threadInfoToUpdate, rawMessageInfos, ); if (repliesCountUpdate) { updateInfos.push(repliesCountUpdate); } else if (values(changedFields).length > 0) { updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: threadInfoToUpdate, }); } if (rawMessageInfos.length > 0) { updateInfos.push({ type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID, unread: true, }); } return { rawMessageInfos, updateInfos, }; }, + canBeProcessed( + dmOperation: DMChangeThreadSettingsOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) { + return addMembersSpec.canBeProcessed( + createAddMembersOperation(dmOperation), + viewerID, + utilities, + ); + }, }); export { changeThreadSettingsSpec }; diff --git a/lib/shared/dm-ops/create-sidebar-spec.js b/lib/shared/dm-ops/create-sidebar-spec.js index a588817f8..f18cd6e93 100644 --- a/lib/shared/dm-ops/create-sidebar-spec.js +++ b/lib/shared/dm-ops/create-sidebar-spec.js @@ -1,110 +1,113 @@ // @flow import uuid from 'uuid'; import { createThickRawThreadInfo } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMCreateSidebarOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { type RawMessageInfo, messageTruncationStatus, } from '../../types/message-types.js'; import { threadTypes } from '../../types/thread-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { isInvalidSidebarSource } from '../message-utils.js'; const createSidebarSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMCreateSidebarOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, time, parentThreadID, memberIDs, sourceMessageID, roleID, newSidebarSourceMessageID, newCreateSidebarMessageID, } = dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; const rawThreadInfo = createThickRawThreadInfo( { threadID, threadType: threadTypes.THICK_SIDEBAR, creationTime: time, parentThreadID, allMemberIDs, roleID, creatorID, sourceMessageID, containingThreadID: parentThreadID, repliesCount: 1, }, viewerID, ); const sourceMessage = await utilities.fetchMessage(sourceMessageID); if (!sourceMessage) { throw new Error( `could not find sourceMessage ${sourceMessageID}... probably ` + 'joined thick thread ${parentThreadID} after its creation', ); } if (isInvalidSidebarSource(sourceMessage)) { throw new Error( `sourceMessage ${sourceMessageID} is an invalid sidebar source`, ); } const rawMessageInfos: Array = [ { type: messageTypes.SIDEBAR_SOURCE, id: newSidebarSourceMessageID, threadID, creatorID, time, sourceMessage, }, { type: messageTypes.CREATE_SIDEBAR, id: newCreateSidebarMessageID, threadID, creatorID, time: time + 1, sourceMessageAuthorID: sourceMessage.creatorID, initialThreadState: { parentThreadID, color: rawThreadInfo.color, memberIDs: allMemberIDs, }, }, ]; const threadJoinUpdateInfo = { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: rawThreadInfo, rawMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }; return { rawMessageInfos: [], // included in updateInfos below updateInfos: [threadJoinUpdateInfo], }; }, + canBeProcessed() { + return { isProcessingPossible: true }; + }, }); export { createSidebarSpec }; diff --git a/lib/shared/dm-ops/create-thread-spec.js b/lib/shared/dm-ops/create-thread-spec.js index 8307e6667..2c609010b 100644 --- a/lib/shared/dm-ops/create-thread-spec.js +++ b/lib/shared/dm-ops/create-thread-spec.js @@ -1,196 +1,199 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec } from './dm-op-spec.js'; import { specialRoles } from '../../permissions/special-roles.js'; import { getAllThreadPermissions, makePermissionsBlob, getThickThreadRolePermissionsBlob, } from '../../permissions/thread-permissions.js'; import type { CreateThickRawThreadInfoInput, DMCreateThreadOperation, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { type RawMessageInfo, messageTruncationStatus, } from '../../types/message-types.js'; import { type ThickRawThreadInfo, type RoleInfo, minimallyEncodeMemberInfo, minimallyEncodeRoleInfo, minimallyEncodeThreadCurrentUserInfo, } from '../../types/minimally-encoded-thread-permissions-types.js'; import { joinThreadSubscription } from '../../types/subscription-types.js'; import type { ThreadPermissionsInfo } from '../../types/thread-permission-types.js'; import type { ThickThreadType } from '../../types/thread-types-enum.js'; import type { ThickMemberInfo } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { generatePendingThreadColor } from '../color-utils.js'; function createRoleAndPermissionForThickThreads( threadType: ThickThreadType, threadID: string, roleID: string, ): { +role: RoleInfo, +membershipPermissions: ThreadPermissionsInfo } { const rolePermissions = getThickThreadRolePermissionsBlob(threadType); const membershipPermissions = getAllThreadPermissions( makePermissionsBlob(rolePermissions, null, threadID, threadType), threadID, ); const role: RoleInfo = { ...minimallyEncodeRoleInfo({ id: roleID, name: 'Members', permissions: rolePermissions, isDefault: true, }), specialRole: specialRoles.DEFAULT_ROLE, }; return { membershipPermissions, role, }; } type MutableThickRawThreadInfo = { ...ThickRawThreadInfo }; function createThickRawThreadInfo( input: CreateThickRawThreadInfoInput, viewerID: string, ): MutableThickRawThreadInfo { const { threadID, threadType, creationTime, parentThreadID, allMemberIDs, roleID, creatorID, name, avatar, description, color, containingThreadID, community, sourceMessageID, repliesCount, pinnedCount, } = input; const threadColor = color ?? generatePendingThreadColor(allMemberIDs); const { membershipPermissions, role } = createRoleAndPermissionForThickThreads(threadType, threadID, roleID); const newThread: MutableThickRawThreadInfo = { thick: true, minimallyEncoded: true, id: threadID, type: threadType, color: threadColor, creationTime, parentThreadID, members: allMemberIDs.map(memberID => minimallyEncodeMemberInfo({ id: memberID, role: role.id, permissions: membershipPermissions, isSender: memberID === viewerID, subscription: joinThreadSubscription, }), ), roles: { [role.id]: role, }, currentUser: minimallyEncodeThreadCurrentUserInfo({ role: role.id, permissions: membershipPermissions, subscription: joinThreadSubscription, unread: creatorID !== viewerID, }), repliesCount: repliesCount ?? 0, name, avatar, description, containingThreadID, community, }; if (sourceMessageID) { newThread.sourceMessageID = sourceMessageID; } if (pinnedCount) { newThread.pinnedCount = pinnedCount; } return newThread; } const createThreadSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMCreateThreadOperation, viewerID: string, ) => { const { threadID, creatorID, time, threadType, memberIDs, roleID, newMessageID, } = dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; const rawThreadInfo = createThickRawThreadInfo( { threadID, threadType, creationTime: time, allMemberIDs, roleID, creatorID, }, viewerID, ); const rawMessageInfos: Array = [ { type: messageTypes.CREATE_THREAD, id: newMessageID, threadID, creatorID, time, initialThreadState: { type: threadType, color: rawThreadInfo.color, memberIDs: allMemberIDs, }, }, ]; const threadJoinUpdateInfo = { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: rawThreadInfo, rawMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }; return { rawMessageInfos: [], // included in updateInfos below updateInfos: [threadJoinUpdateInfo], }; }, + canBeProcessed() { + return { isProcessingPossible: true }; + }, }); export { createThickRawThreadInfo, createThreadSpec, createRoleAndPermissionForThickThreads, }; diff --git a/lib/shared/dm-ops/dm-op-spec.js b/lib/shared/dm-ops/dm-op-spec.js index ba327c253..c4e5725c0 100644 --- a/lib/shared/dm-ops/dm-op-spec.js +++ b/lib/shared/dm-ops/dm-op-spec.js @@ -1,19 +1,29 @@ // @flow import type { DMOperation, DMOperationResult } from '../../types/dm-ops.js'; import type { RawMessageInfo } from '../../types/message-types.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; export type ProcessDMOperationUtilities = { // Needed to fetch sidebar source messages +fetchMessage: (messageID: string) => Promise, +threadInfos: RawThreadInfos, }; export type DMOperationSpec = { +processDMOperation: ( dmOp: DMOp, viewerID: string, utilities: ProcessDMOperationUtilities, ) => Promise, + +canBeProcessed: ( + dmOp: DMOp, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) => + | { +isProcessingPossible: true } + | { + +isProcessingPossible: false, + +reason: { +type: 'missing_thread', +threadID: string }, + }, }; diff --git a/lib/shared/dm-ops/join-thread-spec.js b/lib/shared/dm-ops/join-thread-spec.js index d842edf08..9949faad7 100644 --- a/lib/shared/dm-ops/join-thread-spec.js +++ b/lib/shared/dm-ops/join-thread-spec.js @@ -1,158 +1,165 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import { createRoleAndPermissionForThickThreads, createThickRawThreadInfo, } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { createUpdateUnreadCountUpdate } from './dm-op-utils.js'; import type { DMJoinThreadOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { messageTruncationStatus, type RawMessageInfo, } from '../../types/message-types.js'; -import { - minimallyEncodeMemberInfo, - type ThickRawThreadInfo, -} from '../../types/minimally-encoded-thread-permissions-types.js'; +import { minimallyEncodeMemberInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import { joinThreadSubscription } from '../../types/subscription-types.js'; import type { ThickMemberInfo } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { values } from '../../utils/objects.js'; import { roleIsDefaultRole, userIsMember } from '../thread-utils.js'; const joinThreadSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMJoinThreadOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { editorID, time, messageID, existingThreadDetails } = dmOperation; + const currentThreadInfo = + utilities.threadInfos[existingThreadDetails.threadID]; + const joinThreadMessage = { type: messageTypes.JOIN_THREAD, id: messageID, threadID: existingThreadDetails.threadID, creatorID: editorID, time, }; - const currentThreadInfoOptional = - utilities.threadInfos[existingThreadDetails.threadID]; - if (userIsMember(currentThreadInfoOptional, editorID)) { + if (userIsMember(currentThreadInfo, editorID)) { return { rawMessageInfos: [joinThreadMessage], updateInfos: [ { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID: existingThreadDetails.threadID, unread: true, }, ], }; } const updateInfos: Array = []; const rawMessageInfos: Array = []; if (viewerID === editorID) { const newThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, allMemberIDs: [...existingThreadDetails.allMemberIDs, editorID], }, viewerID, ); updateInfos.push( { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: newThreadInfo, rawMessageInfos: [joinThreadMessage], truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }, { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID: existingThreadDetails.threadID, unread: true, }, ); const repliesCountUpdate = createUpdateUnreadCountUpdate(newThreadInfo, [ joinThreadMessage, ]); if (repliesCountUpdate) { updateInfos.push(repliesCountUpdate); } } else { - if (!currentThreadInfoOptional || !currentThreadInfoOptional.thick) { - // We can't perform this operation now. It should be queued for later. - return { - rawMessageInfos: [], - updateInfos: [], - }; - } - const currentThreadInfo: ThickRawThreadInfo = currentThreadInfoOptional; + invariant(currentThreadInfo.thick, 'Thread should be thick'); rawMessageInfos.push(joinThreadMessage); 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 member = minimallyEncodeMemberInfo({ id: editorID, role: defaultRoleID, permissions: membershipPermissions, isSender: editorID === viewerID, subscription: joinThreadSubscription, }); const updatedThreadInfo = { ...currentThreadInfo, members: [...currentThreadInfo.members, member], }; const updateWithRepliesCount = createUpdateUnreadCountUpdate( updatedThreadInfo, [joinThreadMessage], ); updateInfos.push( updateWithRepliesCount ?? { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }, { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID: existingThreadDetails.threadID, unread: true, }, ); } return { rawMessageInfos, updateInfos, }; }, + canBeProcessed( + dmOperation: DMJoinThreadOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) { + if (utilities.threadInfos[dmOperation.existingThreadDetails.threadID]) { + return { isProcessingPossible: true }; + } + return { + isProcessingPossible: false, + reason: { + type: 'missing_thread', + threadID: dmOperation.existingThreadDetails.threadID, + }, + }; + }, }); export { joinThreadSpec }; diff --git a/lib/shared/dm-ops/leave-thread-spec.js b/lib/shared/dm-ops/leave-thread-spec.js index c7378db5c..495c478fe 100644 --- a/lib/shared/dm-ops/leave-thread-spec.js +++ b/lib/shared/dm-ops/leave-thread-spec.js @@ -1,91 +1,100 @@ // @flow +import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { createUpdateUnreadCountUpdate } from './dm-op-utils.js'; import type { DMLeaveThreadOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; -import type { ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import { threadTypes } from '../../types/thread-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { userIsMember } from '../thread-utils.js'; const leaveThreadSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMLeaveThreadOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { editorID, time, messageID, threadID } = dmOperation; - const threadInfoOptional = utilities.threadInfos[threadID]; - if (!threadInfoOptional || !threadInfoOptional.thick) { - // We can't perform this operation now. It should be queued for later. - return { - rawMessageInfos: [], - updateInfos: [], - }; - } - const threadInfo: ThickRawThreadInfo = threadInfoOptional; + const threadInfo = utilities.threadInfos[threadID]; + invariant(threadInfo.thick, 'Thread should be thick'); const leaveThreadMessage = { type: messageTypes.LEAVE_THREAD, id: messageID, threadID, creatorID: editorID, time, }; const updateInfos: Array = []; if ( viewerID === editorID && userIsMember(threadInfo, editorID) && (threadInfo.type !== threadTypes.THICK_SIDEBAR || (threadInfo.parentThreadID && !utilities.threadInfos[threadInfo.parentThreadID])) ) { updateInfos.push({ type: updateTypes.DELETE_THREAD, id: uuid.v4(), time, threadID, }); } else { const updatedThreadInfo = { ...threadInfo, members: threadInfo.members.filter(member => member.id !== editorID), }; const updateWithRepliesCount = createUpdateUnreadCountUpdate( updatedThreadInfo, [leaveThreadMessage], ); updateInfos.push( updateWithRepliesCount ?? { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }, { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID, unread: true, }, ); } return { rawMessageInfos: [leaveThreadMessage], updateInfos, }; }, + canBeProcessed( + dmOperation: DMLeaveThreadOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) { + if (utilities.threadInfos[dmOperation.threadID]) { + return { isProcessingPossible: true }; + } + return { + isProcessingPossible: false, + reason: { + type: 'missing_thread', + threadID: dmOperation.threadID, + }, + }; + }, }); export { leaveThreadSpec }; diff --git a/lib/shared/dm-ops/process-dm-ops.js b/lib/shared/dm-ops/process-dm-ops.js index dbe228d01..747fed724 100644 --- a/lib/shared/dm-ops/process-dm-ops.js +++ b/lib/shared/dm-ops/process-dm-ops.js @@ -1,50 +1,59 @@ // @flow import * as React from 'react'; import { dmOpSpecs } from './dm-op-specs.js'; import { useLoggedInUserInfo } from '../../hooks/account-hooks.js'; import { useGetLatestMessageEdit } from '../../hooks/latest-message-edit.js'; import { type DMOperation, processDMOpsActionType, } from '../../types/dm-ops.js'; import { useDispatch, useSelector } from '../../utils/redux-utils.js'; function useProcessDMOperation(): (dmOp: DMOperation) => Promise { const fetchMessage = useGetLatestMessageEdit(); const threadInfos = useSelector(state => state.threadStore.threadInfos); const utilities = React.useMemo( () => ({ fetchMessage, threadInfos, }), [fetchMessage, threadInfos], ); const dispatch = useDispatch(); const loggedInUserInfo = useLoggedInUserInfo(); const viewerID = loggedInUserInfo?.id; return React.useCallback( async (dmOp: DMOperation) => { if (!viewerID) { console.log('ignored DMOperation because logged out'); return; } + const processingCheckResult = dmOpSpecs[dmOp.type].canBeProcessed( + dmOp, + viewerID, + utilities, + ); + if (!processingCheckResult.isProcessingPossible) { + // TODO queue for later + return; + } const { rawMessageInfos, updateInfos } = await dmOpSpecs[ dmOp.type ].processDMOperation(dmOp, viewerID, utilities); dispatch({ type: processDMOpsActionType, payload: { rawMessageInfos, updateInfos, }, }); }, [dispatch, viewerID, utilities], ); } export { useProcessDMOperation }; diff --git a/lib/shared/dm-ops/remove-members-spec.js b/lib/shared/dm-ops/remove-members-spec.js index c9c706ca9..e7b39752a 100644 --- a/lib/shared/dm-ops/remove-members-spec.js +++ b/lib/shared/dm-ops/remove-members-spec.js @@ -1,95 +1,104 @@ // @flow +import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { createUpdateUnreadCountUpdate } from './dm-op-utils.js'; import type { DMRemoveMembersOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; -import type { ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import { threadTypes } from '../../types/thread-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; const removeMembersSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMRemoveMembersOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { editorID, time, messageID, threadID, removedUserIDs } = dmOperation; - const threadInfoOptional = utilities.threadInfos[threadID]; - if (!threadInfoOptional || !threadInfoOptional.thick) { - // We can't perform this operation now. It should be queued for later. - return { - rawMessageInfos: [], - updateInfos: [], - }; - } - const threadInfo: ThickRawThreadInfo = threadInfoOptional; + const threadInfo = utilities.threadInfos[threadID]; + invariant(threadInfo.thick, 'Thread should be thick'); const removeMembersMessage = { type: messageTypes.REMOVE_MEMBERS, id: messageID, threadID, time, creatorID: editorID, removedUserIDs: [...removedUserIDs], }; const removedUserIDsSet = new Set(removedUserIDs); const viewerIsRemoved = removedUserIDsSet.has(viewerID); const updateInfos: Array = []; if ( viewerIsRemoved && (threadInfo.type !== threadTypes.THICK_SIDEBAR || (threadInfo.parentThreadID && !utilities.threadInfos[threadInfo.parentThreadID])) ) { updateInfos.push({ type: updateTypes.DELETE_THREAD, id: uuid.v4(), time, threadID, }); } else { const updatedThreadInfo = { ...threadInfo, members: threadInfo.members.filter( member => !removedUserIDsSet.has(member.id), ), }; const updateWithRepliesCount = createUpdateUnreadCountUpdate( updatedThreadInfo, [removeMembersMessage], ); updateInfos.push( updateWithRepliesCount ?? { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }, { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID, unread: true, }, ); } return { rawMessageInfos: [removeMembersMessage], updateInfos, }; }, + canBeProcessed( + dmOperation: DMRemoveMembersOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) { + if (utilities.threadInfos[dmOperation.threadID]) { + return { isProcessingPossible: true }; + } + return { + isProcessingPossible: false, + reason: { + type: 'missing_thread', + threadID: dmOperation.threadID, + }, + }; + }, }); export { removeMembersSpec }; diff --git a/lib/shared/dm-ops/send-edit-message-spec.js b/lib/shared/dm-ops/send-edit-message-spec.js index 560c85945..558413d37 100644 --- a/lib/shared/dm-ops/send-edit-message-spec.js +++ b/lib/shared/dm-ops/send-edit-message-spec.js @@ -1,60 +1,74 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { createUpdateUnreadCountUpdate } from './dm-op-utils.js'; import type { DMSendEditMessageOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; const sendEditMessageSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMSendEditMessageOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, time, messageID, targetMessageID, text } = dmOperation; const editMessage = { type: messageTypes.EDIT_MESSAGE, id: messageID, threadID, creatorID, time, targetMessageID, text, }; const updateInfos: Array = []; if (creatorID !== viewerID) { updateInfos.push({ type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID, unread: true, }); } const threadInfo = utilities.threadInfos[threadID]; - if (threadInfo) { - const repliesCountUpdate = createUpdateUnreadCountUpdate(threadInfo, [ - editMessage, - ]); - if (repliesCountUpdate) { - updateInfos.push(repliesCountUpdate); - } + const repliesCountUpdate = createUpdateUnreadCountUpdate(threadInfo, [ + editMessage, + ]); + if (repliesCountUpdate) { + updateInfos.push(repliesCountUpdate); } return { rawMessageInfos: [editMessage], updateInfos, }; }, + canBeProcessed( + dmOperation: DMSendEditMessageOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) { + if (utilities.threadInfos[dmOperation.threadID]) { + return { isProcessingPossible: true }; + } + return { + isProcessingPossible: false, + reason: { + type: 'missing_thread', + threadID: dmOperation.threadID, + }, + }; + }, }); export { sendEditMessageSpec }; diff --git a/lib/shared/dm-ops/send-reaction-message-spec.js b/lib/shared/dm-ops/send-reaction-message-spec.js index 2a26e0fc6..ef19112be 100644 --- a/lib/shared/dm-ops/send-reaction-message-spec.js +++ b/lib/shared/dm-ops/send-reaction-message-spec.js @@ -1,68 +1,82 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { createUpdateUnreadCountUpdate } from './dm-op-utils.js'; import type { DMSendReactionMessageOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; const sendReactionMessageSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMSendReactionMessageOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, time, messageID, targetMessageID, reaction, action, } = dmOperation; const reactionMessage = { type: messageTypes.REACTION, id: messageID, threadID, creatorID, time, targetMessageID, reaction, action, }; const updateInfos: Array = []; if (creatorID !== viewerID) { updateInfos.push({ type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID, unread: true, }); } const threadInfo = utilities.threadInfos[threadID]; - if (threadInfo) { - const repliesCountUpdate = createUpdateUnreadCountUpdate(threadInfo, [ - reactionMessage, - ]); - if (repliesCountUpdate) { - updateInfos.push(repliesCountUpdate); - } + const repliesCountUpdate = createUpdateUnreadCountUpdate(threadInfo, [ + reactionMessage, + ]); + if (repliesCountUpdate) { + updateInfos.push(repliesCountUpdate); } return { rawMessageInfos: [reactionMessage], updateInfos, }; }, + canBeProcessed( + dmOperation: DMSendReactionMessageOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) { + if (utilities.threadInfos[dmOperation.threadID]) { + return { isProcessingPossible: true }; + } + return { + isProcessingPossible: false, + reason: { + type: 'missing_thread', + threadID: dmOperation.threadID, + }, + }; + }, }); export { sendReactionMessageSpec }; diff --git a/lib/shared/dm-ops/send-text-message-spec.js b/lib/shared/dm-ops/send-text-message-spec.js index cfdaa463d..faeef690b 100644 --- a/lib/shared/dm-ops/send-text-message-spec.js +++ b/lib/shared/dm-ops/send-text-message-spec.js @@ -1,57 +1,71 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { createUpdateUnreadCountUpdate } from './dm-op-utils.js'; import type { DMSendTextMessageOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; const sendTextMessageSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMSendTextMessageOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, time, messageID, text } = dmOperation; const textMessage = { type: messageTypes.TEXT, id: messageID, threadID, creatorID, time, text, }; const updateInfos: Array = []; if (creatorID !== viewerID) { updateInfos.push({ type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID, unread: true, }); } const threadInfo = utilities.threadInfos[threadID]; - if (threadInfo) { - const repliesCountUpdate = createUpdateUnreadCountUpdate(threadInfo, [ - textMessage, - ]); - if (repliesCountUpdate) { - updateInfos.push(repliesCountUpdate); - } + const repliesCountUpdate = createUpdateUnreadCountUpdate(threadInfo, [ + textMessage, + ]); + if (repliesCountUpdate) { + updateInfos.push(repliesCountUpdate); } return { rawMessageInfos: [textMessage], updateInfos, }; }, + canBeProcessed( + dmOperation: DMSendTextMessageOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) { + if (utilities.threadInfos[dmOperation.threadID]) { + return { isProcessingPossible: true }; + } + return { + isProcessingPossible: false, + reason: { + type: 'missing_thread', + threadID: dmOperation.threadID, + }, + }; + }, }); export { sendTextMessageSpec };