diff --git a/lib/shared/dm-ops/dm-op-specs.js b/lib/shared/dm-ops/dm-op-specs.js index 0f186be29..023841840 100644 --- a/lib/shared/dm-ops/dm-op-specs.js +++ b/lib/shared/dm-ops/dm-op-specs.js @@ -1,25 +1,27 @@ // @flow import { addMembersSpec } from './add-members-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.JOIN_THREAD]: joinThreadSpec, [dmOperationTypes.LEAVE_THREAD]: leaveThreadSpec, + [dmOperationTypes.REMOVE_MEMBERS]: removeMembersSpec, }); diff --git a/lib/shared/dm-ops/remove-members-spec.js b/lib/shared/dm-ops/remove-members-spec.js new file mode 100644 index 000000000..235ffab55 --- /dev/null +++ b/lib/shared/dm-ops/remove-members-spec.js @@ -0,0 +1,90 @@ +// @flow + +import uuid from 'uuid'; + +import type { + DMOperationSpec, + ProcessDMOperationUtilities, +} from './dm-op-spec.js'; +import type { DMRemoveMembersOperation } from '../../types/dm-ops.js'; +import { messageTypes } from '../../types/message-types-enum.js'; +import type { ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; +import { threadTypes } from '../../types/thread-types-enum.js'; +import { updateTypes } from '../../types/update-types-enum.js'; +import type { ClientUpdateInfo } from '../../types/update-types.js'; + +const removeMembersSpec: DMOperationSpec = + Object.freeze({ + processDMOperation: async ( + dmOperation: DMRemoveMembersOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) => { + const { editorID, time, messageID, threadID, removedUserIDs } = + dmOperation; + + const threadInfoOptional = utilities.threadInfos[threadID]; + if (!threadInfoOptional || !threadInfoOptional.thick) { + // We can't perform this operation now. It should be queued for later. + return { + rawMessageInfos: [], + updateInfos: [], + }; + } + const threadInfo: ThickRawThreadInfo = threadInfoOptional; + + const removeMembersMessage = { + type: messageTypes.REMOVE_MEMBERS, + id: messageID, + threadID, + time, + creatorID: editorID, + removedUserIDs: [...removedUserIDs], + }; + + const removedUserIDsSet = new Set(removedUserIDs); + const viewerIsRemoved = removedUserIDsSet.has(viewerID); + const updateInfos: Array = []; + if ( + viewerIsRemoved && + (threadInfo.type !== threadTypes.THICK_SIDEBAR || + (threadInfo.parentThreadID && + !utilities.threadInfos[threadInfo.parentThreadID])) + ) { + updateInfos.push({ + type: updateTypes.DELETE_THREAD, + id: uuid.v4(), + time, + threadID, + }); + } else { + const updatedThreadInfo = { + ...threadInfo, + members: threadInfo.members.filter( + member => !removedUserIDsSet.has(member.id), + ), + }; + updateInfos.push( + { + type: updateTypes.UPDATE_THREAD, + id: uuid.v4(), + time, + threadInfo: updatedThreadInfo, + }, + { + type: updateTypes.UPDATE_THREAD_READ_STATUS, + id: uuid.v4(), + time, + threadID, + unread: true, + }, + ); + } + return { + rawMessageInfos: [removeMembersMessage], + updateInfos, + }; + }, + }); + +export { removeMembersSpec }; diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js index ad5688b7e..1abdd5bf7 100644 --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -1,262 +1,283 @@ // @flow 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 { 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 { 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', JOIN_THREAD: 'join_thread', LEAVE_THREAD: 'leave_thread', + REMOVE_MEMBERS: 'remove_members', }); export type DMOperationType = $Values; export type CreateThickRawThreadInfoInput = { +threadID: string, +threadType: ThickThreadType, +creationTime: number, +parentThreadID?: ?string, +allMemberIDs: $ReadOnlyArray, +roleID: string, +creatorID: string, +name?: ?string, +avatar?: ?ClientAvatar, +description?: ?string, +color?: ?string, +containingThreadID?: ?string, +community?: ?string, +sourceMessageID?: ?string, +repliesCount?: ?number, +pinnedCount?: ?number, }; export const createThickRawThreadInfoInputValidator: TInterface = tShape({ threadID: t.String, threadType: thickThreadTypeValidator, creationTime: t.Number, parentThreadID: t.maybe(t.String), allMemberIDs: t.list(tUserID), roleID: t.String, creatorID: tUserID, name: t.maybe(t.String), avatar: t.maybe(clientAvatarValidator), description: t.maybe(t.String), color: t.maybe(t.String), containingThreadID: t.maybe(t.String), community: 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, }); export type DMAddMembersOperation = { +type: 'add_members', +editorID: string, +time: number, +messageID: string, +addedUserIDs: $ReadOnlyArray, +existingThreadDetails: CreateThickRawThreadInfoInput, }; export const dmAddMembersOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_MEMBERS), editorID: tUserID, time: t.Number, messageID: t.String, addedUserIDs: t.list(tUserID), existingThreadDetails: createThickRawThreadInfoInputValidator, }); export type DMJoinThreadOperation = { +type: 'join_thread', +editorID: string, +time: number, +messageID: string, +existingThreadDetails: CreateThickRawThreadInfoInput, }; export const dmJoinThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.JOIN_THREAD), editorID: 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 DMOperation = | DMCreateThreadOperation | DMCreateSidebarOperation | DMSendTextMessageOperation | DMSendReactionMessageOperation | DMSendEditMessageOperation | DMAddMembersOperation | DMJoinThreadOperation - | DMLeaveThreadOperation; + | DMLeaveThreadOperation + | DMRemoveMembersOperation; export const dmOperationValidator: TUnion = t.union([ dmCreateThreadOperationValidator, dmCreateSidebarOperationValidator, dmSendTextMessageOperationValidator, dmSendReactionMessageOperationValidator, dmSendEditMessageOperationValidator, dmAddMembersOperationValidator, dmJoinThreadOperationValidator, dmLeaveThreadOperationValidator, + dmRemoveMembersOperationValidator, ]); export type DMOperationResult = { rawMessageInfos: Array, updateInfos: Array, }; export const processDMOpsActionType = 'PROCESS_DM_OPS'; export type ProcessDMOpsPayload = { +rawMessageInfos: $ReadOnlyArray, +updateInfos: $ReadOnlyArray, }; export const scheduleP2PMessagesActionType = 'SCHEDULE_P2P_MESSAGES'; export type ScheduleP2PMessagesPayload = { +dmOpID: string, +messages: $ReadOnlyArray, };