diff --git a/lib/shared/dm-ops/add-members-spec.js b/lib/shared/dm-ops/add-members-spec.js index c375f048d..86fd604f6 100644 --- a/lib/shared/dm-ops/add-members-spec.js +++ b/lib/shared/dm-ops/add-members-spec.js @@ -1,188 +1,188 @@ // @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, dmAddMembersOperationValidator, } 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 } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { ThickRawThreadInfo } 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 { ThickMemberInfo } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { values } from '../../utils/objects.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; import { roleIsDefaultRole, userIsMember } from '../thread-utils.js'; function createAddNewMembersMessageDataWithInfoFromDMOperation( dmOperation: DMAddMembersOperation, ): { +messageData: AddMembersMessageData, +rawMessageInfo: RawMessageInfo, } { const { editorID, time, addedUserIDs, threadID, messageID } = dmOperation; const messageData = { type: messageTypes.ADD_MEMBERS, threadID, creatorID: editorID, time, addedUserIDs: [...addedUserIDs], }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } function createPermissionsForNewMembers( threadInfo: ThickRawThreadInfo, utilities: ProcessDMOperationUtilities, ): { +membershipPermissions: ThreadPermissionsInfo, +roleID: string, } { const defaultRoleID = values(threadInfo.roles).find(role => roleIsDefaultRole(role), )?.id; invariant(defaultRoleID, 'Default role ID must exist'); const { parentThreadID } = threadInfo; const parentThreadInfo = parentThreadID ? utilities.threadInfos[parentThreadID] : null; if (parentThreadID && !parentThreadInfo) { console.log( `Parent thread with ID ${parentThreadID} was expected while adding ` + 'thread members but is missing from the store', ); } const { membershipPermissions } = createRoleAndPermissionForThickThreads( threadInfo.type, threadInfo.id, defaultRoleID, parentThreadInfo, ); return { membershipPermissions, roleID: defaultRoleID, }; } const addMembersSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async (dmOperation: DMAddMembersOperation) => { - return { - messageDatasWithMessageInfos: [ - createAddNewMembersMessageDataWithInfoFromDMOperation(dmOperation), - ], - }; - }, processDMOperation: async ( dmOperation: DMAddMembersOperation, utilities: ProcessDMOperationUtilities, ) => { const { editorID, time, addedUserIDs, threadID } = dmOperation; const { viewerID, threadInfos } = utilities; - const { rawMessageInfo } = + const messageDataWithMessageInfos = createAddNewMembersMessageDataWithInfoFromDMOperation(dmOperation); + + const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const currentThreadInfo = threadInfos[threadID]; const { membershipPermissions, roleID } = createPermissionsForNewMembers( currentThreadInfo, utilities, ); const memberTimestamps = { ...currentThreadInfo.timestamps.members }; const newMembers = []; for (const userID of addedUserIDs) { if (!memberTimestamps[userID]) { memberTimestamps[userID] = { isMember: time, subscription: currentThreadInfo.creationTime, }; } if (memberTimestamps[userID].isMember > time) { continue; } memberTimestamps[userID] = { ...memberTimestamps[userID], isMember: time, }; if (userIsMember(currentThreadInfo, userID)) { continue; } newMembers.push( minimallyEncodeMemberInfo({ id: userID, role: roleID, permissions: membershipPermissions, isSender: editorID === viewerID, subscription: joinThreadSubscription, }), ); } const resultThreadInfo = { ...currentThreadInfo, members: [...currentThreadInfo.members, ...newMembers], timestamps: { ...currentThreadInfo.timestamps, members: memberTimestamps, }, }; const updateInfos = [ { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: resultThreadInfo, }, ]; + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + return { rawMessageInfos, updateInfos, blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMAddMembersOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, operationValidator: dmAddMembersOperationValidator, }); export { addMembersSpec, createAddNewMembersMessageDataWithInfoFromDMOperation, createPermissionsForNewMembers, }; 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 99323de4c..9d502127e 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,216 +1,212 @@ // @flow import uuid from 'uuid'; import { createPermissionsForNewMembers } from './add-members-spec.js'; import { createThickRawThreadInfo } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMAddViewerToThreadMembersOperation, dmAddViewerToThreadMembersValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { RawMessageInfo } from '../../types/message-types.js'; import { messageTruncationStatus } from '../../types/message-types.js'; import type { AddMembersMessageData } from '../../types/messages/add-members.js'; import { minimallyEncodeMemberInfo, minimallyEncodeThreadCurrentUserInfo, } 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 { rawMessageInfoFromMessageData } from '../message-utils.js'; import { userIsMember } from '../thread-utils.js'; function createAddViewerToThreadMembersMessageDataWithInfoFromDMOp( dmOperation: DMAddViewerToThreadMembersOperation, ): { +messageData: AddMembersMessageData, +rawMessageInfo: RawMessageInfo, } { const { editorID, time, addedUserIDs, existingThreadDetails, messageID } = dmOperation; const messageData = { type: messageTypes.ADD_MEMBERS, threadID: existingThreadDetails.threadID, creatorID: editorID, time, addedUserIDs: [...addedUserIDs], }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const addViewerToThreadMembersSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async ( - dmOperation: DMAddViewerToThreadMembersOperation, - ) => { - return { - messageDatasWithMessageInfos: [ - createAddViewerToThreadMembersMessageDataWithInfoFromDMOp( - dmOperation, - ), - ], - }; - }, processDMOperation: async ( dmOperation: DMAddViewerToThreadMembersOperation, utilities: ProcessDMOperationUtilities, ) => { const { time, messageID, addedUserIDs, existingThreadDetails, editorID } = dmOperation; const { threadInfos } = utilities; - const { rawMessageInfo } = + const messageDataWithMessageInfos = createAddViewerToThreadMembersMessageDataWithInfoFromDMOp(dmOperation); + const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = messageID ? [rawMessageInfo] : []; const threadID = existingThreadDetails.threadID; const currentThreadInfo = threadInfos[threadID]; const memberTimestamps = { ...currentThreadInfo?.timestamps?.members, }; const newMembers = []; for (const userID of addedUserIDs) { if (!memberTimestamps[userID]) { memberTimestamps[userID] = { isMember: time, subscription: existingThreadDetails.creationTime, }; } if (memberTimestamps[userID].isMember > time) { continue; } memberTimestamps[userID] = { ...memberTimestamps[userID], isMember: time, }; if (!userIsMember(currentThreadInfo, userID)) { newMembers.push(userID); } } + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + if (currentThreadInfo) { const { membershipPermissions, roleID } = createPermissionsForNewMembers(currentThreadInfo, utilities); const newMemberInfos = newMembers.map(userID => minimallyEncodeMemberInfo({ id: userID, role: roleID, permissions: membershipPermissions, isSender: editorID === utilities.viewerID, subscription: joinThreadSubscription, }), ); const resultThreadInfo = { ...currentThreadInfo, members: [...currentThreadInfo.members, ...newMemberInfos], currentUser: minimallyEncodeThreadCurrentUserInfo({ role: roleID, permissions: membershipPermissions, subscription: joinThreadSubscription, unread: true, }), timestamps: { ...currentThreadInfo.timestamps, members: { ...currentThreadInfo.timestamps.members, ...memberTimestamps, }, }, }; const updateInfos = [ { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: resultThreadInfo, }, ]; return { rawMessageInfos, updateInfos, blobOps: [], + notificationsCreationData, }; } const resultThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, allMemberIDsWithSubscriptions: [ ...existingThreadDetails.allMemberIDsWithSubscriptions, ...newMembers.map(id => ({ id, subscription: joinThreadSubscription, })), ], timestamps: { ...existingThreadDetails.timestamps, members: { ...existingThreadDetails.timestamps.members, ...memberTimestamps, }, }, }, utilities, ); const updateInfos = [ { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: resultThreadInfo, rawMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }, ]; return { rawMessageInfos: [], updateInfos, blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMAddViewerToThreadMembersOperation, utilities: ProcessDMOperationUtilities, ) => { const { viewerID } = utilities; // We expect the viewer to be in the added users when the DM op // is processed. An exception is for ops generated // by InitialStateSharingHandler, which won't contain a messageID if ( dmOperation.addedUserIDs.includes(viewerID) || !dmOperation.messageID ) { return { isProcessingPossible: true }; } console.log('Invalid DM operation', dmOperation); return { isProcessingPossible: false, reason: { type: 'invalid', }, }; }, supportsAutoRetry: true, operationValidator: dmAddViewerToThreadMembersValidator, }); export { addViewerToThreadMembersSpec, createAddViewerToThreadMembersMessageDataWithInfoFromDMOp, }; diff --git a/lib/shared/dm-ops/change-thread-read-status-spec.js b/lib/shared/dm-ops/change-thread-read-status-spec.js index 221e70e16..9a5715cc1 100644 --- a/lib/shared/dm-ops/change-thread-read-status-spec.js +++ b/lib/shared/dm-ops/change-thread-read-status-spec.js @@ -1,79 +1,79 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMChangeThreadReadStatusOperation, dmChangeThreadReadStatusOperationValidator, } from '../../types/dm-ops.js'; import { updateTypes } from '../../types/update-types-enum.js'; const changeThreadReadStatusSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async ( - dmOperation: DMChangeThreadReadStatusOperation, - ) => { - const { threadID, unread } = dmOperation; - if (unread) { - return { badgeUpdateData: { threadID } }; - } - return { rescindData: { threadID } }; - }, processDMOperation: async ( dmOperation: DMChangeThreadReadStatusOperation, utilities: ProcessDMOperationUtilities, ) => { const { threadID, unread, time } = dmOperation; + let notificationsCreationData; + if (unread) { + notificationsCreationData = { badgeUpdateData: { threadID } }; + } else { + notificationsCreationData = { rescindData: { threadID } }; + } + const threadInfo = utilities.threadInfos[threadID]; if (threadInfo.timestamps.currentUser.unread > time) { return { rawMessageInfos: [], updateInfos: [], blobOps: [], + notificationsCreationData, }; } const updateInfos = [ { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID: threadInfo.id, unread, }, ]; return { rawMessageInfos: [], updateInfos, blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMChangeThreadReadStatusOperation, utilities: ProcessDMOperationUtilities, ) => { const { creatorID, threadID } = dmOperation; const { threadInfos, viewerID } = utilities; if (viewerID !== creatorID) { return { isProcessingPossible: false, reason: { type: 'invalid' } }; } if (!threadInfos[threadID]) { return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: true, operationValidator: dmChangeThreadReadStatusOperationValidator, }); export { changeThreadReadStatusSpec }; diff --git a/lib/shared/dm-ops/change-thread-settings-spec.js b/lib/shared/dm-ops/change-thread-settings-spec.js index 6d2fe7e03..2613f9a61 100644 --- a/lib/shared/dm-ops/change-thread-settings-spec.js +++ b/lib/shared/dm-ops/change-thread-settings-spec.js @@ -1,215 +1,212 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMBlobOperation, type DMChangeThreadSettingsOperation, type DMThreadSettingsChanges, dmChangeThreadSettingsOperationValidator, } from '../../types/dm-ops.js'; import type { RawMessageInfo } from '../../types/message-types'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ChangeSettingsMessageData } from '../../types/messages/change-settings.js'; import type { RawThreadInfo, ThickRawThreadInfo, } from '../../types/minimally-encoded-thread-permissions-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { blobHashFromBlobServiceURI } from '../../utils/blob-service.js'; import { values } from '../../utils/objects.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function getThreadIDFromChangeThreadSettingsDMOp( dmOperation: DMChangeThreadSettingsOperation, ): string { return dmOperation.type === 'change_thread_settings' ? dmOperation.threadID : dmOperation.existingThreadDetails.threadID; } function createChangeSettingsMessageDatasAndUpdate( dmOperation: DMChangeThreadSettingsOperation, ): { +fieldNameToMessageData: { +[fieldName: string]: { +messageData: ChangeSettingsMessageData, +rawMessageInfo: RawMessageInfo, }, }, +threadInfoUpdate: DMThreadSettingsChanges, } { const { changes, editorID, time, messageIDsPrefix } = dmOperation; const { name, description, color, avatar } = changes; const threadID = getThreadIDFromChangeThreadSettingsDMOp(dmOperation); const threadInfoUpdate: { ...DMThreadSettingsChanges } = {}; if (name !== undefined && name !== null) { threadInfoUpdate.name = name; } if (description !== undefined && description !== null) { threadInfoUpdate.description = description; } if (color) { threadInfoUpdate.color = color; } if (avatar || avatar === null) { threadInfoUpdate.avatar = avatar; } const fieldNameToMessageData: { [fieldName: string]: { +messageData: ChangeSettingsMessageData, +rawMessageInfo: RawMessageInfo, }, } = {}; const { avatar: avatarObject, ...rest } = threadInfoUpdate; let normalizedThreadInfoUpdate; if (avatarObject) { normalizedThreadInfoUpdate = { ...rest, avatar: JSON.stringify(avatarObject), }; } else if (avatarObject === null) { // clear thread avatar normalizedThreadInfoUpdate = { ...rest, avatar: '' }; } else { normalizedThreadInfoUpdate = { ...rest }; } for (const fieldName in normalizedThreadInfoUpdate) { const value = normalizedThreadInfoUpdate[fieldName]; const messageData: ChangeSettingsMessageData = { type: messageTypes.CHANGE_SETTINGS, threadID, creatorID: editorID, time, field: fieldName, value: value, }; const rawMessageInfo = rawMessageInfoFromMessageData( messageData, `${messageIDsPrefix}/${fieldName}`, ); fieldNameToMessageData[fieldName] = { messageData, rawMessageInfo }; } return { fieldNameToMessageData, threadInfoUpdate }; } function getBlobOpsFromOperation( dmOperation: DMChangeThreadSettingsOperation, threadInfo: ?RawThreadInfo, ): Array { const ops: Array = []; const prevAvatar = threadInfo?.avatar; if (prevAvatar && prevAvatar.type === 'encrypted_image') { ops.push({ type: 'remove_holder', blobHash: blobHashFromBlobServiceURI(prevAvatar.blobURI), dmOpType: 'inbound_and_outbound', }); } const { avatar } = dmOperation.changes; if (avatar && avatar?.type === 'encrypted_image') { ops.push({ type: 'establish_holder', blobHash: blobHashFromBlobServiceURI(avatar.blobURI), dmOpType: 'inbound_only', }); } return ops; } const changeThreadSettingsSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async ( - dmOperation: DMChangeThreadSettingsOperation, - ) => { - const { fieldNameToMessageData } = - createChangeSettingsMessageDatasAndUpdate(dmOperation); - - return { messageDatasWithMessageInfos: values(fieldNameToMessageData) }; - }, processDMOperation: async ( dmOperation: DMChangeThreadSettingsOperation, utilities: ProcessDMOperationUtilities, ) => { const { time } = dmOperation; const threadID = getThreadIDFromChangeThreadSettingsDMOp(dmOperation); const threadInfo = utilities.threadInfos[threadID]; const updateInfos: Array = []; const { fieldNameToMessageData, threadInfoUpdate } = createChangeSettingsMessageDatasAndUpdate(dmOperation); const blobOps = getBlobOpsFromOperation(dmOperation, threadInfo); const messageDataWithMessageInfoPairs = values(fieldNameToMessageData); const rawMessageInfos = messageDataWithMessageInfoPairs.map( ({ rawMessageInfo }) => rawMessageInfo, ); let threadInfoToUpdate: ThickRawThreadInfo = threadInfo; for (const fieldName in threadInfoUpdate) { const timestamp = threadInfoToUpdate.timestamps[fieldName]; if (timestamp < time) { threadInfoToUpdate = { ...threadInfoToUpdate, [fieldName]: threadInfoUpdate[fieldName], timestamps: { ...threadInfoToUpdate.timestamps, [fieldName]: time, }, }; } } if (messageDataWithMessageInfoPairs.length > 0) { updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: threadInfoToUpdate, }); } + const notificationsCreationData = { + messageDatasWithMessageInfos: values(fieldNameToMessageData), + }; + return { rawMessageInfos, updateInfos, blobOps, + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMChangeThreadSettingsOperation, utilities: ProcessDMOperationUtilities, ) => { if (!utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: true, operationValidator: dmChangeThreadSettingsOperationValidator, }); export { changeThreadSettingsSpec, createChangeSettingsMessageDatasAndUpdate }; diff --git a/lib/shared/dm-ops/change-thread-subscription.js b/lib/shared/dm-ops/change-thread-subscription.js index 98aced38a..7a2ef5d8b 100644 --- a/lib/shared/dm-ops/change-thread-subscription.js +++ b/lib/shared/dm-ops/change-thread-subscription.js @@ -1,116 +1,118 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import type { ProcessDMOperationUtilities, DMOperationSpec, } from './dm-op-spec.js'; import { type DMChangeThreadSubscriptionOperation, dmChangeThreadSubscriptionOperationValidator, } from '../../types/dm-ops.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; const changeThreadSubscriptionSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMChangeThreadSubscriptionOperation, utilities: ProcessDMOperationUtilities, ) => { const { creatorID, threadID, subscription, time } = dmOperation; const { viewerID, threadInfos } = utilities; const threadInfo = threadInfos[threadID]; if (threadInfo.timestamps.members[creatorID].subscription > time) { return { updateInfos: [], rawMessageInfos: [], blobOps: [], + notificationsCreationData: null, }; } const creatorMemberInfo = threadInfo.members.find( member => member.id === creatorID, ); invariant(creatorMemberInfo, 'operation creator missing in thread'); const updatedCreatorMemberInfo = { ...creatorMemberInfo, subscription, }; const otherMemberInfos = threadInfo.members.filter( member => member.id !== creatorID, ); const membersUpdate = [...otherMemberInfos, updatedCreatorMemberInfo]; const currentUserUpdate = viewerID === creatorID ? { ...threadInfo.currentUser, subscription, } : threadInfo.currentUser; const threadInfoUpdate = { ...threadInfo, members: membersUpdate, currentUser: currentUserUpdate, timestamps: { ...threadInfo.timestamps, members: { ...threadInfo.timestamps.members, [creatorID]: { ...threadInfo.timestamps.members[creatorID], subscription: time, }, }, }, }; const updateInfos: Array = [ { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: threadInfoUpdate, }, ]; return { updateInfos, rawMessageInfos: [], blobOps: [], + notificationsCreationData: null, }; }, canBeProcessed: async ( dmOperation: DMChangeThreadSubscriptionOperation, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID } = dmOperation; if (!utilities.threadInfos[threadID]) { return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID }, }; } if ( !utilities.threadInfos[threadID].members.find( memberInfo => memberInfo.id === creatorID, ) ) { return { isProcessingPossible: false, reason: { type: 'missing_membership', threadID, userID: creatorID }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: true, operationValidator: dmChangeThreadSubscriptionOperationValidator, }); export { changeThreadSubscriptionSpec }; diff --git a/lib/shared/dm-ops/create-entry-spec.js b/lib/shared/dm-ops/create-entry-spec.js index 70c6371c5..240a370a7 100644 --- a/lib/shared/dm-ops/create-entry-spec.js +++ b/lib/shared/dm-ops/create-entry-spec.js @@ -1,112 +1,111 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMCreateEntryOperation, dmCreateEntryOperationValidator, } from '../../types/dm-ops.js'; import type { ThickRawEntryInfo } from '../../types/entry-types.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { EntryUpdateInfo } from '../../types/update-types.js'; import { dateFromString } from '../../utils/date-utils.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMCreateEntryOperation, ) { const { threadID, creatorID, time, entryID, entryDate, text, messageID } = dmOperation; const messageData = { type: messageTypes.CREATE_ENTRY, threadID, creatorID, time, entryID, date: entryDate, text, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { rawMessageInfo, messageData }; } const createEntrySpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async (dmOperation: DMCreateEntryOperation) => { - return { - messageDatasWithMessageInfos: [ - createMessageDataWithInfoFromDMOperation(dmOperation), - ], - }; - }, processDMOperation: async (dmOperation: DMCreateEntryOperation) => { const { threadID, creatorID, time, entryID, entryDate, text } = dmOperation; - const { rawMessageInfo } = + const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); + const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const date = dateFromString(entryDate); const rawEntryInfo: ThickRawEntryInfo = { id: entryID, threadID, text, year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), creationTime: time, creatorID, thick: true, deleted: false, lastUpdatedTime: time, }; const entryUpdateInfo: EntryUpdateInfo = { entryInfo: rawEntryInfo, type: updateTypes.UPDATE_ENTRY, id: uuid.v4(), time, }; + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + return { rawMessageInfos, updateInfos: [entryUpdateInfo], blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMCreateEntryOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.entryInfos[dmOperation.entryID]) { console.log( 'Discarded a CREATE_ENTRY operation because entry with ' + `the same ID ${dmOperation.entryID} already exists in the store`, ); return { isProcessingPossible: false, reason: { type: 'invalid', }, }; } if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, operationValidator: dmCreateEntryOperationValidator, }); export { createEntrySpec }; diff --git a/lib/shared/dm-ops/create-sidebar-spec.js b/lib/shared/dm-ops/create-sidebar-spec.js index 1451a98f1..abb3df18c 100644 --- a/lib/shared/dm-ops/create-sidebar-spec.js +++ b/lib/shared/dm-ops/create-sidebar-spec.js @@ -1,227 +1,219 @@ // @flow import uuid from 'uuid'; import { createThickRawThreadInfo } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMCreateSidebarOperation, dmCreateSidebarOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { type RawMessageInfo, messageTruncationStatus, } from '../../types/message-types.js'; import { joinThreadSubscription } from '../../types/subscription-types.js'; import { threadTypes } from '../../types/thread-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { generatePendingThreadColor } from '../color-utils.js'; import { isInvalidSidebarSource, rawMessageInfoFromMessageData, } from '../message-utils.js'; import { createThreadTimestamps } from '../thread-utils.js'; async function createMessageDatasWithInfosFromDMOperation( dmOperation: DMCreateSidebarOperation, utilities: ProcessDMOperationUtilities, threadColor?: string, ) { const { threadID, creatorID, time, parentThreadID, memberIDs, sourceMessageID, newSidebarSourceMessageID, newCreateSidebarMessageID, } = 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, }, }; const sidebarSourceMessageInfo = rawMessageInfoFromMessageData( sidebarSourceMessageData, newSidebarSourceMessageID, ); const createSidebarMessageInfo = rawMessageInfoFromMessageData( createSidebarMessageData, newCreateSidebarMessageID, ); return { sidebarSourceMessageData, createSidebarMessageData, sidebarSourceMessageInfo, createSidebarMessageInfo, }; } const createSidebarSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async ( - dmOperation: DMCreateSidebarOperation, - utilities: ProcessDMOperationUtilities, - ) => { - const { - sidebarSourceMessageData, - createSidebarMessageData, - createSidebarMessageInfo, - sidebarSourceMessageInfo, - } = await createMessageDatasWithInfosFromDMOperation( - dmOperation, - utilities, - ); - return { - messageDatasWithMessageInfos: [ - { - messageData: sidebarSourceMessageData, - rawMessageInfo: sidebarSourceMessageInfo, - }, - { - messageData: createSidebarMessageData, - rawMessageInfo: createSidebarMessageInfo, - }, - ], - }; - }, processDMOperation: async ( dmOperation: DMCreateSidebarOperation, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, time, parentThreadID, memberIDs, sourceMessageID, roleID, } = dmOperation; const { viewerID } = utilities; const allMemberIDs = [creatorID, ...memberIDs]; const allMemberIDsWithSubscriptions = allMemberIDs.map(id => ({ id, subscription: joinThreadSubscription, })); const rawThreadInfo = createThickRawThreadInfo( { threadID, threadType: threadTypes.THICK_SIDEBAR, creationTime: time, parentThreadID, allMemberIDsWithSubscriptions, roleID, unread: creatorID !== viewerID, sourceMessageID, containingThreadID: parentThreadID, timestamps: createThreadTimestamps(time, allMemberIDs), }, utilities, ); - const { sidebarSourceMessageInfo, createSidebarMessageInfo } = - await createMessageDatasWithInfosFromDMOperation( - dmOperation, - utilities, - rawThreadInfo.color, - ); + const { + sidebarSourceMessageData, + createSidebarMessageData, + createSidebarMessageInfo, + sidebarSourceMessageInfo, + } = await createMessageDatasWithInfosFromDMOperation( + dmOperation, + utilities, + rawThreadInfo.color, + ); const rawMessageInfos: Array = [ sidebarSourceMessageInfo, createSidebarMessageInfo, ]; const threadJoinUpdateInfo = { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: rawThreadInfo, rawMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }; + const notificationsCreationData = { + messageDatasWithMessageInfos: [ + { + messageData: sidebarSourceMessageData, + rawMessageInfo: sidebarSourceMessageInfo, + }, + { + messageData: createSidebarMessageData, + rawMessageInfo: createSidebarMessageInfo, + }, + ], + }; + return { rawMessageInfos: [], // included in updateInfos below updateInfos: [threadJoinUpdateInfo], blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMCreateSidebarOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { console.log( 'Discarded a CREATE_SIDEBAR operation because thread ' + `with the same ID ${dmOperation.threadID} already exists ` + 'in the store', ); return { isProcessingPossible: false, reason: { type: 'invalid', }, }; } const sourceMessage = await utilities.fetchMessage( dmOperation.sourceMessageID, ); if (!sourceMessage) { return { isProcessingPossible: false, reason: { type: 'missing_message', messageID: dmOperation.sourceMessageID, }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: true, operationValidator: dmCreateSidebarOperationValidator, }); export { createSidebarSpec }; diff --git a/lib/shared/dm-ops/create-thread-spec.js b/lib/shared/dm-ops/create-thread-spec.js index 1d86d1e94..5c86259d1 100644 --- a/lib/shared/dm-ops/create-thread-spec.js +++ b/lib/shared/dm-ops/create-thread-spec.js @@ -1,316 +1,315 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { specialRoles } from '../../permissions/special-roles.js'; import { getAllThreadPermissions, makePermissionsBlob, getThickThreadRolePermissionsBlob, makePermissionsForChildrenBlob, } from '../../permissions/thread-permissions.js'; import { type CreateThickRawThreadInfoInput, type DMCreateThreadOperation, dmCreateThreadOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.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'; import { createThreadTimestamps } from '../thread-utils.js'; function createPermissionsInfo( threadID: string, threadType: ThickThreadType, isMember: boolean, parentThreadInfo: ?ThickRawThreadInfo, ): ThreadPermissionsInfo { let rolePermissions = null; if (isMember) { rolePermissions = getThickThreadRolePermissionsBlob(threadType); } let permissionsFromParent = null; if (parentThreadInfo) { const parentThreadRolePermissions = getThickThreadRolePermissionsBlob( parentThreadInfo.type, ); const parentPermissionsBlob = makePermissionsBlob( parentThreadRolePermissions, null, parentThreadInfo.id, parentThreadInfo.type, ); permissionsFromParent = makePermissionsForChildrenBlob( parentPermissionsBlob, ); } return getAllThreadPermissions( makePermissionsBlob( rolePermissions, permissionsFromParent, threadID, threadType, ), threadID, ); } function createRoleAndPermissionForThickThreads( threadType: ThickThreadType, threadID: string, roleID: string, parentThreadInfo: ?ThickRawThreadInfo, ): { +role: RoleInfo, +membershipPermissions: ThreadPermissionsInfo } { const rolePermissions = getThickThreadRolePermissionsBlob(threadType); const membershipPermissions = createPermissionsInfo( threadID, threadType, true, parentThreadInfo, ); 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, utilities: ProcessDMOperationUtilities, ): MutableThickRawThreadInfo { const { threadID, threadType, creationTime, parentThreadID, allMemberIDsWithSubscriptions, roleID, unread, name, avatar, description, color, containingThreadID, sourceMessageID, repliesCount, pinnedCount, timestamps, } = input; const memberIDs = allMemberIDsWithSubscriptions.map(({ id }) => id); const threadColor = color ?? generatePendingThreadColor(memberIDs); const parentThreadInfo = parentThreadID ? utilities.threadInfos[parentThreadID] : null; if (parentThreadID && !parentThreadInfo) { console.log( `Parent thread with ID ${parentThreadID} was expected while creating ` + 'thick thread but is missing from the store', ); } const { membershipPermissions, role } = createRoleAndPermissionForThickThreads( threadType, threadID, roleID, parentThreadInfo, ); const viewerIsMember = allMemberIDsWithSubscriptions.some( member => member.id === utilities.viewerID, ); const viewerRoleID = viewerIsMember ? role.id : null; const viewerMembershipPermissions = createPermissionsInfo( threadID, threadType, viewerIsMember, parentThreadInfo, ); const newThread: MutableThickRawThreadInfo = { thick: true, minimallyEncoded: true, id: threadID, type: threadType, color: threadColor, creationTime, parentThreadID, members: allMemberIDsWithSubscriptions.map( ({ id: memberID, subscription }) => minimallyEncodeMemberInfo({ id: memberID, role: memberID === utilities.viewerID ? viewerRoleID : role.id, permissions: memberID === utilities.viewerID ? viewerMembershipPermissions : membershipPermissions, isSender: memberID === utilities.viewerID, subscription, }), ), roles: { [role.id]: role, }, currentUser: minimallyEncodeThreadCurrentUserInfo({ role: viewerRoleID, permissions: viewerMembershipPermissions, subscription: joinThreadSubscription, unread, }), repliesCount: repliesCount ?? 0, name, avatar, description: description ?? '', containingThreadID, timestamps, }; if (sourceMessageID) { newThread.sourceMessageID = sourceMessageID; } if (pinnedCount) { newThread.pinnedCount = pinnedCount; } return newThread; } function createMessageDataWithInfoFromDMOperation( dmOperation: DMCreateThreadOperation, ) { const { threadID, creatorID, time, threadType, memberIDs, newMessageID } = dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; const color = generatePendingThreadColor(allMemberIDs); const messageData = { type: messageTypes.CREATE_THREAD, threadID, creatorID, time, initialThreadState: { type: threadType, color, memberIDs: allMemberIDs, }, }; const rawMessageInfo = rawMessageInfoFromMessageData( messageData, newMessageID, ); return { messageData, rawMessageInfo }; } const createThreadSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async (dmOperation: DMCreateThreadOperation) => { - return { - messageDatasWithMessageInfos: [ - createMessageDataWithInfoFromDMOperation(dmOperation), - ], - }; - }, processDMOperation: async ( dmOperation: DMCreateThreadOperation, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, time, threadType, memberIDs, roleID } = dmOperation; const { viewerID } = utilities; const allMemberIDs = [creatorID, ...memberIDs]; const allMemberIDsWithSubscriptions = allMemberIDs.map(id => ({ id, subscription: joinThreadSubscription, })); const rawThreadInfo = createThickRawThreadInfo( { threadID, threadType, creationTime: time, allMemberIDsWithSubscriptions, roleID, unread: creatorID !== viewerID, timestamps: createThreadTimestamps(time, allMemberIDs), }, utilities, ); - const { rawMessageInfo } = + const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); + const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const threadJoinUpdateInfo = { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: rawThreadInfo, rawMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }; + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + return { rawMessageInfos: [], // included in updateInfos below updateInfos: [threadJoinUpdateInfo], blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMCreateThreadOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { console.log( 'Discarded a CREATE_THREAD operation because thread ' + `with the same ID ${dmOperation.threadID} already exists ` + 'in the store', ); return { isProcessingPossible: false, reason: { type: 'invalid', }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: true, operationValidator: dmCreateThreadOperationValidator, }); export { createThickRawThreadInfo, createThreadSpec, createRoleAndPermissionForThickThreads, createPermissionsInfo, }; diff --git a/lib/shared/dm-ops/delete-entry-spec.js b/lib/shared/dm-ops/delete-entry-spec.js index e70a01ac9..f84fd4df6 100644 --- a/lib/shared/dm-ops/delete-entry-spec.js +++ b/lib/shared/dm-ops/delete-entry-spec.js @@ -1,125 +1,125 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMDeleteEntryOperation, dmDeleteEntryOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { EntryUpdateInfo } from '../../types/update-types.js'; import { dateFromString } from '../../utils/date-utils.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMDeleteEntryOperation, ) { const { threadID, creatorID, time, entryID, entryDate, prevText, messageID } = dmOperation; const messageData = { type: messageTypes.DELETE_ENTRY, threadID, creatorID, time, entryID, date: entryDate, text: prevText, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { rawMessageInfo, messageData }; } const deleteEntrySpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async (dmOperation: DMDeleteEntryOperation) => { - return { - messageDatasWithMessageInfos: [ - createMessageDataWithInfoFromDMOperation(dmOperation), - ], - }; - }, processDMOperation: async ( dmOperation: DMDeleteEntryOperation, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, time, creationTime, entryID, entryDate: dateString, prevText, } = dmOperation; const rawEntryInfo = utilities.entryInfos[entryID]; - const { rawMessageInfo } = + const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); + const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; invariant(rawEntryInfo?.thick, 'Entry thread should be thick'); const timestamp = rawEntryInfo.lastUpdatedTime; + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + if (timestamp > time) { return { rawMessageInfos, updateInfos: [], blobOps: [], + notificationsCreationData, }; } const date = dateFromString(dateString); const rawEntryInfoToUpdate = { id: entryID, threadID, text: prevText, year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), creationTime, creatorID, thick: true, deleted: true, lastUpdatedTime: time, }; const entryUpdateInfo: EntryUpdateInfo = { entryInfo: rawEntryInfoToUpdate, type: updateTypes.UPDATE_ENTRY, id: uuid.v4(), time, }; return { rawMessageInfos, updateInfos: [entryUpdateInfo], blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMDeleteEntryOperation, utilities: ProcessDMOperationUtilities, ) => { if (!utilities.entryInfos[dmOperation.entryID]) { return { isProcessingPossible: false, reason: { type: 'missing_entry', entryID: dmOperation.entryID, }, }; } return { isProcessingPossible: true, }; }, supportsAutoRetry: true, operationValidator: dmDeleteEntryOperationValidator, }); export { deleteEntrySpec }; diff --git a/lib/shared/dm-ops/dm-op-spec.js b/lib/shared/dm-ops/dm-op-spec.js index af6c83233..dd87a8639 100644 --- a/lib/shared/dm-ops/dm-op-spec.js +++ b/lib/shared/dm-ops/dm-op-spec.js @@ -1,50 +1,44 @@ // @flow import type { TInterface } from 'tcomb'; import type { DMOperation, DMOperationResult } from '../../types/dm-ops.js'; import type { RawEntryInfos } from '../../types/entry-types.js'; import type { UserIdentitiesResponse } from '../../types/identity-service-types.js'; import type { RawMessageInfo } from '../../types/message-types.js'; -import type { NotificationsCreationData } from '../../types/notif-types.js'; import type { ThickRawThreadInfos } from '../../types/thread-types.js'; export type ProcessDMOperationUtilities = { +viewerID: string, - // Needed to fetch sidebar source messages +fetchMessage: (messageID: string) => Promise, +threadInfos: ThickRawThreadInfos, +entryInfos: RawEntryInfos, +findUserIdentities: ( userIDs: $ReadOnlyArray, ) => Promise, }; export type ProcessingPossibilityCheckResult = | { +isProcessingPossible: true } | { +isProcessingPossible: false, +reason: | { +type: 'missing_thread', +threadID: string } | { +type: 'missing_entry', +entryID: string } | { +type: 'missing_message', +messageID: string } | { +type: 'missing_membership', +threadID: string, +userID: string } | { +type: 'invalid' }, }; export type DMOperationSpec = { - +notificationsCreationData?: ( - dmOp: DMOp, - utilities: ProcessDMOperationUtilities, - ) => Promise, +processDMOperation: ( dmOp: DMOp, utilities: ProcessDMOperationUtilities, ) => Promise, +canBeProcessed: ( dmOp: DMOp, utilities: ProcessDMOperationUtilities, ) => Promise, +supportsAutoRetry: boolean, +operationValidator: TInterface, }; diff --git a/lib/shared/dm-ops/edit-entry-spec.js b/lib/shared/dm-ops/edit-entry-spec.js index 357dd97df..3258f9737 100644 --- a/lib/shared/dm-ops/edit-entry-spec.js +++ b/lib/shared/dm-ops/edit-entry-spec.js @@ -1,126 +1,126 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMEditEntryOperation, dmEditEntryOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { EntryUpdateInfo } from '../../types/update-types.js'; import { dateFromString } from '../../utils/date-utils.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMEditEntryOperation, ) { const { threadID, creatorID, time, entryID, entryDate, text, messageID } = dmOperation; const messageData = { type: messageTypes.EDIT_ENTRY, threadID, creatorID, entryID, time, date: entryDate, text, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const editEntrySpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async (dmOperation: DMEditEntryOperation) => { - return { - messageDatasWithMessageInfos: [ - createMessageDataWithInfoFromDMOperation(dmOperation), - ], - }; - }, processDMOperation: async ( dmOperation: DMEditEntryOperation, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, creationTime, time, entryID, entryDate: dateString, text, } = dmOperation; const rawEntryInfo = utilities.entryInfos[entryID]; - const { rawMessageInfo } = + const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); + const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; invariant(rawEntryInfo?.thick, 'Entry should be thick'); const timestamp = rawEntryInfo.lastUpdatedTime; + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + if (timestamp > time) { return { rawMessageInfos, updateInfos: [], blobOps: [], + notificationsCreationData, }; } const date = dateFromString(dateString); const rawEntryInfoToUpdate = { id: entryID, threadID, text, year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), creationTime, creatorID, thick: true, deleted: false, lastUpdatedTime: time, }; const entryUpdateInfo: EntryUpdateInfo = { entryInfo: rawEntryInfoToUpdate, type: updateTypes.UPDATE_ENTRY, id: uuid.v4(), time, }; return { rawMessageInfos, updateInfos: [entryUpdateInfo], blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMEditEntryOperation, utilities: ProcessDMOperationUtilities, ) => { if (!utilities.entryInfos[dmOperation.entryID]) { return { isProcessingPossible: false, reason: { type: 'missing_entry', entryID: dmOperation.entryID, }, }; } return { isProcessingPossible: true, }; }, supportsAutoRetry: true, operationValidator: dmEditEntryOperationValidator, }); export { editEntrySpec }; diff --git a/lib/shared/dm-ops/join-thread-spec.js b/lib/shared/dm-ops/join-thread-spec.js index 839f4c4d9..7fb94085e 100644 --- a/lib/shared/dm-ops/join-thread-spec.js +++ b/lib/shared/dm-ops/join-thread-spec.js @@ -1,204 +1,204 @@ // @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, dmJoinThreadOperationValidator, } 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 createMessageDataWithInfoFromDMOperation( dmOperation: DMJoinThreadOperation, ) { const { joinerID, time, existingThreadDetails, messageID } = dmOperation; const messageData = { type: messageTypes.JOIN_THREAD, threadID: existingThreadDetails.threadID, creatorID: joinerID, time, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const joinThreadSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async (dmOperation: DMJoinThreadOperation) => { - return { - messageDatasWithMessageInfos: [ - createMessageDataWithInfoFromDMOperation(dmOperation), - ], - }; - }, processDMOperation: async ( dmOperation: DMJoinThreadOperation, utilities: ProcessDMOperationUtilities, ) => { const { joinerID, time, existingThreadDetails } = dmOperation; const { viewerID, threadInfos } = utilities; const currentThreadInfo = threadInfos[existingThreadDetails.threadID]; - const { rawMessageInfo } = + const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); + const { rawMessageInfo } = messageDataWithMessageInfos; const joinThreadMessageInfos = [rawMessageInfo]; const memberTimestamps = { ...currentThreadInfo?.timestamps?.members }; if (!memberTimestamps[joinerID]) { memberTimestamps[joinerID] = { isMember: time, subscription: existingThreadDetails.creationTime, }; } + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + if (memberTimestamps[joinerID].isMember > time) { return { rawMessageInfos: joinThreadMessageInfos, updateInfos: [], blobOps: [], + notificationsCreationData, }; } memberTimestamps[joinerID] = { ...memberTimestamps[joinerID], isMember: time, }; const updateInfos: Array = []; const rawMessageInfos: Array = []; if (userIsMember(currentThreadInfo, joinerID)) { rawMessageInfos.push(...joinThreadMessageInfos); updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: { ...currentThreadInfo, timestamps: { ...currentThreadInfo.timestamps, members: memberTimestamps, }, }, }); } else if (viewerID === joinerID) { const newThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, allMemberIDsWithSubscriptions: [ ...existingThreadDetails.allMemberIDsWithSubscriptions, { id: joinerID, subscription: joinThreadSubscription }, ], timestamps: { ...existingThreadDetails.timestamps, members: memberTimestamps, }, }, utilities, ); updateInfos.push({ type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: newThreadInfo, rawMessageInfos: joinThreadMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }); } else { rawMessageInfos.push(...joinThreadMessageInfos); const defaultRoleID = values(currentThreadInfo.roles).find(role => roleIsDefaultRole(role), )?.id; invariant(defaultRoleID, 'Default role ID must exist'); const parentThreadID = existingThreadDetails.parentThreadID; const parentThreadInfo = parentThreadID ? utilities.threadInfos[parentThreadID] : null; if (parentThreadID && !parentThreadInfo) { console.log( `Parent thread with ID ${parentThreadID} was expected while joining ` + 'thick thread but is missing from the store', ); } const { membershipPermissions } = createRoleAndPermissionForThickThreads( currentThreadInfo.type, currentThreadInfo.id, defaultRoleID, parentThreadInfo, ); const member = minimallyEncodeMemberInfo({ id: joinerID, role: defaultRoleID, permissions: membershipPermissions, isSender: joinerID === viewerID, subscription: joinThreadSubscription, }); const updatedThreadInfo = { ...currentThreadInfo, members: [...currentThreadInfo.members, member], timestamps: { ...currentThreadInfo.timestamps, members: memberTimestamps, }, }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }); } return { rawMessageInfos, updateInfos, blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMJoinThreadOperation, utilities: ProcessDMOperationUtilities, ) => { const { viewerID, threadInfos } = utilities; if ( threadInfos[dmOperation.existingThreadDetails.threadID] || dmOperation.joinerID === viewerID ) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.existingThreadDetails.threadID, }, }; }, supportsAutoRetry: true, operationValidator: dmJoinThreadOperationValidator, }); export { joinThreadSpec }; diff --git a/lib/shared/dm-ops/leave-thread-spec.js b/lib/shared/dm-ops/leave-thread-spec.js index 0059e69f4..cc1148b6b 100644 --- a/lib/shared/dm-ops/leave-thread-spec.js +++ b/lib/shared/dm-ops/leave-thread-spec.js @@ -1,276 +1,279 @@ // @flow import uuid from 'uuid'; import { createPermissionsInfo } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMLeaveThreadOperation, dmLeaveThreadOperationValidator, } 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 { minimallyEncodeThreadCurrentUserInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import { threadTypes } from '../../types/thread-types-enum.js'; import type { ThickRawThreadInfos } 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 createMessageDataWithInfoFromDMOperation( dmOperation: DMLeaveThreadOperation, ) { const { editorID, time, threadID, messageID } = dmOperation; const messageData = { type: messageTypes.LEAVE_THREAD, threadID, creatorID: editorID, time, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } function createDeleteSubthreadsUpdates( dmOperation: DMLeaveThreadOperation, threadInfo: ThickRawThreadInfo, threadInfos: ThickRawThreadInfos, ): Array { const updates: Array = []; for (const thread of values(threadInfos)) { if (thread.parentThreadID !== threadInfo.id) { continue; } updates.push({ type: updateTypes.DELETE_THREAD, id: uuid.v4(), time: dmOperation.time, threadID: thread.id, }); } return updates; } function createLeaveSubthreadsUpdates( dmOperation: DMLeaveThreadOperation, threadInfo: ThickRawThreadInfo, threadInfos: ThickRawThreadInfos, ): Array { const updates: Array = []; for (const thread of values(threadInfos)) { if (thread.parentThreadID !== threadInfo.id) { continue; } const userID = dmOperation.editorID; let userTimestamps = thread.timestamps.members[userID]; if (!userTimestamps) { userTimestamps = { isMember: thread.creationTime, subscription: thread.creationTime, }; } if (userTimestamps.isMember > dmOperation.time) { continue; } const updatedThread = { ...thread, members: thread.members.filter(member => member.id !== userID), timestamps: { ...thread.timestamps, members: { ...thread.timestamps.members, [userID]: { ...userTimestamps, isMember: dmOperation.time, }, }, }, }; updates.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time: dmOperation.time, threadInfo: updatedThread, }); } return updates; } const leaveThreadSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async (dmOperation: DMLeaveThreadOperation) => { - return { - messageDatasWithMessageInfos: [ - createMessageDataWithInfoFromDMOperation(dmOperation), - ], - }; - }, processDMOperation: async ( dmOperation: DMLeaveThreadOperation, utilities: ProcessDMOperationUtilities, ) => { const { editorID, time, threadID } = dmOperation; const { viewerID, threadInfos } = utilities; const threadInfo = threadInfos[threadID]; - const { rawMessageInfo } = + const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); + const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const memberTimestamps = { ...threadInfo.timestamps.members }; if (!memberTimestamps[editorID]) { memberTimestamps[editorID] = { isMember: time, subscription: threadInfo.creationTime, }; } memberTimestamps[editorID] = { ...memberTimestamps[editorID], isMember: time, }; + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + if (viewerID === editorID) { if (threadInfo.timestamps.members[editorID]?.isMember > time) { return { rawMessageInfos, updateInfos: [], blobOps: [], + notificationsCreationData, }; } if (threadInfo.type !== threadTypes.THICK_SIDEBAR) { return { rawMessageInfos, updateInfos: [ { type: updateTypes.DELETE_THREAD, id: uuid.v4(), time, threadID, }, ...createDeleteSubthreadsUpdates( dmOperation, threadInfo, threadInfos, ), ], blobOps: [], + notificationsCreationData, }; } const parentThreadID = threadInfo.parentThreadID; const parentThreadInfo = parentThreadID ? utilities.threadInfos[parentThreadID] : null; if (parentThreadID && !parentThreadInfo) { console.log( `Parent thread with ID ${parentThreadID} was expected while ` + 'leaving a thread but is missing from the store', ); } const viewerMembershipPermissions = createPermissionsInfo( threadID, threadInfo.type, false, parentThreadInfo, ); const { minimallyEncoded, permissions, ...currentUserInfo } = threadInfo.currentUser; const currentUser = minimallyEncodeThreadCurrentUserInfo({ ...currentUserInfo, role: null, permissions: viewerMembershipPermissions, }); const updatedThreadInfo = { ...threadInfo, members: threadInfo.members.filter(member => member.id !== editorID), currentUser, timestamps: { ...threadInfo.timestamps, members: memberTimestamps, }, }; return { rawMessageInfos, updateInfos: [ { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }, ], blobOps: [], + notificationsCreationData, }; } const updateInfos = createLeaveSubthreadsUpdates( dmOperation, threadInfo, threadInfos, ); // It is possible that the editor has joined this thread after leaving it, // but regardless, we should possibly leave the sidebars. We need to do // that because it isn't guaranteed that the editor rejoined them. if (threadInfo.timestamps.members[editorID]?.isMember > time) { return { rawMessageInfos, updateInfos, blobOps: [], + notificationsCreationData, }; } const updatedThreadInfo = { ...threadInfo, members: threadInfo.members.filter(member => member.id !== editorID), timestamps: { ...threadInfo.timestamps, members: memberTimestamps, }, }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }); return { rawMessageInfos, updateInfos, blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMLeaveThreadOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, operationValidator: dmLeaveThreadOperationValidator, }); export { leaveThreadSpec }; diff --git a/lib/shared/dm-ops/process-dm-ops.js b/lib/shared/dm-ops/process-dm-ops.js index 3184b90f3..5c8527f4a 100644 --- a/lib/shared/dm-ops/process-dm-ops.js +++ b/lib/shared/dm-ops/process-dm-ops.js @@ -1,407 +1,384 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import type { ProcessDMOperationUtilities } from './dm-op-spec.js'; import { dmOpSpecs } from './dm-op-specs.js'; import { type OutboundDMOperationSpecification, type DMOperationSpecification, useCreateMessagesToPeersFromDMOp, dmOperationSpecificationTypes, type OutboundComposableDMOperationSpecification, useSendDMOperationUtils, } from './dm-op-utils.js'; import { useProcessBlobHolders } from '../../actions/holder-actions.js'; import { processNewUserIDsActionType } from '../../actions/user-actions.js'; import { useDispatchWithMetadata } from '../../hooks/ops-hooks.js'; import { usePeerToPeerCommunication, type ProcessOutboundP2PMessagesResult, } from '../../tunnelbroker/peer-to-peer-context.js'; import { useConfirmPeerToPeerMessage } from '../../tunnelbroker/use-confirm-peer-to-peer-message.js'; import { processDMOpsActionType, queueDMOpsActionType, dmOperationValidator, } from '../../types/dm-ops.js'; -import type { NotificationsCreationData } from '../../types/notif-types.js'; import type { DispatchMetadata } from '../../types/redux-types.js'; import type { OutboundP2PMessage } from '../../types/sqlite-types.js'; import { extractUserIDsFromPayload } from '../../utils/conversion-utils.js'; import { useSelector, useDispatch } from '../../utils/redux-utils.js'; import { useStaffAlert } from '../staff-utils.js'; function useProcessDMOperation(): ( dmOperationSpecification: DMOperationSpecification, dmOpID: ?string, ) => Promise { const baseUtilities = useSendDMOperationUtils(); const dispatchWithMetadata = useDispatchWithMetadata(); const processBlobHolders = useProcessBlobHolders(); const createMessagesToPeersFromDMOp = useCreateMessagesToPeersFromDMOp(); const confirmPeerToPeerMessage = useConfirmPeerToPeerMessage(); const dispatch = useDispatch(); const { showAlertToStaff } = useStaffAlert(); return React.useCallback( async ( dmOperationSpecification: DMOperationSpecification, dmOpID: ?string, ) => { const { viewerID, ...restUtilities } = baseUtilities; if (!viewerID) { showAlertToStaff( 'Ignored DMOperation because logged out', JSON.stringify(dmOperationSpecification.op), ); return; } const utilities: ProcessDMOperationUtilities = { ...restUtilities, viewerID, }; const { op: dmOp } = dmOperationSpecification; let outboundP2PMessages: ?$ReadOnlyArray = null; if ( dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND ) { outboundP2PMessages = await createMessagesToPeersFromDMOp( dmOp, dmOperationSpecification.recipients, ); } let dispatchMetadata: ?DispatchMetadata = null; if ( dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND && dmOpID ) { dispatchMetadata = { dmOpID, }; } else if ( dmOperationSpecification.type === dmOperationSpecificationTypes.INBOUND ) { dispatchMetadata = dmOperationSpecification.metadata; } let composableMessageID: ?string = null; if ( dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND && !dmOpSpecs[dmOp.type].supportsAutoRetry ) { composableMessageID = dmOp.messageID; } if ( dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND && dmOperationSpecification.sendOnly ) { - const notificationsCreationData = await dmOpSpecs[ + const { notificationsCreationData } = await dmOpSpecs[ dmOp.type - ].notificationsCreationData?.(dmOp, utilities); + ].processDMOperation(dmOp, utilities); dispatchWithMetadata( { type: processDMOpsActionType, payload: { rawMessageInfos: [], updateInfos: [], outboundP2PMessages, composableMessageID, notificationsCreationData, }, }, dispatchMetadata, ); return; } if (!dmOpSpecs[dmOp.type].operationValidator.is(dmOp)) { showAlertToStaff( "Ignoring operation because it doesn't pass validation", JSON.stringify(dmOp), ); await confirmPeerToPeerMessage(dispatchMetadata); return; } const processingCheckResult = await dmOpSpecs[dmOp.type].canBeProcessed( dmOp, utilities, ); if (!processingCheckResult.isProcessingPossible) { if (processingCheckResult.reason.type === 'invalid') { showAlertToStaff( 'Ignoring operation because it is invalid', JSON.stringify(dmOp), ); await confirmPeerToPeerMessage(dispatchMetadata); return; } let condition; if (processingCheckResult.reason.type === 'missing_thread') { condition = { type: 'thread', threadID: processingCheckResult.reason.threadID, }; } else if (processingCheckResult.reason.type === 'missing_entry') { condition = { type: 'entry', entryID: processingCheckResult.reason.entryID, }; } else if (processingCheckResult.reason.type === 'missing_message') { condition = { type: 'message', messageID: processingCheckResult.reason.messageID, }; } else if (processingCheckResult.reason.type === 'missing_membership') { condition = { type: 'membership', threadID: processingCheckResult.reason.threadID, userID: processingCheckResult.reason.userID, }; } if (condition?.type) { showAlertToStaff( `Adding operation to the ${condition.type} queue`, JSON.stringify(dmOp), ); } else { showAlertToStaff( 'Operation should be added to a queue but its type is missing', JSON.stringify(dmOp), ); } dispatchWithMetadata( { type: queueDMOpsActionType, payload: { operation: dmOp, timestamp: Date.now(), condition, }, }, dispatchMetadata, ); await confirmPeerToPeerMessage(dispatchMetadata); return; } const newUserIDs = extractUserIDsFromPayload(dmOperationValidator, dmOp); if (newUserIDs.length > 0) { dispatch({ type: processNewUserIDsActionType, payload: { userIDs: newUserIDs }, }); } - const dmOpSpec = dmOpSpecs[dmOp.type]; - const notificationsCreationDataPromise: Promise = - (async () => { - if ( - dmOperationSpecification.type === - dmOperationSpecificationTypes.INBOUND || - !dmOpSpec.notificationsCreationData - ) { - return null; - } - return await dmOpSpec.notificationsCreationData(dmOp, utilities); - })(); - - const [ - { rawMessageInfos, updateInfos, blobOps }, + const { + rawMessageInfos, + updateInfos, + blobOps, notificationsCreationData, - ] = await Promise.all([ - dmOpSpec.processDMOperation(dmOp, utilities), - notificationsCreationDataPromise, - ]); + } = await dmOpSpecs[dmOp.type].processDMOperation(dmOp, utilities); + + const outboundNotificationsCreationData = + dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND + ? notificationsCreationData + : null; const holderOps = blobOps .map(({ dmOpType, ...holderOp }) => { if ( (dmOpType === 'inbound_only' && dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND) || (dmOpType === 'outbound_only' && dmOperationSpecification.type === dmOperationSpecificationTypes.INBOUND) ) { return null; } return holderOp; }) .filter(Boolean); void processBlobHolders(holderOps); dispatchWithMetadata( { type: processDMOpsActionType, payload: { rawMessageInfos, updateInfos, outboundP2PMessages, composableMessageID, - notificationsCreationData, + notificationsCreationData: outboundNotificationsCreationData, }, }, dispatchMetadata, ); }, [ baseUtilities, processBlobHolders, dispatchWithMetadata, showAlertToStaff, createMessagesToPeersFromDMOp, confirmPeerToPeerMessage, dispatch, ], ); } function useProcessAndSendDMOperation(): ( dmOperationSpecification: OutboundDMOperationSpecification, ) => Promise { const processDMOps = useProcessDMOperation(); const { getDMOpsSendingPromise } = usePeerToPeerCommunication(); return React.useCallback( async (dmOperationSpecification: OutboundDMOperationSpecification) => { const { promise, dmOpID } = getDMOpsSendingPromise(); await processDMOps(dmOperationSpecification, dmOpID); await promise; }, [getDMOpsSendingPromise, processDMOps], ); } function useSendComposableDMOperation(): ( dmOperationSpecification: OutboundComposableDMOperationSpecification, ) => Promise { const { getDMOpsSendingPromise } = usePeerToPeerCommunication(); const dispatchWithMetadata = useDispatchWithMetadata(); const baseUtilities = useSendDMOperationUtils(); const { processOutboundMessages } = usePeerToPeerCommunication(); const localMessageInfos = useSelector(state => state.messageStore.local); const createMessagesToPeersFromDMOp = useCreateMessagesToPeersFromDMOp(); return React.useCallback( async ( dmOperationSpecification: OutboundComposableDMOperationSpecification, ): Promise => { const { viewerID, ...restUtilities } = baseUtilities; if (!viewerID) { console.log('ignored DMOperation because logged out'); return { result: 'failure', failedMessageIDs: [], }; } const utilities: ProcessDMOperationUtilities = { ...restUtilities, viewerID, }; const { promise, dmOpID } = getDMOpsSendingPromise(); const { op, composableMessageID, recipients } = dmOperationSpecification; const localMessageInfo = localMessageInfos[composableMessageID]; if ( localMessageInfo?.outboundP2PMessageIDs && localMessageInfo.outboundP2PMessageIDs.length > 0 ) { processOutboundMessages(localMessageInfo.outboundP2PMessageIDs, dmOpID); try { // This code should never throw. return await promise; } catch (e) { invariant( localMessageInfo.outboundP2PMessageIDs, 'outboundP2PMessageIDs should be defined', ); return { result: 'failure', failedMessageIDs: localMessageInfo.outboundP2PMessageIDs, }; } } const outboundP2PMessages = await createMessagesToPeersFromDMOp( op, recipients, ); - const spec = dmOpSpecs[op.type]; - - const notificationsCreationDataPromise: Promise = - (async () => { - if (!spec?.notificationsCreationData) { - return null; - } - return await spec.notificationsCreationData(op, utilities); - })(); - - const [{ rawMessageInfos, updateInfos }, notificationsCreationData] = - await Promise.all([ - dmOpSpecs[op.type].processDMOperation(op, utilities), - notificationsCreationDataPromise, - ]); + const { rawMessageInfos, updateInfos, notificationsCreationData } = + await dmOpSpecs[op.type].processDMOperation(op, utilities); dispatchWithMetadata( { type: processDMOpsActionType, payload: { rawMessageInfos, updateInfos, outboundP2PMessages, composableMessageID, notificationsCreationData, }, }, { dmOpID, }, ); try { // This code should never throw. return await promise; } catch (e) { return { result: 'failure', failedMessageIDs: outboundP2PMessages.map( message => message.messageID, ), }; } }, [ baseUtilities, getDMOpsSendingPromise, localMessageInfos, createMessagesToPeersFromDMOp, dispatchWithMetadata, processOutboundMessages, ], ); } export { useProcessDMOperation, useProcessAndSendDMOperation, useSendComposableDMOperation, }; diff --git a/lib/shared/dm-ops/remove-members-spec.js b/lib/shared/dm-ops/remove-members-spec.js index 719fc157c..064c2f357 100644 --- a/lib/shared/dm-ops/remove-members-spec.js +++ b/lib/shared/dm-ops/remove-members-spec.js @@ -1,136 +1,134 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMRemoveMembersOperation, dmRemoveMembersOperationValidator, } 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 createMessageDataWithInfoFromDMOperation( dmOperation: DMRemoveMembersOperation, ) { const { editorID, time, threadID, removedUserIDs, messageID } = dmOperation; const messageData = { type: messageTypes.REMOVE_MEMBERS, threadID, time, creatorID: editorID, removedUserIDs: [...removedUserIDs], }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const removeMembersSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async ( - dmOperation: DMRemoveMembersOperation, - ) => { - return { - messageDatasWithMessageInfos: [ - createMessageDataWithInfoFromDMOperation(dmOperation), - ], - }; - }, processDMOperation: async ( dmOperation: DMRemoveMembersOperation, utilities: ProcessDMOperationUtilities, ) => { const { time, threadID, removedUserIDs } = dmOperation; const { viewerID, threadInfos } = utilities; const threadInfo = threadInfos[threadID]; - const { rawMessageInfo } = + const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); + const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const memberTimestamps = { ...threadInfo.timestamps.members }; const removedUserIDsSet = new Set(); for (const userID of removedUserIDs) { if (!memberTimestamps[userID]) { memberTimestamps[userID] = { isMember: time, subscription: threadInfo.creationTime, }; } if (memberTimestamps[userID].isMember > time) { continue; } memberTimestamps[userID] = { ...memberTimestamps[userID], isMember: time, }; removedUserIDsSet.add(userID); } const viewerIsRemoved = removedUserIDsSet.has(viewerID); const updateInfos: Array = []; if ( viewerIsRemoved && (threadInfo.type !== threadTypes.THICK_SIDEBAR || (threadInfo.parentThreadID && !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), ), timestamps: { ...threadInfo.timestamps, members: memberTimestamps, }, }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }); } + + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + return { rawMessageInfos, updateInfos, blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMRemoveMembersOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, operationValidator: dmRemoveMembersOperationValidator, }); 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 3ef32f54e..f2defaf91 100644 --- a/lib/shared/dm-ops/send-edit-message-spec.js +++ b/lib/shared/dm-ops/send-edit-message-spec.js @@ -1,77 +1,74 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMSendEditMessageOperation, dmSendEditMessageOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMSendEditMessageOperation, ) { const { threadID, creatorID, time, targetMessageID, text, messageID } = dmOperation; const messageData = { type: messageTypes.EDIT_MESSAGE, threadID, creatorID, time, targetMessageID, text, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const sendEditMessageSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async ( - dmOperation: DMSendEditMessageOperation, - ) => { - return { - messageDatasWithMessageInfos: [ - createMessageDataWithInfoFromDMOperation(dmOperation), - ], - }; - }, processDMOperation: async (dmOperation: DMSendEditMessageOperation) => { - const { rawMessageInfo } = + const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); + const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + return { rawMessageInfos, updateInfos: [], blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMSendEditMessageOperation, utilities: ProcessDMOperationUtilities, ) => { const targetMessage = await utilities.fetchMessage( dmOperation.targetMessageID, ); if (!targetMessage) { return { isProcessingPossible: false, reason: { type: 'missing_message', messageID: dmOperation.targetMessageID, }, }; } return { isProcessingPossible: true, }; }, supportsAutoRetry: true, operationValidator: dmSendEditMessageOperationValidator, }); export { sendEditMessageSpec }; diff --git a/lib/shared/dm-ops/send-multimedia-message-spec.js b/lib/shared/dm-ops/send-multimedia-message-spec.js index e94249699..401b6c563 100644 --- a/lib/shared/dm-ops/send-multimedia-message-spec.js +++ b/lib/shared/dm-ops/send-multimedia-message-spec.js @@ -1,108 +1,106 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { encryptedMediaBlobURI, encryptedVideoThumbnailBlobURI, } from '../../media/media-utils.js'; import { type DMSendMultimediaMessageOperation, type DMBlobOperation, dmSendMultimediaMessageOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { blobHashFromBlobServiceURI } from '../../utils/blob-service.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMSendMultimediaMessageOperation, ) { const { threadID, creatorID, time, media, messageID } = dmOperation; const messageData = { type: messageTypes.MULTIMEDIA, threadID, creatorID, time, media, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } function getBlobOpsFromOperation( dmOperation: DMSendMultimediaMessageOperation, ): Array { const ops: Array = []; for (const media of dmOperation.media) { if (media.type !== 'encrypted_photo' && media.type !== 'encrypted_video') { continue; } const blobURI = encryptedMediaBlobURI(media); ops.push({ type: 'establish_holder', blobHash: blobHashFromBlobServiceURI(blobURI), dmOpType: 'inbound_only', }); if (media.type === 'encrypted_video') { const thumbnailBlobURI = encryptedVideoThumbnailBlobURI(media); ops.push({ type: 'establish_holder', blobHash: blobHashFromBlobServiceURI(thumbnailBlobURI), dmOpType: 'inbound_only', }); } } return ops; } const sendMultimediaMessageSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async ( - dmOperation: DMSendMultimediaMessageOperation, - ) => { - return { - messageDatasWithMessageInfos: [ - createMessageDataWithInfoFromDMOperation(dmOperation), - ], - }; - }, processDMOperation: async ( dmOperation: DMSendMultimediaMessageOperation, ) => { - const { rawMessageInfo } = + const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); + const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const updateInfos: Array = []; const blobOps = getBlobOpsFromOperation(dmOperation); + + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + return { rawMessageInfos, updateInfos, blobOps, + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMSendMultimediaMessageOperation, utilities: ProcessDMOperationUtilities, ) => { if (!utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: false, operationValidator: dmSendMultimediaMessageOperationValidator, }); export { sendMultimediaMessageSpec }; diff --git a/lib/shared/dm-ops/send-reaction-message-spec.js b/lib/shared/dm-ops/send-reaction-message-spec.js index 91674aaab..6c2d9133e 100644 --- a/lib/shared/dm-ops/send-reaction-message-spec.js +++ b/lib/shared/dm-ops/send-reaction-message-spec.js @@ -1,85 +1,82 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMSendReactionMessageOperation, dmSendReactionMessageOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMSendReactionMessageOperation, ) { const { threadID, creatorID, time, targetMessageID, reaction, action, messageID, } = dmOperation; const messageData = { type: messageTypes.REACTION, threadID, creatorID, time, targetMessageID, reaction, action, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const sendReactionMessageSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async ( - dmOperation: DMSendReactionMessageOperation, - ) => { - return { - messageDatasWithMessageInfos: [ - createMessageDataWithInfoFromDMOperation(dmOperation), - ], - }; - }, processDMOperation: async (dmOperation: DMSendReactionMessageOperation) => { - const { rawMessageInfo } = + const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); + const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + return { rawMessageInfos, updateInfos: [], blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMSendReactionMessageOperation, utilities: ProcessDMOperationUtilities, ) => { const targetMessage = await utilities.fetchMessage( dmOperation.targetMessageID, ); if (!targetMessage) { return { isProcessingPossible: false, reason: { type: 'missing_message', messageID: dmOperation.targetMessageID, }, }; } return { isProcessingPossible: true, }; }, supportsAutoRetry: true, operationValidator: dmSendReactionMessageOperationValidator, }); 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 2b187b1f2..b032af67a 100644 --- a/lib/shared/dm-ops/send-text-message-spec.js +++ b/lib/shared/dm-ops/send-text-message-spec.js @@ -1,71 +1,69 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMSendTextMessageOperation, dmSendTextMessageOperationValidator, } 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 createMessageDataWithInfoFromDMOperation( dmOperation: DMSendTextMessageOperation, ) { const { threadID, creatorID, time, text, messageID } = dmOperation; const messageData = { type: messageTypes.TEXT, threadID, creatorID, time, text, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const sendTextMessageSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async ( - dmOperation: DMSendTextMessageOperation, - ) => { - return { - messageDatasWithMessageInfos: [ - createMessageDataWithInfoFromDMOperation(dmOperation), - ], - }; - }, processDMOperation: async (dmOperation: DMSendTextMessageOperation) => { - const { rawMessageInfo } = + const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); + const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const updateInfos: Array = []; + + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + return { rawMessageInfos, updateInfos, blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMSendTextMessageOperation, utilities: ProcessDMOperationUtilities, ) => { if (!utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: false, operationValidator: dmSendTextMessageOperationValidator, }); export { sendTextMessageSpec }; diff --git a/lib/shared/dm-ops/update-relationship-spec.js b/lib/shared/dm-ops/update-relationship-spec.js index 97839e9f6..19a544fd3 100644 --- a/lib/shared/dm-ops/update-relationship-spec.js +++ b/lib/shared/dm-ops/update-relationship-spec.js @@ -1,114 +1,105 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMUpdateRelationshipOperation, dmUpdateRelationshipOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; async function createMessageDataWithInfoFromDMOperation( dmOperation: DMUpdateRelationshipOperation, utilities: ProcessDMOperationUtilities, ) { const { threadID, creatorID, time, operation, messageID, targetUserID } = dmOperation; const { findUserIdentities } = utilities; if (operation !== 'farcaster_mutual') { const messageData = { type: messageTypes.UPDATE_RELATIONSHIP, threadID, creatorID, targetID: targetUserID, time, operation, }; const rawMessageInfo = rawMessageInfoFromMessageData( messageData, messageID, ); return { rawMessageInfo, messageData }; } const { identities: userIdentities } = await findUserIdentities([ creatorID, targetUserID, ]); const creatorFID = userIdentities[creatorID]?.farcasterID; const targetFID = userIdentities[targetUserID]?.farcasterID; if (!creatorFID || !targetFID) { const errorMap = { [creatorID]: creatorFID, [targetUserID]: targetFID }; throw new Error( 'could not fetch FID for either creator or target: ' + JSON.stringify(errorMap), ); } const messageData = { type: messageTypes.UPDATE_RELATIONSHIP, threadID, creatorID, creatorFID, targetID: targetUserID, targetFID, time, operation, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { rawMessageInfo, messageData }; } const updateRelationshipSpec: DMOperationSpec = Object.freeze({ - notificationsCreationData: async ( - dmOperation: DMUpdateRelationshipOperation, - utilities: ProcessDMOperationUtilities, - ) => { - const messageDataWithMessageInfo = - await createMessageDataWithInfoFromDMOperation( - dmOperation, - - utilities, - ); - return { - messageDatasWithMessageInfos: [messageDataWithMessageInfo], - }; - }, processDMOperation: async ( dmOperation: DMUpdateRelationshipOperation, utilities: ProcessDMOperationUtilities, ) => { - const { rawMessageInfo } = await createMessageDataWithInfoFromDMOperation( - dmOperation, - utilities, - ); + const messageDataWithMessageInfos = + await createMessageDataWithInfoFromDMOperation(dmOperation, utilities); + const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; + + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + }; + return { rawMessageInfos, updateInfos: [], blobOps: [], + notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMUpdateRelationshipOperation, utilities: ProcessDMOperationUtilities, ) => { try { await createMessageDataWithInfoFromDMOperation(dmOperation, utilities); return { isProcessingPossible: true, }; } catch (e) { return { isProcessingPossible: false, reason: { type: 'invalid' }, }; } }, supportsAutoRetry: true, operationValidator: dmUpdateRelationshipOperationValidator, }); export { updateRelationshipSpec }; diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js index 6ab7b232e..7182a125a 100644 --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -1,610 +1,611 @@ // @flow import t, { type TInterface, type TUnion } from 'tcomb'; import { clientAvatarValidator, type ClientAvatar } from './avatar-types.js'; import type { BlobOperation } from './holder-types.js'; import { type Media, mediaValidator } from './media-types.js'; import type { RawMessageInfo } from './message-types.js'; import type { RelationshipOperation } from './messages/update-relationship.js'; import type { NotificationsCreationData } from './notif-types.js'; import type { OutboundP2PMessage } from './sqlite-types.js'; import { type ThreadSubscription, threadSubscriptionValidator, } from './subscription-types.js'; import { type NonSidebarThickThreadType, nonSidebarThickThreadTypes, type ThickThreadType, thickThreadTypeValidator, } from './thread-types-enum.js'; import { threadTimestampsValidator, type ThreadTimestamps, } from './thread-types.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_MULTIMEDIA_MESSAGE: 'send_multimedia_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_SUBSCRIPTION: 'change_thread_subscription', CHANGE_THREAD_READ_STATUS: 'change_thread_read_status', CREATE_ENTRY: 'create_entry', DELETE_ENTRY: 'delete_entry', EDIT_ENTRY: 'edit_entry', UPDATE_RELATIONSHIP: 'update_relationship', }); export type DMOperationType = $Values; type MemberIDWithSubscription = { +id: string, +subscription: ThreadSubscription, }; export const memberIDWithSubscriptionValidator: TInterface = tShape({ id: tUserID, subscription: threadSubscriptionValidator, }); export type CreateThickRawThreadInfoInput = { +threadID: string, +threadType: ThickThreadType, +creationTime: number, +parentThreadID?: ?string, +allMemberIDsWithSubscriptions: $ReadOnlyArray, +roleID: string, +unread: boolean, +timestamps: ThreadTimestamps, +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), allMemberIDsWithSubscriptions: t.list(memberIDWithSubscriptionValidator), roleID: t.String, unread: t.Boolean, timestamps: threadTimestampsValidator, 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 DMSendMultimediaMessageOperation = { +type: 'send_multimedia_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +media: $ReadOnlyArray, }; export const dmSendMultimediaMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_MULTIMEDIA_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, media: t.list(mediaValidator), }); 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, +addedUserIDs: $ReadOnlyArray, }; const dmAddMembersBaseValidatorShape = { editorID: tUserID, time: t.Number, addedUserIDs: t.list(tUserID), }; export type DMAddMembersOperation = $ReadOnly<{ +type: 'add_members', +threadID: string, +messageID: string, ...DMAddMembersBase, }>; export const dmAddMembersOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_MEMBERS), threadID: t.String, messageID: t.String, ...dmAddMembersBaseValidatorShape, }); export type DMAddViewerToThreadMembersOperation = $ReadOnly<{ +type: 'add_viewer_to_thread_members', +messageID: ?string, +existingThreadDetails: CreateThickRawThreadInfoInput, ...DMAddMembersBase, }>; export const dmAddViewerToThreadMembersValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_VIEWER_TO_THREAD_MEMBERS), messageID: t.maybe(t.String), 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 DMThreadSettingsChanges = { +name?: string, +description?: string, +color?: string, +avatar?: ClientAvatar | null, }; export type DMChangeThreadSettingsOperation = $ReadOnly<{ +type: 'change_thread_settings', +threadID: string, +editorID: string, +time: number, +changes: DMThreadSettingsChanges, +messageIDsPrefix: string, }>; export const dmChangeThreadSettingsOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS), threadID: t.String, editorID: tUserID, time: t.Number, changes: tShape({ name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), avatar: t.maybe(clientAvatarValidator), }), messageIDsPrefix: t.String, }); export type DMChangeThreadSubscriptionOperation = { +type: 'change_thread_subscription', +time: number, +threadID: string, +creatorID: string, +subscription: ThreadSubscription, }; export const dmChangeThreadSubscriptionOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_SUBSCRIPTION), time: t.Number, threadID: t.String, creatorID: tUserID, subscription: threadSubscriptionValidator, }); export type DMChangeThreadReadStatusOperation = { +type: 'change_thread_read_status', +time: number, +threadID: string, +creatorID: string, +unread: boolean, }; export const dmChangeThreadReadStatusOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_READ_STATUS), time: t.Number, threadID: t.String, creatorID: tUserID, unread: t.Boolean, }); export type ComposableDMOperation = | DMSendTextMessageOperation | DMSendMultimediaMessageOperation; export type DMCreateEntryOperation = { +type: 'create_entry', +threadID: string, +creatorID: string, +time: number, +entryID: string, +entryDate: string, +text: string, +messageID: string, }; export const dmCreateEntryOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CREATE_ENTRY), threadID: t.String, creatorID: tUserID, time: t.Number, entryID: t.String, entryDate: t.String, text: t.String, messageID: t.String, }); export type DMDeleteEntryOperation = { +type: 'delete_entry', +threadID: string, +creatorID: string, +time: number, +creationTime: number, +entryID: string, +entryDate: string, +prevText: string, +messageID: string, }; export const dmDeleteEntryOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.DELETE_ENTRY), threadID: t.String, creatorID: tUserID, time: t.Number, creationTime: t.Number, entryID: t.String, entryDate: t.String, prevText: t.String, messageID: t.String, }); export type DMEditEntryOperation = { +type: 'edit_entry', +threadID: string, +creatorID: string, +time: number, +creationTime: number, +entryID: string, +entryDate: string, +text: string, +messageID: string, }; export const dmEditEntryOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.EDIT_ENTRY), threadID: t.String, creatorID: tUserID, creationTime: t.Number, time: t.Number, entryID: t.String, entryDate: t.String, text: t.String, messageID: t.String, }); export type DMUpdateRelationshipOperation = { +type: 'update_relationship', +threadID: string, +creatorID: string, +time: number, +operation: RelationshipOperation, +targetUserID: string, +messageID: string, }; export const dmUpdateRelationshipOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.UPDATE_RELATIONSHIP), threadID: t.String, creatorID: tUserID, time: t.Number, operation: t.enums.of([ 'request_sent', 'request_accepted', 'farcaster_mutual', ]), targetUserID: tUserID, messageID: t.String, }); export type DMOperation = | DMCreateThreadOperation | DMCreateSidebarOperation | DMSendTextMessageOperation | DMSendMultimediaMessageOperation | DMSendReactionMessageOperation | DMSendEditMessageOperation | DMAddMembersOperation | DMAddViewerToThreadMembersOperation | DMJoinThreadOperation | DMLeaveThreadOperation | DMRemoveMembersOperation | DMChangeThreadSettingsOperation | DMChangeThreadSubscriptionOperation | DMChangeThreadReadStatusOperation | DMCreateEntryOperation | DMDeleteEntryOperation | DMEditEntryOperation | DMUpdateRelationshipOperation; export const dmOperationValidator: TUnion = t.union([ dmCreateThreadOperationValidator, dmCreateSidebarOperationValidator, dmSendTextMessageOperationValidator, dmSendMultimediaMessageOperationValidator, dmSendReactionMessageOperationValidator, dmSendEditMessageOperationValidator, dmAddMembersOperationValidator, dmAddViewerToThreadMembersValidator, dmJoinThreadOperationValidator, dmLeaveThreadOperationValidator, dmRemoveMembersOperationValidator, dmChangeThreadSettingsOperationValidator, dmChangeThreadSubscriptionOperationValidator, dmChangeThreadReadStatusOperationValidator, dmCreateEntryOperationValidator, dmDeleteEntryOperationValidator, dmEditEntryOperationValidator, dmUpdateRelationshipOperationValidator, ]); export type DMBlobOperation = $ReadOnly<{ ...BlobOperation, +dmOpType: 'inbound_only' | 'outbound_only' | 'inbound_and_outbound', }>; export type DMOperationResult = { rawMessageInfos: Array, updateInfos: Array, blobOps: Array, + notificationsCreationData: ?NotificationsCreationData, }; 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. +composableMessageID: ?string, +notificationsCreationData: ?NotificationsCreationData, }; export const queueDMOpsActionType = 'QUEUE_DM_OPS'; export type QueueDMOpsPayload = { +operation: DMOperation, +timestamp: number, +condition: | { +type: 'thread', +threadID: string, } | { +type: 'entry', +entryID: string, } | { +type: 'message', +messageID: string, } | { +type: 'membership', +threadID: string, +userID: string, }, }; 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 const clearQueuedMessageDMOpsActionType = 'CLEAR_QUEUED_MESSAGE_DM_OPS'; export type ClearQueuedMessageDMOpsPayload = { +messageID: string, }; export const clearQueuedEntryDMOpsActionType = 'CLEAR_QUEUED_ENTRY_DM_OPS'; export type ClearQueuedEntryDMOpsPayload = { +entryID: string, }; export const clearQueuedMembershipDMOpsActionType = 'CLEAR_QUEUED_MEMBERSHIP_DM_OPS'; export type ClearQueuedMembershipDMOpsPayload = { +threadID: string, +userID: string, }; export type OperationsQueue = $ReadOnlyArray<{ +operation: DMOperation, +timestamp: number, }>; export type QueuedDMOperations = { +threadQueue: { +[threadID: string]: OperationsQueue, }, +messageQueue: { +[messageID: string]: OperationsQueue, }, +entryQueue: { +[entryID: string]: OperationsQueue, }, +membershipQueue: { +[threadID: string]: { +[memberID: string]: OperationsQueue, }, }, };