diff --git a/lib/shared/dm-ops/add-members-spec.js b/lib/shared/dm-ops/add-members-spec.js index fe37b7822..0fe28ff82 100644 --- a/lib/shared/dm-ops/add-members-spec.js +++ b/lib/shared/dm-ops/add-members-spec.js @@ -1,127 +1,147 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import { createRoleAndPermissionForThickThreads } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMAddMembersOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { type RawMessageInfo } from '../../types/message-types.js'; +import type { AddMembersMessageData } from '../../types/messages/add-members.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 { rawMessageInfoFromMessageData } from '../message-utils.js'; import { roleIsDefaultRole, userIsMember } from '../thread-utils.js'; export type AddMembersResult = { rawMessageInfos: Array, updateInfos: Array, threadInfo: ?ThickRawThreadInfo, }; -function createAddNewMembersResults( +function createAddNewMembersMessageDataFromDMOperation( dmOperation: DMAddMembersOperation, - viewerID: string, - utilities: ProcessDMOperationUtilities, -): AddMembersResult { - const { editorID, time, messageID, addedUserIDs, threadID } = dmOperation; - const addMembersMessage = { +): AddMembersMessageData { + const { editorID, time, addedUserIDs, threadID } = dmOperation; + return { type: messageTypes.ADD_MEMBERS, - id: messageID, threadID, creatorID: editorID, time, addedUserIDs: [...addedUserIDs], }; +} +function createAddNewMembersResults( + dmOperation: DMAddMembersOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, +): AddMembersResult { + const { editorID, time, messageID, addedUserIDs, threadID } = dmOperation; + const messageData = + createAddNewMembersMessageDataFromDMOperation(dmOperation); + const rawMessageInfos = [ + rawMessageInfoFromMessageData(messageData, messageID), + ]; const currentThreadInfo = utilities.threadInfos[threadID]; if (!currentThreadInfo.thick) { return { rawMessageInfos: [], updateInfos: [], threadInfo: null, }; } 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 updateInfos = [ { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: resultThreadInfo, }, ]; return { - rawMessageInfos: [addMembersMessage], + rawMessageInfos, updateInfos, threadInfo: resultThreadInfo, }; } const addMembersSpec: DMOperationSpec = Object.freeze({ + notificationsCreationData: async (dmOperation: DMAddMembersOperation) => { + const messageData = + createAddNewMembersMessageDataFromDMOperation(dmOperation); + return { messageDatas: [messageData] }; + }, 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.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, }); -export { addMembersSpec, createAddNewMembersResults }; +export { + addMembersSpec, + createAddNewMembersResults, + createAddNewMembersMessageDataFromDMOperation, +}; 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 index b5fcc6124..300e25a37 100644 --- a/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js +++ b/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js @@ -1,81 +1,104 @@ // @flow import uuid from 'uuid'; import type { AddMembersResult } from './add-members-spec.js'; import { createThickRawThreadInfo } from './create-thread-spec.js'; import type { DMOperationSpec } from './dm-op-spec.js'; import type { DMAddViewerToThreadMembersOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { messageTruncationStatus } from '../../types/message-types.js'; +import type { AddMembersMessageData } from '../../types/messages/add-members.js'; import { updateTypes } from '../../types/update-types-enum.js'; +import { rawMessageInfoFromMessageData } from '../message-utils.js'; -function createAddViewerToThreadMembersResults( +function createAddViewerToThreadMembersMessageDataFromDMOp( dmOperation: DMAddViewerToThreadMembersOperation, - viewerID: string, -): AddMembersResult { - const { editorID, time, messageID, addedUserIDs, existingThreadDetails } = - dmOperation; - const addMembersMessage = { +): AddMembersMessageData { + const { editorID, time, addedUserIDs, existingThreadDetails } = dmOperation; + return { type: messageTypes.ADD_MEMBERS, - id: messageID, threadID: existingThreadDetails.threadID, creatorID: editorID, time, addedUserIDs: [...addedUserIDs], }; +} + +function createAddViewerToThreadMembersResults( + dmOperation: DMAddViewerToThreadMembersOperation, + viewerID: string, +): AddMembersResult { + const { time, messageID, addedUserIDs, existingThreadDetails } = dmOperation; + const messageData = + createAddViewerToThreadMembersMessageDataFromDMOp(dmOperation); + + const rawMessageInfos = [ + rawMessageInfoFromMessageData(messageData, messageID), + ]; const resultThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, allMemberIDs: [...existingThreadDetails.allMemberIDs, ...addedUserIDs], }, viewerID, ); const updateInfos = [ { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: resultThreadInfo, - rawMessageInfos: [addMembersMessage], + rawMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }, ]; return { rawMessageInfos: [], updateInfos, threadInfo: resultThreadInfo, }; } const addViewerToThreadMembersSpec: DMOperationSpec = Object.freeze({ + notificationsCreationData: async ( + dmOperation: DMAddViewerToThreadMembersOperation, + ) => { + const messageData = + createAddViewerToThreadMembersMessageDataFromDMOp(dmOperation); + return { messageDatas: [messageData] }; + }, 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', }, }; }, supportsAutoRetry: true, }); -export { addViewerToThreadMembersSpec, createAddViewerToThreadMembersResults }; +export { + addViewerToThreadMembersSpec, + createAddViewerToThreadMembersResults, + createAddViewerToThreadMembersMessageDataFromDMOp, +}; diff --git a/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js b/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js index 28c5a1dc9..a667737ac 100644 --- a/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js +++ b/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js @@ -1,78 +1,103 @@ // @flow import { addViewerToThreadMembersSpec, createAddViewerToThreadMembersResults, + createAddViewerToThreadMembersMessageDataFromDMOp, } from './add-viewer-to-thread-members-spec.js'; -import { processChangeSettingsOperation } from './change-thread-settings-spec.js'; +import { + processChangeSettingsOperation, + createChangeSettingsMessageDatasAndUpdate, +} from './change-thread-settings-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMChangeThreadSettingsAndAddViewerOperation } from '../../types/dm-ops.js'; +import type { MessageData } from '../../types/message-types.js'; +import { values } from '../../utils/objects.js'; function createAddViewerAndMembersOperation( dmOperation: DMChangeThreadSettingsAndAddViewerOperation, ) { const { editorID, time, messageIDsPrefix, changes, existingThreadDetails } = dmOperation; const newMemberIDs = changes.newMemberIDs && changes.newMemberIDs.length > 0 ? [...new Set(changes.newMemberIDs)] : []; return { type: 'add_viewer_to_thread_members', editorID, time, messageID: `${messageIDsPrefix}/add_members`, addedUserIDs: newMemberIDs, existingThreadDetails, }; } function processAddViewerToThreadMembersOperation( dmOperation: DMChangeThreadSettingsAndAddViewerOperation, viewerID: string, ) { const operation = createAddViewerAndMembersOperation(dmOperation); if (operation.addedUserIDs.length === 0) { return null; } return createAddViewerToThreadMembersResults(operation, viewerID); } const changeThreadSettingsAndAddViewerSpec: DMOperationSpec = Object.freeze({ + notificationsCreationData: async ( + dmOperation: DMChangeThreadSettingsAndAddViewerOperation, + ) => { + const messageDatas: Array = []; + const addNewMembersOperation = + createAddViewerAndMembersOperation(dmOperation); + if (addNewMembersOperation) { + const addNewMembersMessageData = + createAddViewerToThreadMembersMessageDataFromDMOp( + addNewMembersOperation, + ); + messageDatas.push(addNewMembersMessageData); + } + + const { fieldNameToMessageData } = + createChangeSettingsMessageDatasAndUpdate(dmOperation); + messageDatas.push(...values(fieldNameToMessageData)); + return { messageDatas }; + }, processDMOperation: async ( dmOperation: DMChangeThreadSettingsAndAddViewerOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const addMembersResult = processAddViewerToThreadMembersOperation( dmOperation, viewerID, ); return processChangeSettingsOperation( dmOperation, viewerID, utilities, addMembersResult, ); }, canBeProcessed( dmOperation: DMChangeThreadSettingsAndAddViewerOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { const operation = createAddViewerAndMembersOperation(dmOperation); return addViewerToThreadMembersSpec.canBeProcessed( operation, viewerID, utilities, ); }, supportsAutoRetry: true, }); export { changeThreadSettingsAndAddViewerSpec }; diff --git a/lib/shared/dm-ops/change-thread-settings-spec.js b/lib/shared/dm-ops/change-thread-settings-spec.js index 514a4941b..a1b620113 100644 --- a/lib/shared/dm-ops/change-thread-settings-spec.js +++ b/lib/shared/dm-ops/change-thread-settings-spec.js @@ -1,182 +1,243 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import { type AddMembersResult, createAddNewMembersResults, + createAddNewMembersMessageDataFromDMOperation, } from './add-members-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMChangeThreadSettingsAndAddViewerOperation, DMChangeThreadSettingsOperation, DMOperationResult, + DMThreadSettingsChangesBase, } from '../../types/dm-ops.js'; +import type { MessageData, RawMessageInfo } from '../../types/message-types'; import { messageTypes } from '../../types/message-types-enum.js'; -import type { RawMessageInfo } from '../../types/message-types.js'; +import type { ChangeSettingsMessageData } from '../../types/messages/change-settings.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'; +import { rawMessageInfoFromMessageData } from '../message-utils.js'; -function processAddMembersOperation( +function createAddMembersOperation( dmOperation: DMChangeThreadSettingsOperation, - viewerID: string, - utilities: ProcessDMOperationUtilities, ) { const { editorID, time, messageIDsPrefix, changes, threadID } = dmOperation; const newMemberIDs = changes.newMemberIDs && changes.newMemberIDs.length > 0 ? [...new Set(changes.newMemberIDs)] : []; if (!changes.newMemberIDs || changes.newMemberIDs.length === 0) { return null; } - const operation = { + return { type: 'add_members', editorID, time, messageID: `${messageIDsPrefix}/add_members`, addedUserIDs: newMemberIDs, threadID, }; +} + +function processAddMembersOperation( + dmOperation: DMChangeThreadSettingsOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, +) { + const operation = createAddMembersOperation(dmOperation); + if (!operation) { + return null; + } return createAddNewMembersResults(operation, viewerID, utilities); } -function processChangeSettingsOperation( +function getThreadIDFromChangeThreadSettingsDMOp( dmOperation: | DMChangeThreadSettingsOperation | DMChangeThreadSettingsAndAddViewerOperation, - viewerID: string, - utilities: ProcessDMOperationUtilities, - addMembersResult: ?AddMembersResult, -): DMOperationResult { - const { editorID, time, changes, messageIDsPrefix } = dmOperation; - const { name, description, color, avatar } = changes; - const threadID = - dmOperation.type === 'change_thread_settings' - ? dmOperation.threadID - : dmOperation.existingThreadDetails.threadID; - - let threadInfoToUpdate: ?(RawThreadInfo | LegacyRawThreadInfo) = - utilities.threadInfos[threadID]; - const updateInfos: Array = []; - const rawMessageInfos: Array = []; - - if (addMembersResult) { - if (addMembersResult.threadInfo) { - threadInfoToUpdate = addMembersResult.threadInfo; - } - updateInfos.push(...addMembersResult.updateInfos); - rawMessageInfos.push(...addMembersResult.rawMessageInfos); - } +): string { + return dmOperation.type === 'change_thread_settings' + ? dmOperation.threadID + : dmOperation.existingThreadDetails.threadID; +} - invariant(threadInfoToUpdate?.thick, 'Thread should be thick'); +function createChangeSettingsMessageDatasAndUpdate( + dmOperation: + | DMChangeThreadSettingsOperation + | DMChangeThreadSettingsAndAddViewerOperation, +): { + +fieldNameToMessageData: { +[fieldName: string]: ChangeSettingsMessageData }, + +threadInfoUpdate: DMThreadSettingsChangesBase, +} { + const { changes, editorID, time } = dmOperation; + const { name, description, color, avatar } = changes; + const threadID = getThreadIDFromChangeThreadSettingsDMOp(dmOperation); - const changedFields: { [string]: string | number } = {}; + const threadInfoUpdate: { ...DMThreadSettingsChangesBase } = {}; if (name !== undefined && name !== null) { - changedFields.name = name; - threadInfoToUpdate = { - ...threadInfoToUpdate, - name, - }; + threadInfoUpdate.name = name; } if (description !== undefined && description !== null) { - changedFields.description = description; - threadInfoToUpdate = { - ...threadInfoToUpdate, - description, - }; + threadInfoUpdate.description = description; } if (color) { - changedFields.color = color; - threadInfoToUpdate = { - ...threadInfoToUpdate, - color, - }; + threadInfoUpdate.color = color; } if (avatar) { - changedFields.avatar = JSON.stringify(avatar); - threadInfoToUpdate = { - ...threadInfoToUpdate, - avatar, - }; + threadInfoUpdate.avatar = avatar; } - for (const fieldName in changedFields) { - const newValue = changedFields[fieldName]; - rawMessageInfos.push({ + const fieldNameToMessageData: { + [fieldName: string]: ChangeSettingsMessageData, + } = {}; + + const { avatar: avatarObject, ...rest } = threadInfoUpdate; + const normalizedThreadInfoUpdate = avatarObject + ? { ...rest, avatar: JSON.stringify(avatarObject) } + : { ...rest }; + + for (const fieldName in normalizedThreadInfoUpdate) { + const value = normalizedThreadInfoUpdate[fieldName]; + fieldNameToMessageData[fieldName] = { type: messageTypes.CHANGE_SETTINGS, threadID, creatorID: editorID, time, field: fieldName, - value: newValue, - id: `${messageIDsPrefix}/${fieldName}`, - }); + value: value, + }; } - if (values(changedFields).length > 0) { + return { fieldNameToMessageData, threadInfoUpdate }; +} + +function processChangeSettingsOperation( + dmOperation: + | DMChangeThreadSettingsOperation + | DMChangeThreadSettingsAndAddViewerOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + addMembersResult: ?AddMembersResult, +): DMOperationResult { + const { time, messageIDsPrefix } = dmOperation; + const threadID = getThreadIDFromChangeThreadSettingsDMOp(dmOperation); + + let threadInfoToUpdate: ?RawThreadInfo = utilities.threadInfos[threadID]; + const updateInfos: Array = []; + const rawMessageInfos: Array = []; + + if (addMembersResult) { + if (addMembersResult.threadInfo) { + threadInfoToUpdate = addMembersResult.threadInfo; + } + updateInfos.push(...addMembersResult.updateInfos); + rawMessageInfos.push(...addMembersResult.rawMessageInfos); + } + + invariant(threadInfoToUpdate?.thick, 'Thread should be thick'); + const { fieldNameToMessageData, threadInfoUpdate } = + createChangeSettingsMessageDatasAndUpdate(dmOperation); + + const fieldNameToMessageDataPairs = Object.entries(fieldNameToMessageData); + rawMessageInfos.push( + ...fieldNameToMessageDataPairs.map(([fieldName, messageData]) => + rawMessageInfoFromMessageData( + messageData, + `${messageIDsPrefix}/${fieldName}`, + ), + ), + ); + + threadInfoToUpdate = { + ...threadInfoToUpdate, + ...threadInfoUpdate, + }; + + if (fieldNameToMessageDataPairs.length > 0) { updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: threadInfoToUpdate, }); } return { rawMessageInfos, updateInfos, }; } const changeThreadSettingsSpec: DMOperationSpec = Object.freeze({ + notificationsCreationData: async ( + dmOperation: DMChangeThreadSettingsOperation, + ) => { + const messageDatas: Array = []; + const addNewMembersOperation = createAddMembersOperation(dmOperation); + if (addNewMembersOperation) { + const addNewMembersMessageData = + createAddNewMembersMessageDataFromDMOperation(addNewMembersOperation); + messageDatas.push(addNewMembersMessageData); + } + + const { fieldNameToMessageData } = + createChangeSettingsMessageDatasAndUpdate(dmOperation); + messageDatas.push(...values(fieldNameToMessageData)); + return { messageDatas }; + }, processDMOperation: async ( dmOperation: DMChangeThreadSettingsOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const addMembersResult = processAddMembersOperation( dmOperation, viewerID, utilities, ); return processChangeSettingsOperation( dmOperation, viewerID, utilities, addMembersResult, ); }, canBeProcessed( dmOperation: DMChangeThreadSettingsOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, }); -export { changeThreadSettingsSpec, processChangeSettingsOperation }; +export { + changeThreadSettingsSpec, + processChangeSettingsOperation, + createChangeSettingsMessageDatasAndUpdate, +}; diff --git a/lib/shared/dm-ops/create-sidebar-spec.js b/lib/shared/dm-ops/create-sidebar-spec.js index 26f3e04fc..eb1852d56 100644 --- a/lib/shared/dm-ops/create-sidebar-spec.js +++ b/lib/shared/dm-ops/create-sidebar-spec.js @@ -1,113 +1,168 @@ // @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'; +import { generatePendingThreadColor } from '../color-utils.js'; +import { + isInvalidSidebarSource, + rawMessageInfoFromMessageData, +} from '../message-utils.js'; + +async function createMessageDatasFromDMOperation( + dmOperation: DMCreateSidebarOperation, + utilities: ProcessDMOperationUtilities, + threadColor?: string, +) { + const { + threadID, + creatorID, + time, + parentThreadID, + memberIDs, + sourceMessageID, + } = dmOperation; + + const allMemberIDs = [creatorID, ...memberIDs]; + const color = threadColor ?? generatePendingThreadColor(allMemberIDs); + 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 sidebarSourceMessageData = { + type: messageTypes.SIDEBAR_SOURCE, + threadID, + creatorID, + time, + sourceMessage: sourceMessage, + }; + + const createSidebarMessageData = { + type: messageTypes.CREATE_SIDEBAR, + threadID, + creatorID, + time: time + 1, + sourceMessageAuthorID: sourceMessage.creatorID, + initialThreadState: { + parentThreadID, + color, + memberIDs: allMemberIDs, + }, + }; + + return { + sidebarSourceMessageData, + createSidebarMessageData, + }; +} const createSidebarSpec: DMOperationSpec = Object.freeze({ + notificationsCreationData: async ( + dmOperation: DMCreateSidebarOperation, + utilities: ProcessDMOperationUtilities, + ) => { + const { sidebarSourceMessageData, createSidebarMessageData } = + await createMessageDatasFromDMOperation(dmOperation, utilities); + return { + messageDatas: [sidebarSourceMessageData, createSidebarMessageData], + }; + }, 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, }, 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', + const { sidebarSourceMessageData, createSidebarMessageData } = + await createMessageDatasFromDMOperation( + dmOperation, + utilities, + rawThreadInfo.color, ); - } - if (isInvalidSidebarSource(sourceMessage)) { - throw new Error( - `sourceMessage ${sourceMessageID} is an invalid sidebar source`, - ); - } + + const sidebarSourceMessageInfo = rawMessageInfoFromMessageData( + sidebarSourceMessageData, + newSidebarSourceMessageID, + ); + const createSidebarMessageInfo = rawMessageInfoFromMessageData( + createSidebarMessageData, + newCreateSidebarMessageID, + ); 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, - }, - }, + sidebarSourceMessageInfo, + createSidebarMessageInfo, ]; 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 }; }, supportsAutoRetry: true, }); export { createSidebarSpec }; diff --git a/lib/shared/dm-ops/create-thread-spec.js b/lib/shared/dm-ops/create-thread-spec.js index 9f9c9f648..208607eaa 100644 --- a/lib/shared/dm-ops/create-thread-spec.js +++ b/lib/shared/dm-ops/create-thread-spec.js @@ -1,198 +1,209 @@ // @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 { 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'; +import { rawMessageInfoFromMessageData } from '../message-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, 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, }; if (sourceMessageID) { newThread.sourceMessageID = sourceMessageID; } if (pinnedCount) { newThread.pinnedCount = pinnedCount; } return newThread; } +function createMessageDataFromDMOperation( + dmOperation: DMCreateThreadOperation, +) { + const { threadID, creatorID, time, threadType, memberIDs } = dmOperation; + const allMemberIDs = [creatorID, ...memberIDs]; + const color = generatePendingThreadColor(allMemberIDs); + return { + type: messageTypes.CREATE_THREAD, + threadID, + creatorID, + time, + initialThreadState: { + type: threadType, + color, + memberIDs: allMemberIDs, + }, + }; +} + const createThreadSpec: DMOperationSpec = Object.freeze({ + notificationsCreationData: async (dmOperation: DMCreateThreadOperation) => { + const messageData = createMessageDataFromDMOperation(dmOperation); + return { messageDatas: [messageData] }; + }, 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 messageData = createMessageDataFromDMOperation(dmOperation); + const rawMessageInfos = [ + rawMessageInfoFromMessageData(messageData, newMessageID), ]; 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 }; }, supportsAutoRetry: 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 2b8ae2aca..ab02a5a05 100644 --- a/lib/shared/dm-ops/dm-op-spec.js +++ b/lib/shared/dm-ops/dm-op-spec.js @@ -1,32 +1,37 @@ // @flow import type { DMOperation, DMOperationResult } from '../../types/dm-ops.js'; import type { RawMessageInfo } from '../../types/message-types.js'; +import type { NotificationsCreationData } from '../../types/notif-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 = { + +notificationsCreationData?: ( + dmOp: DMOp, + utilities: ProcessDMOperationUtilities, + ) => Promise, +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 } | { +type: 'invalid' }, }, +supportsAutoRetry: boolean, }; diff --git a/lib/shared/dm-ops/join-thread-spec.js b/lib/shared/dm-ops/join-thread-spec.js index 8d30fe327..440e523f7 100644 --- a/lib/shared/dm-ops/join-thread-spec.js +++ b/lib/shared/dm-ops/join-thread-spec.js @@ -1,129 +1,141 @@ // @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 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 } 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 { rawMessageInfoFromMessageData } from '../message-utils.js'; import { roleIsDefaultRole, userIsMember } from '../thread-utils.js'; +function createMessageDataFromDMOperation(dmOperation: DMJoinThreadOperation) { + const { joinerID, time, existingThreadDetails } = dmOperation; + return { + type: messageTypes.JOIN_THREAD, + threadID: existingThreadDetails.threadID, + creatorID: joinerID, + time, + }; +} + const joinThreadSpec: DMOperationSpec = Object.freeze({ + notificationsCreationData: async (dmOperation: DMJoinThreadOperation) => { + const messageData = createMessageDataFromDMOperation(dmOperation); + return { messageDatas: [messageData] }; + }, processDMOperation: async ( dmOperation: DMJoinThreadOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { joinerID, time, messageID, existingThreadDetails } = dmOperation; const currentThreadInfo = utilities.threadInfos[existingThreadDetails.threadID]; - const joinThreadMessage = { - type: messageTypes.JOIN_THREAD, - id: messageID, - threadID: existingThreadDetails.threadID, - creatorID: joinerID, - time, - }; + const messageData = createMessageDataFromDMOperation(dmOperation); + const joinThreadMessageInfos = [ + rawMessageInfoFromMessageData(messageData, messageID), + ]; if (userIsMember(currentThreadInfo, joinerID)) { return { - rawMessageInfos: [joinThreadMessage], + rawMessageInfos: joinThreadMessageInfos, updateInfos: [], }; } const updateInfos: Array = []; const rawMessageInfos: Array = []; if (viewerID === joinerID) { const newThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, allMemberIDs: [...existingThreadDetails.allMemberIDs, joinerID], }, viewerID, ); updateInfos.push({ type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: newThreadInfo, - rawMessageInfos: [joinThreadMessage], + rawMessageInfos: joinThreadMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }); } else { invariant(currentThreadInfo.thick, 'Thread should be thick'); - rawMessageInfos.push(joinThreadMessage); + rawMessageInfos.push(...joinThreadMessageInfos); 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: joinerID, role: defaultRoleID, permissions: membershipPermissions, isSender: joinerID === viewerID, subscription: joinThreadSubscription, }); const updatedThreadInfo = { ...currentThreadInfo, members: [...currentThreadInfo.members, member], }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }); } 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, }, }; }, supportsAutoRetry: true, }); export { joinThreadSpec }; diff --git a/lib/shared/dm-ops/leave-thread-spec.js b/lib/shared/dm-ops/leave-thread-spec.js index 62928cd01..6c1929636 100644 --- a/lib/shared/dm-ops/leave-thread-spec.js +++ b/lib/shared/dm-ops/leave-thread-spec.js @@ -1,87 +1,99 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMLeaveThreadOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.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 { rawMessageInfoFromMessageData } from '../message-utils.js'; import { userIsMember } from '../thread-utils.js'; +function createMessageDataFromDMOperation(dmOperation: DMLeaveThreadOperation) { + const { editorID, time, threadID } = dmOperation; + return { + type: messageTypes.LEAVE_THREAD, + threadID, + creatorID: editorID, + time, + }; +} + const leaveThreadSpec: DMOperationSpec = Object.freeze({ + notificationsCreationData: async (dmOperation: DMLeaveThreadOperation) => { + const messageData = createMessageDataFromDMOperation(dmOperation); + return { messageDatas: [messageData] }; + }, processDMOperation: async ( dmOperation: DMLeaveThreadOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { editorID, time, messageID, threadID } = dmOperation; 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 messageData = createMessageDataFromDMOperation(dmOperation); + const rawMessageInfos = [ + rawMessageInfoFromMessageData(messageData, messageID), + ]; 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), }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }); } return { - rawMessageInfos: [leaveThreadMessage], + rawMessageInfos, 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, }, }; }, supportsAutoRetry: true, }); export { leaveThreadSpec }; diff --git a/lib/shared/dm-ops/remove-members-spec.js b/lib/shared/dm-ops/remove-members-spec.js index d378d5723..91af83b91 100644 --- a/lib/shared/dm-ops/remove-members-spec.js +++ b/lib/shared/dm-ops/remove-members-spec.js @@ -1,91 +1,106 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMRemoveMembersOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.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 { rawMessageInfoFromMessageData } from '../message-utils.js'; + +function createMessageDataFromDMOperation( + dmOperation: DMRemoveMembersOperation, +) { + const { editorID, time, threadID, removedUserIDs } = dmOperation; + return { + type: messageTypes.REMOVE_MEMBERS, + threadID, + time, + creatorID: editorID, + removedUserIDs: [...removedUserIDs], + }; +} const removeMembersSpec: DMOperationSpec = Object.freeze({ + notificationsCreationData: async ( + dmOperation: DMRemoveMembersOperation, + ) => { + const messageData = createMessageDataFromDMOperation(dmOperation); + return { messageDatas: [messageData] }; + }, processDMOperation: async ( dmOperation: DMRemoveMembersOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { - const { editorID, time, messageID, threadID, removedUserIDs } = - dmOperation; + const { time, messageID, threadID, removedUserIDs } = dmOperation; 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 messageData = createMessageDataFromDMOperation(dmOperation); + const rawMessageInfos = [ + rawMessageInfoFromMessageData(messageData, messageID), + ]; 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), ), }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }); } return { - rawMessageInfos: [removeMembersMessage], + rawMessageInfos, 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, }, }; }, supportsAutoRetry: true, }); 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 244ab55de..b41a24bb8 100644 --- a/lib/shared/dm-ops/send-edit-message-spec.js +++ b/lib/shared/dm-ops/send-edit-message-spec.js @@ -1,49 +1,64 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMSendEditMessageOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; +import { rawMessageInfoFromMessageData } from '../message-utils.js'; + +function createMessageDataFromDMOperation( + dmOperation: DMSendEditMessageOperation, +) { + const { threadID, creatorID, time, targetMessageID, text } = dmOperation; + return { + type: messageTypes.EDIT_MESSAGE, + threadID, + creatorID, + time, + targetMessageID, + text, + }; +} const sendEditMessageSpec: DMOperationSpec = Object.freeze({ + notificationsCreationData: async ( + dmOperation: DMSendEditMessageOperation, + ) => { + const messageData = createMessageDataFromDMOperation(dmOperation); + return { messageDatas: [messageData] }; + }, processDMOperation: async (dmOperation: DMSendEditMessageOperation) => { - const { threadID, creatorID, time, messageID, targetMessageID, text } = - dmOperation; - const editMessage = { - type: messageTypes.EDIT_MESSAGE, - id: messageID, - threadID, - creatorID, - time, - targetMessageID, - text, - }; + const { messageID } = dmOperation; + const messageData = createMessageDataFromDMOperation(dmOperation); + const rawMessageInfos = [ + rawMessageInfoFromMessageData(messageData, messageID), + ]; return { - rawMessageInfos: [editMessage], + rawMessageInfos, 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, }, }; }, supportsAutoRetry: true, }); 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 472fea509..e7920d294 100644 --- a/lib/shared/dm-ops/send-reaction-message-spec.js +++ b/lib/shared/dm-ops/send-reaction-message-spec.js @@ -1,57 +1,66 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMSendReactionMessageOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; +import { rawMessageInfoFromMessageData } from '../message-utils.js'; + +function createMessageDataFromDMOperation( + dmOperation: DMSendReactionMessageOperation, +) { + const { threadID, creatorID, time, targetMessageID, reaction, action } = + dmOperation; + return { + type: messageTypes.REACTION, + threadID, + creatorID, + time, + targetMessageID, + reaction, + action, + }; +} const sendReactionMessageSpec: DMOperationSpec = Object.freeze({ + notificationsCreationData: async ( + dmOperation: DMSendReactionMessageOperation, + ) => { + const messageData = createMessageDataFromDMOperation(dmOperation); + return { messageDatas: [messageData] }; + }, processDMOperation: async (dmOperation: DMSendReactionMessageOperation) => { - const { - threadID, - creatorID, - time, - messageID, - targetMessageID, - reaction, - action, - } = dmOperation; - const reactionMessage = { - type: messageTypes.REACTION, - id: messageID, - threadID, - creatorID, - time, - targetMessageID, - reaction, - action, - }; + const { messageID } = dmOperation; + const messageData = createMessageDataFromDMOperation(dmOperation); + const rawMessageInfos = [ + rawMessageInfoFromMessageData(messageData, messageID), + ]; return { - rawMessageInfos: [reactionMessage], + rawMessageInfos, 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, }, }; }, supportsAutoRetry: true, }); 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 8baeabe89..42851cca4 100644 --- a/lib/shared/dm-ops/send-text-message-spec.js +++ b/lib/shared/dm-ops/send-text-message-spec.js @@ -1,48 +1,64 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMSendTextMessageOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; +import { rawMessageInfoFromMessageData } from '../message-utils.js'; + +function createMessageDataFromDMOperation( + dmOperation: DMSendTextMessageOperation, +) { + const { threadID, creatorID, time, text } = dmOperation; + return { + type: messageTypes.TEXT, + threadID, + creatorID, + time, + text, + }; +} const sendTextMessageSpec: DMOperationSpec = Object.freeze({ + notificationsCreationData: async ( + dmOperation: DMSendTextMessageOperation, + ) => { + const messageData = createMessageDataFromDMOperation(dmOperation); + return { messageDatas: [messageData] }; + }, processDMOperation: async (dmOperation: DMSendTextMessageOperation) => { - const { threadID, creatorID, time, messageID, text } = dmOperation; - const textMessage = { - type: messageTypes.TEXT, - id: messageID, - threadID, - creatorID, - time, - text, - }; + const { messageID } = dmOperation; + const messageData = createMessageDataFromDMOperation(dmOperation); + const rawMessageInfos = [ + rawMessageInfoFromMessageData(messageData, messageID), + ]; const updateInfos: Array = []; return { - rawMessageInfos: [textMessage], + rawMessageInfos, 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, }, }; }, supportsAutoRetry: false, }); export { sendTextMessageSpec }; diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js index 7983e075a..b626651d5 100644 --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -1,400 +1,407 @@ // @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), }); +export type DMThreadSettingsChangesBase = { + +name?: string, + +description?: string, + +color?: string, + +avatar?: ClientAvatar, +}; + +type DMThreadSettingsChanges = $ReadOnly<{ + ...DMThreadSettingsChangesBase, + +newMemberIDs?: $ReadOnlyArray, +}>; + type DMChangeThreadSettingsBase = { +editorID: string, +time: number, - +changes: { - +name?: string, - +description?: string, - +color?: string, - +newMemberIDs?: $ReadOnlyArray, - +avatar?: ClientAvatar, - }, + +changes: DMThreadSettingsChanges, +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, // For messages that could be retried from UI, we need to bind DM `messageID` // with `outboundP2PMessages` to keep track of whether all P2P messages // were queued on Tunnelbroker. +messageIDWithoutAutoRetry: ?string, }; 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 clearQueuedThreadDMOpsActionType = 'CLEAR_QUEUED_THREAD_DM_OPS'; export type ClearQueuedThreadDMOpsPayload = { +threadID: string, }; export type QueuedDMOperations = { +operations: { +[threadID: string]: $ReadOnlyArray<{ +operation: DMOperation, +timestamp: number, }>, }, }; export type SendDMStartedPayload = { +messageID: string, }; export type SendDMOpsSuccessPayload = { +messageID: string, +outboundP2PMessageIDs: $ReadOnlyArray, }; export const sendDMActionTypes = Object.freeze({ started: 'SEND_DM_STARTED', success: 'SEND_DM_SUCCESS', failed: 'SEND_DM_FAILED', });