diff --git a/lib/actions/thread-actions.js b/lib/actions/thread-actions.js index 4815d60cd..61f34fbcb 100644 --- a/lib/actions/thread-actions.js +++ b/lib/actions/thread-actions.js @@ -1,493 +1,493 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import uuid from 'uuid'; import genesis from '../facts/genesis.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { useKeyserverCall } from '../keyserver-conn/keyserver-call.js'; import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import { type OutboundDMOperationSpecification, dmOperationSpecificationTypes, } from '../shared/dm-ops/dm-op-utils.js'; import { useProcessAndSendDMOperation } from '../shared/dm-ops/process-dm-ops.js'; import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.js'; import type { DMChangeThreadSettingsOperation, - DMThreadSettingsChangesBase, + DMThreadSettingsChanges, } from '../types/dm-ops.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { thickThreadTypes, threadTypeIsThick, } from '../types/thread-types-enum.js'; import type { ChangeThreadSettingsPayload, LeaveThreadPayload, UpdateThreadRequest, ClientNewThinThreadRequest, NewThreadResult, ClientThreadJoinRequest, ThreadJoinPayload, ThreadFetchMediaRequest, ThreadFetchMediaResult, RoleModificationRequest, RoleModificationPayload, RoleDeletionRequest, RoleDeletionPayload, } from '../types/thread-types.js'; import { values } from '../utils/objects.js'; import { useSelector } from '../utils/redux-utils.js'; export type DeleteThreadInput = { +threadID: string, }; const deleteThreadActionTypes = Object.freeze({ started: 'DELETE_THREAD_STARTED', success: 'DELETE_THREAD_SUCCESS', failed: 'DELETE_THREAD_FAILED', }); const deleteThreadEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const deleteThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: DeleteThreadInput) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'delete_thread', requests, deleteThreadEndpointOptions, ); const response = responses[keyserverID]; return { updatesResult: response.updatesResult, }; }; function useDeleteThread(): ( input: DeleteThreadInput, ) => Promise { return useKeyserverCall(deleteThread); } const changeThreadSettingsActionTypes = Object.freeze({ started: 'CHANGE_THREAD_SETTINGS_STARTED', success: 'CHANGE_THREAD_SETTINGS_SUCCESS', failed: 'CHANGE_THREAD_SETTINGS_FAILED', }); const changeThreadSettingsEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const changeThreadSettings = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: UpdateThreadRequest) => Promise) => async input => { invariant( Object.keys(input.changes).length > 0, 'No changes provided to changeThreadSettings!', ); const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'update_thread', requests, changeThreadSettingsEndpointOptions, ); const response = responses[keyserverID]; return { threadID: input.threadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, }; }; function useChangeThreadSettings( threadInfo: ?ThreadInfo, ): (input: UpdateThreadRequest) => Promise { const processAndSendDMOperation = useProcessAndSendDMOperation(); const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const keyserverCall = useKeyserverCall(changeThreadSettings); return React.useCallback( async (input: UpdateThreadRequest) => { if (!threadInfo || !threadTypeIsThick(threadInfo.type)) { return await keyserverCall(input); } invariant(viewerID, 'viewerID should be set'); - const changes: { ...DMThreadSettingsChangesBase } = {}; + const changes: { ...DMThreadSettingsChanges } = {}; if (input.changes.name) { changes.name = input.changes.name; } if (input.changes.description) { changes.description = input.changes.description; } if (input.changes.color) { changes.color = input.changes.color; } if (input.changes.avatar && input.changes.avatar.type === 'emoji') { changes.avatar = { type: 'emoji', emoji: input.changes.avatar.emoji, color: input.changes.avatar.color, }; } else if (input.changes.avatar && input.changes.avatar.type === 'ens') { changes.avatar = { type: 'ens' }; } // To support `image` and `encrypted_image` avatars we first, need stop // sending multimedia metadata to keyserver. // ENG-8708 const op: DMChangeThreadSettingsOperation = { type: 'change_thread_settings', threadID: threadInfo.id, editorID: viewerID, time: Date.now(), changes, messageIDsPrefix: uuid.v4(), }; const opSpecification: OutboundDMOperationSpecification = { type: dmOperationSpecificationTypes.OUTBOUND, op, recipients: { type: 'all_thread_members', threadID: threadInfo.type === thickThreadTypes.THICK_SIDEBAR && threadInfo.parentThreadID ? threadInfo.parentThreadID : threadInfo.id, }, }; await processAndSendDMOperation(opSpecification); return ({ threadID: threadInfo.id, updatesResult: { newUpdates: [] }, newMessageInfos: [], }: ChangeThreadSettingsPayload); }, [keyserverCall, processAndSendDMOperation, threadInfo, viewerID], ); } export type RemoveUsersFromThreadInput = { +threadID: string, +memberIDs: $ReadOnlyArray, }; const removeUsersFromThreadActionTypes = Object.freeze({ started: 'REMOVE_USERS_FROM_THREAD_STARTED', success: 'REMOVE_USERS_FROM_THREAD_SUCCESS', failed: 'REMOVE_USERS_FROM_THREAD_FAILED', }); const removeMembersFromThreadEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const removeUsersFromThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: RemoveUsersFromThreadInput, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'remove_members', requests, removeMembersFromThreadEndpointOptions, ); const response = responses[keyserverID]; return { threadID: input.threadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, }; }; function useRemoveUsersFromThread(): ( input: RemoveUsersFromThreadInput, ) => Promise { return useKeyserverCall(removeUsersFromThread); } export type ChangeThreadMemberRolesInput = { +threadID: string, +memberIDs: $ReadOnlyArray, +newRole: string, }; const changeThreadMemberRolesActionTypes = Object.freeze({ started: 'CHANGE_THREAD_MEMBER_ROLES_STARTED', success: 'CHANGE_THREAD_MEMBER_ROLES_SUCCESS', failed: 'CHANGE_THREAD_MEMBER_ROLES_FAILED', }); const changeThreadMemberRoleEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const changeThreadMemberRoles = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: ChangeThreadMemberRolesInput, ) => Promise) => async input => { const { threadID, memberIDs, newRole } = input; const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: { threadID, memberIDs, role: newRole, }, }; const responses = await callKeyserverEndpoint( 'update_role', requests, changeThreadMemberRoleEndpointOptions, ); const response = responses[keyserverID]; return { threadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, }; }; function useChangeThreadMemberRoles(): ( input: ChangeThreadMemberRolesInput, ) => Promise { return useKeyserverCall(changeThreadMemberRoles); } const newThreadActionTypes = Object.freeze({ started: 'NEW_THREAD_STARTED', success: 'NEW_THREAD_SUCCESS', failed: 'NEW_THREAD_FAILED', }); const newThinThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: ClientNewThinThreadRequest) => Promise) => async input => { const parentThreadID = input.parentThreadID ?? genesis().id; const keyserverID = extractKeyserverIDFromID(parentThreadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint('create_thread', requests); const response = responses[keyserverID]; return { newThreadID: response.newThreadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, userInfos: response.userInfos, }; }; function useNewThinThread(): ( input: ClientNewThinThreadRequest, ) => Promise { return useKeyserverCall(newThinThread); } const joinThreadActionTypes = Object.freeze({ started: 'JOIN_THREAD_STARTED', success: 'JOIN_THREAD_SUCCESS', failed: 'JOIN_THREAD_FAILED', }); const joinThreadOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const joinThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: ClientThreadJoinRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'join_thread', requests, joinThreadOptions, ); const response = responses[keyserverID]; const userInfos = values(response.userInfos); return { updatesResult: response.updatesResult, rawMessageInfos: response.rawMessageInfos, truncationStatuses: response.truncationStatuses, userInfos, keyserverID, }; }; function useJoinThread(): ( input: ClientThreadJoinRequest, ) => Promise { return useKeyserverCall(joinThread); } export type LeaveThreadInput = { +threadID: string, }; const leaveThreadActionTypes = Object.freeze({ started: 'LEAVE_THREAD_STARTED', success: 'LEAVE_THREAD_SUCCESS', failed: 'LEAVE_THREAD_FAILED', }); const leaveThreadEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const leaveThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: LeaveThreadInput) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'leave_thread', requests, leaveThreadEndpointOptions, ); const response = responses[keyserverID]; return { updatesResult: response.updatesResult, }; }; function useLeaveThread(): ( input: LeaveThreadInput, ) => Promise { return useKeyserverCall(leaveThread); } const fetchThreadMedia = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: ThreadFetchMediaRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'fetch_thread_media', requests, ); const response = responses[keyserverID]; return { media: response.media }; }; function useFetchThreadMedia(): ( input: ThreadFetchMediaRequest, ) => Promise { return useKeyserverCall(fetchThreadMedia); } const modifyCommunityRoleActionTypes = Object.freeze({ started: 'MODIFY_COMMUNITY_ROLE_STARTED', success: 'MODIFY_COMMUNITY_ROLE_SUCCESS', failed: 'MODIFY_COMMUNITY_ROLE_FAILED', }); const modifyCommunityRole = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: RoleModificationRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.community); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'modify_community_role', requests, ); const response = responses[keyserverID]; return { threadInfo: response.threadInfo, updatesResult: response.updatesResult, }; }; function useModifyCommunityRole(): ( input: RoleModificationRequest, ) => Promise { return useKeyserverCall(modifyCommunityRole); } const deleteCommunityRoleActionTypes = Object.freeze({ started: 'DELETE_COMMUNITY_ROLE_STARTED', success: 'DELETE_COMMUNITY_ROLE_SUCCESS', failed: 'DELETE_COMMUNITY_ROLE_FAILED', }); const deleteCommunityRole = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: RoleDeletionRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.community); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'delete_community_role', requests, ); const response = responses[keyserverID]; return { threadInfo: response.threadInfo, updatesResult: response.updatesResult, }; }; function useDeleteCommunityRole(): ( input: RoleDeletionRequest, ) => Promise { return useKeyserverCall(deleteCommunityRole); } export { deleteThreadActionTypes, useDeleteThread, changeThreadSettingsActionTypes, useChangeThreadSettings, removeUsersFromThreadActionTypes, useRemoveUsersFromThread, changeThreadMemberRolesActionTypes, useChangeThreadMemberRoles, newThreadActionTypes, useNewThinThread, joinThreadActionTypes, useJoinThread, leaveThreadActionTypes, useLeaveThread, useFetchThreadMedia, modifyCommunityRoleActionTypes, useModifyCommunityRole, deleteCommunityRoleActionTypes, useDeleteCommunityRole, }; diff --git a/lib/shared/dm-ops/add-members-spec.js b/lib/shared/dm-ops/add-members-spec.js index 0fe28ff82..2ec1a8236 100644 --- a/lib/shared/dm-ops/add-members-spec.js +++ b/lib/shared/dm-ops/add-members-spec.js @@ -1,147 +1,128 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import { createRoleAndPermissionForThickThreads } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMAddMembersOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { type RawMessageInfo } from '../../types/message-types.js'; import type { AddMembersMessageData } from '../../types/messages/add-members.js'; import { minimallyEncodeMemberInfo, type ThickRawThreadInfo, } from '../../types/minimally-encoded-thread-permissions-types.js'; import { joinThreadSubscription } from '../../types/subscription-types.js'; import type { ThickMemberInfo } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { values } from '../../utils/objects.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; import { roleIsDefaultRole, userIsMember } from '../thread-utils.js'; export type AddMembersResult = { rawMessageInfos: Array, updateInfos: Array, threadInfo: ?ThickRawThreadInfo, }; function createAddNewMembersMessageDataFromDMOperation( dmOperation: DMAddMembersOperation, ): AddMembersMessageData { const { editorID, time, addedUserIDs, threadID } = dmOperation; return { type: messageTypes.ADD_MEMBERS, threadID, creatorID: editorID, time, addedUserIDs: [...addedUserIDs], }; } -function createAddNewMembersResults( - dmOperation: DMAddMembersOperation, - viewerID: string, - utilities: ProcessDMOperationUtilities, -): AddMembersResult { - const { editorID, time, messageID, addedUserIDs, threadID } = dmOperation; - const messageData = - createAddNewMembersMessageDataFromDMOperation(dmOperation); - const rawMessageInfos = [ - rawMessageInfoFromMessageData(messageData, messageID), - ]; - const currentThreadInfo = utilities.threadInfos[threadID]; - if (!currentThreadInfo.thick) { - return { - rawMessageInfos: [], - updateInfos: [], - threadInfo: null, - }; - } - const defaultRoleID = values(currentThreadInfo.roles).find(role => - roleIsDefaultRole(role), - )?.id; - invariant(defaultRoleID, 'Default role ID must exist'); - const { membershipPermissions } = createRoleAndPermissionForThickThreads( - currentThreadInfo.type, - currentThreadInfo.id, - defaultRoleID, - ); - const newMembers = addedUserIDs - .filter(userID => !userIsMember(currentThreadInfo, userID)) - .map(userID => - minimallyEncodeMemberInfo({ - id: userID, - role: defaultRoleID, - permissions: membershipPermissions, - isSender: editorID === viewerID, - subscription: joinThreadSubscription, - }), - ); - - const resultThreadInfo = { - ...currentThreadInfo, - members: [...currentThreadInfo.members, ...newMembers], - }; - const updateInfos = [ - { - type: updateTypes.UPDATE_THREAD, - id: uuid.v4(), - time, - threadInfo: resultThreadInfo, - }, - ]; - - return { - rawMessageInfos, - updateInfos, - threadInfo: resultThreadInfo, - }; -} - const addMembersSpec: DMOperationSpec = Object.freeze({ notificationsCreationData: async (dmOperation: DMAddMembersOperation) => { const messageData = createAddNewMembersMessageDataFromDMOperation(dmOperation); return { messageDatas: [messageData] }; }, processDMOperation: async ( dmOperation: DMAddMembersOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { - const { rawMessageInfos, updateInfos } = createAddNewMembersResults( - dmOperation, - viewerID, - utilities, + const { editorID, time, messageID, addedUserIDs, threadID } = dmOperation; + const messageData = + createAddNewMembersMessageDataFromDMOperation(dmOperation); + const rawMessageInfos = [ + rawMessageInfoFromMessageData(messageData, messageID), + ]; + const currentThreadInfo = utilities.threadInfos[threadID]; + if (!currentThreadInfo.thick) { + return { + rawMessageInfos: [], + updateInfos: [], + }; + } + const defaultRoleID = values(currentThreadInfo.roles).find(role => + roleIsDefaultRole(role), + )?.id; + invariant(defaultRoleID, 'Default role ID must exist'); + const { membershipPermissions } = createRoleAndPermissionForThickThreads( + currentThreadInfo.type, + currentThreadInfo.id, + defaultRoleID, ); - return { rawMessageInfos, updateInfos }; + const newMembers = addedUserIDs + .filter(userID => !userIsMember(currentThreadInfo, userID)) + .map(userID => + minimallyEncodeMemberInfo({ + id: userID, + role: defaultRoleID, + permissions: membershipPermissions, + isSender: editorID === viewerID, + subscription: joinThreadSubscription, + }), + ); + + const resultThreadInfo = { + ...currentThreadInfo, + members: [...currentThreadInfo.members, ...newMembers], + }; + const updateInfos = [ + { + type: updateTypes.UPDATE_THREAD, + id: uuid.v4(), + time, + threadInfo: resultThreadInfo, + }, + ]; + + return { + rawMessageInfos, + updateInfos, + }; }, canBeProcessed( dmOperation: DMAddMembersOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, }); -export { - addMembersSpec, - createAddNewMembersResults, - createAddNewMembersMessageDataFromDMOperation, -}; +export { addMembersSpec, createAddNewMembersMessageDataFromDMOperation }; diff --git a/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js b/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js deleted file mode 100644 index a667737ac..000000000 --- a/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js +++ /dev/null @@ -1,103 +0,0 @@ -// @flow - -import { - addViewerToThreadMembersSpec, - createAddViewerToThreadMembersResults, - createAddViewerToThreadMembersMessageDataFromDMOp, -} from './add-viewer-to-thread-members-spec.js'; -import { - processChangeSettingsOperation, - createChangeSettingsMessageDatasAndUpdate, -} from './change-thread-settings-spec.js'; -import type { - DMOperationSpec, - ProcessDMOperationUtilities, -} from './dm-op-spec.js'; -import type { DMChangeThreadSettingsAndAddViewerOperation } from '../../types/dm-ops.js'; -import type { MessageData } from '../../types/message-types.js'; -import { values } from '../../utils/objects.js'; - -function createAddViewerAndMembersOperation( - dmOperation: DMChangeThreadSettingsAndAddViewerOperation, -) { - const { editorID, time, messageIDsPrefix, changes, existingThreadDetails } = - dmOperation; - const newMemberIDs = - changes.newMemberIDs && changes.newMemberIDs.length > 0 - ? [...new Set(changes.newMemberIDs)] - : []; - return { - type: 'add_viewer_to_thread_members', - editorID, - time, - messageID: `${messageIDsPrefix}/add_members`, - addedUserIDs: newMemberIDs, - existingThreadDetails, - }; -} - -function processAddViewerToThreadMembersOperation( - dmOperation: DMChangeThreadSettingsAndAddViewerOperation, - viewerID: string, -) { - const operation = createAddViewerAndMembersOperation(dmOperation); - if (operation.addedUserIDs.length === 0) { - return null; - } - return createAddViewerToThreadMembersResults(operation, viewerID); -} - -const changeThreadSettingsAndAddViewerSpec: DMOperationSpec = - Object.freeze({ - notificationsCreationData: async ( - dmOperation: DMChangeThreadSettingsAndAddViewerOperation, - ) => { - const messageDatas: Array = []; - const addNewMembersOperation = - createAddViewerAndMembersOperation(dmOperation); - if (addNewMembersOperation) { - const addNewMembersMessageData = - createAddViewerToThreadMembersMessageDataFromDMOp( - addNewMembersOperation, - ); - messageDatas.push(addNewMembersMessageData); - } - - const { fieldNameToMessageData } = - createChangeSettingsMessageDatasAndUpdate(dmOperation); - messageDatas.push(...values(fieldNameToMessageData)); - return { messageDatas }; - }, - processDMOperation: async ( - dmOperation: DMChangeThreadSettingsAndAddViewerOperation, - viewerID: string, - utilities: ProcessDMOperationUtilities, - ) => { - const addMembersResult = processAddViewerToThreadMembersOperation( - dmOperation, - viewerID, - ); - - return processChangeSettingsOperation( - dmOperation, - viewerID, - utilities, - addMembersResult, - ); - }, - canBeProcessed( - dmOperation: DMChangeThreadSettingsAndAddViewerOperation, - viewerID: string, - utilities: ProcessDMOperationUtilities, - ) { - const operation = createAddViewerAndMembersOperation(dmOperation); - return addViewerToThreadMembersSpec.canBeProcessed( - operation, - viewerID, - utilities, - ); - }, - supportsAutoRetry: true, - }); - -export { changeThreadSettingsAndAddViewerSpec }; diff --git a/lib/shared/dm-ops/change-thread-settings-spec.js b/lib/shared/dm-ops/change-thread-settings-spec.js index a1b620113..f43970590 100644 --- a/lib/shared/dm-ops/change-thread-settings-spec.js +++ b/lib/shared/dm-ops/change-thread-settings-spec.js @@ -1,243 +1,161 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; -import { - type AddMembersResult, - createAddNewMembersResults, - createAddNewMembersMessageDataFromDMOperation, -} from './add-members-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { - DMChangeThreadSettingsAndAddViewerOperation, DMChangeThreadSettingsOperation, - DMOperationResult, - DMThreadSettingsChangesBase, + DMThreadSettingsChanges, } from '../../types/dm-ops.js'; import type { MessageData, 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 } 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 { values } from '../../utils/objects.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; -function createAddMembersOperation( - dmOperation: DMChangeThreadSettingsOperation, -) { - const { editorID, time, messageIDsPrefix, changes, threadID } = dmOperation; - const newMemberIDs = - changes.newMemberIDs && changes.newMemberIDs.length > 0 - ? [...new Set(changes.newMemberIDs)] - : []; - if (!changes.newMemberIDs || changes.newMemberIDs.length === 0) { - return null; - } - return { - type: 'add_members', - editorID, - time, - messageID: `${messageIDsPrefix}/add_members`, - addedUserIDs: newMemberIDs, - threadID, - }; -} - -function processAddMembersOperation( - dmOperation: DMChangeThreadSettingsOperation, - viewerID: string, - utilities: ProcessDMOperationUtilities, -) { - const operation = createAddMembersOperation(dmOperation); - if (!operation) { - return null; - } - return createAddNewMembersResults(operation, viewerID, utilities); -} - function getThreadIDFromChangeThreadSettingsDMOp( - dmOperation: - | DMChangeThreadSettingsOperation - | DMChangeThreadSettingsAndAddViewerOperation, + dmOperation: DMChangeThreadSettingsOperation, ): string { return dmOperation.type === 'change_thread_settings' ? dmOperation.threadID : dmOperation.existingThreadDetails.threadID; } function createChangeSettingsMessageDatasAndUpdate( - dmOperation: - | DMChangeThreadSettingsOperation - | DMChangeThreadSettingsAndAddViewerOperation, + dmOperation: DMChangeThreadSettingsOperation, ): { +fieldNameToMessageData: { +[fieldName: string]: ChangeSettingsMessageData }, - +threadInfoUpdate: DMThreadSettingsChangesBase, + +threadInfoUpdate: DMThreadSettingsChanges, } { const { changes, editorID, time } = dmOperation; const { name, description, color, avatar } = changes; const threadID = getThreadIDFromChangeThreadSettingsDMOp(dmOperation); - const threadInfoUpdate: { ...DMThreadSettingsChangesBase } = {}; + 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) { threadInfoUpdate.avatar = avatar; } const fieldNameToMessageData: { [fieldName: string]: ChangeSettingsMessageData, } = {}; const { avatar: avatarObject, ...rest } = threadInfoUpdate; const normalizedThreadInfoUpdate = avatarObject ? { ...rest, avatar: JSON.stringify(avatarObject) } : { ...rest }; for (const fieldName in normalizedThreadInfoUpdate) { const value = normalizedThreadInfoUpdate[fieldName]; fieldNameToMessageData[fieldName] = { type: messageTypes.CHANGE_SETTINGS, threadID, creatorID: editorID, time, field: fieldName, value: value, }; } return { fieldNameToMessageData, threadInfoUpdate }; } -function processChangeSettingsOperation( - dmOperation: - | DMChangeThreadSettingsOperation - | DMChangeThreadSettingsAndAddViewerOperation, - viewerID: string, - utilities: ProcessDMOperationUtilities, - addMembersResult: ?AddMembersResult, -): DMOperationResult { - const { time, messageIDsPrefix } = dmOperation; - const threadID = getThreadIDFromChangeThreadSettingsDMOp(dmOperation); - - let threadInfoToUpdate: ?RawThreadInfo = utilities.threadInfos[threadID]; - const updateInfos: Array = []; - const rawMessageInfos: Array = []; - - if (addMembersResult) { - if (addMembersResult.threadInfo) { - threadInfoToUpdate = addMembersResult.threadInfo; - } - updateInfos.push(...addMembersResult.updateInfos); - rawMessageInfos.push(...addMembersResult.rawMessageInfos); - } - - invariant(threadInfoToUpdate?.thick, 'Thread should be thick'); - const { fieldNameToMessageData, threadInfoUpdate } = - createChangeSettingsMessageDatasAndUpdate(dmOperation); - - const fieldNameToMessageDataPairs = Object.entries(fieldNameToMessageData); - rawMessageInfos.push( - ...fieldNameToMessageDataPairs.map(([fieldName, messageData]) => - rawMessageInfoFromMessageData( - messageData, - `${messageIDsPrefix}/${fieldName}`, - ), - ), - ); - - threadInfoToUpdate = { - ...threadInfoToUpdate, - ...threadInfoUpdate, - }; - - if (fieldNameToMessageDataPairs.length > 0) { - updateInfos.push({ - type: updateTypes.UPDATE_THREAD, - id: uuid.v4(), - time, - threadInfo: threadInfoToUpdate, - }); - } - - return { - rawMessageInfos, - updateInfos, - }; -} - const changeThreadSettingsSpec: DMOperationSpec = Object.freeze({ notificationsCreationData: async ( dmOperation: DMChangeThreadSettingsOperation, ) => { const messageDatas: Array = []; - const addNewMembersOperation = createAddMembersOperation(dmOperation); - if (addNewMembersOperation) { - const addNewMembersMessageData = - createAddNewMembersMessageDataFromDMOperation(addNewMembersOperation); - messageDatas.push(addNewMembersMessageData); - } const { fieldNameToMessageData } = createChangeSettingsMessageDatasAndUpdate(dmOperation); messageDatas.push(...values(fieldNameToMessageData)); return { messageDatas }; }, processDMOperation: async ( dmOperation: DMChangeThreadSettingsOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { - const addMembersResult = processAddMembersOperation( - dmOperation, - viewerID, - utilities, - ); + const { time, messageIDsPrefix } = dmOperation; + const threadID = getThreadIDFromChangeThreadSettingsDMOp(dmOperation); + + let threadInfoToUpdate: ?RawThreadInfo = utilities.threadInfos[threadID]; + const updateInfos: Array = []; + const rawMessageInfos: Array = []; + + invariant(threadInfoToUpdate?.thick, 'Thread should be thick'); + const { fieldNameToMessageData, threadInfoUpdate } = + createChangeSettingsMessageDatasAndUpdate(dmOperation); - return processChangeSettingsOperation( - dmOperation, - viewerID, - utilities, - addMembersResult, + const fieldNameToMessageDataPairs = Object.entries( + fieldNameToMessageData, ); + rawMessageInfos.push( + ...fieldNameToMessageDataPairs.map(([fieldName, messageData]) => + rawMessageInfoFromMessageData( + messageData, + `${messageIDsPrefix}/${fieldName}`, + ), + ), + ); + + threadInfoToUpdate = { + ...threadInfoToUpdate, + ...threadInfoUpdate, + }; + + if (fieldNameToMessageDataPairs.length > 0) { + updateInfos.push({ + type: updateTypes.UPDATE_THREAD, + id: uuid.v4(), + time, + threadInfo: threadInfoToUpdate, + }); + } + + return { + rawMessageInfos, + updateInfos, + }; }, canBeProcessed( dmOperation: DMChangeThreadSettingsOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, }); -export { - changeThreadSettingsSpec, - processChangeSettingsOperation, - createChangeSettingsMessageDatasAndUpdate, -}; +export { changeThreadSettingsSpec, createChangeSettingsMessageDatasAndUpdate }; diff --git a/lib/shared/dm-ops/dm-op-specs.js b/lib/shared/dm-ops/dm-op-specs.js index f7ddc1b57..e40898dcb 100644 --- a/lib/shared/dm-ops/dm-op-specs.js +++ b/lib/shared/dm-ops/dm-op-specs.js @@ -1,34 +1,31 @@ // @flow import { addMembersSpec } from './add-members-spec.js'; import { addViewerToThreadMembersSpec } from './add-viewer-to-thread-members-spec.js'; -import { changeThreadSettingsAndAddViewerSpec } from './change-thread-settings-and-add-viewer-spec.js'; import { changeThreadSettingsSpec } from './change-thread-settings-spec.js'; import { createSidebarSpec } from './create-sidebar-spec.js'; import { createThreadSpec } from './create-thread-spec.js'; import type { DMOperationSpec } from './dm-op-spec.js'; import { joinThreadSpec } from './join-thread-spec.js'; import { leaveThreadSpec } from './leave-thread-spec.js'; import { removeMembersSpec } from './remove-members-spec.js'; import { sendEditMessageSpec } from './send-edit-message-spec.js'; import { sendReactionMessageSpec } from './send-reaction-message-spec.js'; import { sendTextMessageSpec } from './send-text-message-spec.js'; import { type DMOperationType, dmOperationTypes } from '../../types/dm-ops.js'; export const dmOpSpecs: { +[DMOperationType]: DMOperationSpec, } = Object.freeze({ [dmOperationTypes.CREATE_THREAD]: createThreadSpec, [dmOperationTypes.CREATE_SIDEBAR]: createSidebarSpec, [dmOperationTypes.SEND_TEXT_MESSAGE]: sendTextMessageSpec, [dmOperationTypes.SEND_REACTION_MESSAGE]: sendReactionMessageSpec, [dmOperationTypes.SEND_EDIT_MESSAGE]: sendEditMessageSpec, [dmOperationTypes.ADD_MEMBERS]: addMembersSpec, [dmOperationTypes.ADD_VIEWER_TO_THREAD_MEMBERS]: addViewerToThreadMembersSpec, [dmOperationTypes.JOIN_THREAD]: joinThreadSpec, [dmOperationTypes.LEAVE_THREAD]: leaveThreadSpec, [dmOperationTypes.REMOVE_MEMBERS]: removeMembersSpec, [dmOperationTypes.CHANGE_THREAD_SETTINGS]: changeThreadSettingsSpec, - [dmOperationTypes.CHANGE_THREAD_SETTINGS_AND_ADD_VIEWER]: - changeThreadSettingsAndAddViewerSpec, }); diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js index c7015e343..8ea4950fa 100644 --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -1,409 +1,378 @@ // @flow -import t, { type TInterface, type TUnion, type TStructProps } from 'tcomb'; +import t, { type TInterface, type TUnion } from 'tcomb'; import { clientAvatarValidator, type ClientAvatar } from './avatar-types.js'; import type { RawMessageInfo } from './message-types.js'; import type { NotificationsCreationData } from './notif-types.js'; import type { OutboundP2PMessage } from './sqlite-types.js'; import { type NonSidebarThickThreadType, nonSidebarThickThreadTypes, type ThickThreadType, thickThreadTypeValidator, } from './thread-types-enum.js'; import type { ClientUpdateInfo } from './update-types.js'; import { values } from '../utils/objects.js'; import { tColor, tShape, tString, tUserID } from '../utils/validation-utils.js'; export const dmOperationTypes = Object.freeze({ CREATE_THREAD: 'create_thread', CREATE_SIDEBAR: 'create_sidebar', SEND_TEXT_MESSAGE: 'send_text_message', SEND_REACTION_MESSAGE: 'send_reaction_message', SEND_EDIT_MESSAGE: 'send_edit_message', ADD_MEMBERS: 'add_members', ADD_VIEWER_TO_THREAD_MEMBERS: 'add_viewer_to_thread_members', JOIN_THREAD: 'join_thread', LEAVE_THREAD: 'leave_thread', REMOVE_MEMBERS: 'remove_members', CHANGE_THREAD_SETTINGS: 'change_thread_settings', - CHANGE_THREAD_SETTINGS_AND_ADD_VIEWER: - 'change_thread_settings_and_add_viewer', }); export type DMOperationType = $Values; export type CreateThickRawThreadInfoInput = { +threadID: string, +threadType: ThickThreadType, +creationTime: number, +parentThreadID?: ?string, +allMemberIDs: $ReadOnlyArray, +roleID: string, +unread: boolean, +name?: ?string, +avatar?: ?ClientAvatar, +description?: ?string, +color?: ?string, +containingThreadID?: ?string, +sourceMessageID?: ?string, +repliesCount?: ?number, +pinnedCount?: ?number, }; export const createThickRawThreadInfoInputValidator: TInterface = tShape({ threadID: t.String, threadType: thickThreadTypeValidator, creationTime: t.Number, parentThreadID: t.maybe(t.String), allMemberIDs: t.list(tUserID), roleID: t.String, unread: t.Boolean, name: t.maybe(t.String), avatar: t.maybe(clientAvatarValidator), description: t.maybe(t.String), color: t.maybe(t.String), containingThreadID: t.maybe(t.String), sourceMessageID: t.maybe(t.String), repliesCount: t.maybe(t.Number), pinnedCount: t.maybe(t.Number), }); export type DMCreateThreadOperation = { +type: 'create_thread', +threadID: string, +creatorID: string, +time: number, +threadType: NonSidebarThickThreadType, +memberIDs: $ReadOnlyArray, +roleID: string, +newMessageID: string, }; export const dmCreateThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CREATE_THREAD), threadID: t.String, creatorID: tUserID, time: t.Number, threadType: t.enums.of(values(nonSidebarThickThreadTypes)), memberIDs: t.list(tUserID), roleID: t.String, newMessageID: t.String, }); export type DMCreateSidebarOperation = { +type: 'create_sidebar', +threadID: string, +creatorID: string, +time: number, +parentThreadID: string, +memberIDs: $ReadOnlyArray, +sourceMessageID: string, +roleID: string, +newSidebarSourceMessageID: string, +newCreateSidebarMessageID: string, }; export const dmCreateSidebarOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CREATE_SIDEBAR), threadID: t.String, creatorID: tUserID, time: t.Number, parentThreadID: t.String, memberIDs: t.list(tUserID), sourceMessageID: t.String, roleID: t.String, newSidebarSourceMessageID: t.String, newCreateSidebarMessageID: t.String, }); export type DMSendTextMessageOperation = { +type: 'send_text_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +text: string, }; export const dmSendTextMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_TEXT_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, text: t.String, }); export type DMSendReactionMessageOperation = { +type: 'send_reaction_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +targetMessageID: string, +reaction: string, +action: 'add_reaction' | 'remove_reaction', }; export const dmSendReactionMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_REACTION_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, targetMessageID: t.String, reaction: t.String, action: t.enums.of(['add_reaction', 'remove_reaction']), }); export type DMSendEditMessageOperation = { +type: 'send_edit_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +targetMessageID: string, +text: string, }; export const dmSendEditMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_EDIT_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, targetMessageID: t.String, text: t.String, }); type DMAddMembersBase = { +editorID: string, +time: number, +messageID: string, +addedUserIDs: $ReadOnlyArray, }; const dmAddMembersBaseValidatorShape = { editorID: tUserID, time: t.Number, messageID: t.String, addedUserIDs: t.list(tUserID), }; export type DMAddMembersOperation = $ReadOnly<{ +type: 'add_members', +threadID: string, ...DMAddMembersBase, }>; export const dmAddMembersOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_MEMBERS), threadID: t.String, ...dmAddMembersBaseValidatorShape, }); export type DMAddViewerToThreadMembersOperation = $ReadOnly<{ +type: 'add_viewer_to_thread_members', +existingThreadDetails: CreateThickRawThreadInfoInput, ...DMAddMembersBase, }>; export const dmAddViewerToThreadMembersValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_VIEWER_TO_THREAD_MEMBERS), existingThreadDetails: createThickRawThreadInfoInputValidator, ...dmAddMembersBaseValidatorShape, }); export type DMJoinThreadOperation = { +type: 'join_thread', +joinerID: string, +time: number, +messageID: string, +existingThreadDetails: CreateThickRawThreadInfoInput, }; export const dmJoinThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.JOIN_THREAD), joinerID: tUserID, time: t.Number, messageID: t.String, existingThreadDetails: createThickRawThreadInfoInputValidator, }); export type DMLeaveThreadOperation = { +type: 'leave_thread', +editorID: string, +time: number, +messageID: string, +threadID: string, }; export const dmLeaveThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.LEAVE_THREAD), editorID: tUserID, time: t.Number, messageID: t.String, threadID: t.String, }); export type DMRemoveMembersOperation = { +type: 'remove_members', +editorID: string, +time: number, +messageID: string, +threadID: string, +removedUserIDs: $ReadOnlyArray, }; export const dmRemoveMembersOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.REMOVE_MEMBERS), editorID: tUserID, time: t.Number, messageID: t.String, threadID: t.String, removedUserIDs: t.list(tUserID), }); -export type DMThreadSettingsChangesBase = { +export type DMThreadSettingsChanges = { +name?: string, +description?: string, +color?: string, +avatar?: ClientAvatar, }; -type DMThreadSettingsChanges = $ReadOnly<{ - ...DMThreadSettingsChangesBase, - +newMemberIDs?: $ReadOnlyArray, -}>; - -type DMChangeThreadSettingsBase = { +export type DMChangeThreadSettingsOperation = $ReadOnly<{ + +type: 'change_thread_settings', + +threadID: string, +editorID: string, +time: number, +changes: DMThreadSettingsChanges, +messageIDsPrefix: string, -}; - -const dmChangeThreadSettingsBaseValidatorShape: TStructProps = - { +}>; +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), - newMemberIDs: t.maybe(t.list(tUserID)), avatar: t.maybe(clientAvatarValidator), }), messageIDsPrefix: t.String, - }; - -export type DMChangeThreadSettingsOperation = $ReadOnly<{ - +type: 'change_thread_settings', - +threadID: string, - ...DMChangeThreadSettingsBase, -}>; -export const dmChangeThreadSettingsOperationValidator: TInterface = - tShape({ - type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS), - threadID: t.String, - ...dmChangeThreadSettingsBaseValidatorShape, - }); - -export type DMChangeThreadSettingsAndAddViewerOperation = $ReadOnly<{ - +type: 'change_thread_settings_and_add_viewer', - +existingThreadDetails: CreateThickRawThreadInfoInput, - ...DMChangeThreadSettingsBase, -}>; -export const dmChangeThreadSettingsAndAddViewerOperationValidator: TInterface = - tShape({ - type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS_AND_ADD_VIEWER), - existingThreadDetails: createThickRawThreadInfoInputValidator, - ...dmChangeThreadSettingsBaseValidatorShape, }); export type DMOperation = | DMCreateThreadOperation | DMCreateSidebarOperation | DMSendTextMessageOperation | DMSendReactionMessageOperation | DMSendEditMessageOperation | DMAddMembersOperation | DMAddViewerToThreadMembersOperation | DMJoinThreadOperation | DMLeaveThreadOperation | DMRemoveMembersOperation - | DMChangeThreadSettingsOperation - | DMChangeThreadSettingsAndAddViewerOperation; + | DMChangeThreadSettingsOperation; export const dmOperationValidator: TUnion = t.union([ dmCreateThreadOperationValidator, dmCreateSidebarOperationValidator, dmSendTextMessageOperationValidator, dmSendReactionMessageOperationValidator, dmSendEditMessageOperationValidator, dmAddMembersOperationValidator, dmAddViewerToThreadMembersValidator, dmJoinThreadOperationValidator, dmLeaveThreadOperationValidator, dmRemoveMembersOperationValidator, dmChangeThreadSettingsOperationValidator, - dmChangeThreadSettingsAndAddViewerOperationValidator, ]); export type DMOperationResult = { rawMessageInfos: Array, updateInfos: Array, }; export const processDMOpsActionType = 'PROCESS_DM_OPS'; export type ProcessDMOpsPayload = { +rawMessageInfos: $ReadOnlyArray, +updateInfos: $ReadOnlyArray, +outboundP2PMessages: ?$ReadOnlyArray, // For messages that could be retried from UI, we need to bind DM `messageID` // with `outboundP2PMessages` to keep track of whether all P2P messages // were queued on Tunnelbroker. +messageIDWithoutAutoRetry: ?string, +notificationsCreationData: ?NotificationsCreationData, }; export const queueDMOpsActionType = 'QUEUE_DM_OPS'; export type QueueDMOpsPayload = { +operation: DMOperation, +threadID: string, +timestamp: number, }; export const pruneDMOpsQueueActionType = 'PRUNE_DM_OPS_QUEUE'; export type PruneDMOpsQueuePayload = { +pruneMaxTimestamp: number, }; export const clearQueuedThreadDMOpsActionType = 'CLEAR_QUEUED_THREAD_DM_OPS'; export type ClearQueuedThreadDMOpsPayload = { +threadID: string, }; export type QueuedDMOperations = { +operations: { +[threadID: string]: $ReadOnlyArray<{ +operation: DMOperation, +timestamp: number, }>, }, }; export type SendDMStartedPayload = { +messageID: string, }; export type SendDMOpsSuccessPayload = { +messageID: string, +outboundP2PMessageIDs: $ReadOnlyArray, }; export const sendDMActionTypes = Object.freeze({ started: 'SEND_DM_STARTED', success: 'SEND_DM_SUCCESS', failed: 'SEND_DM_FAILED', });