diff --git a/lib/shared/dm-ops/add-members-spec.js b/lib/shared/dm-ops/add-members-spec.js new file mode 100644 --- /dev/null +++ b/lib/shared/dm-ops/add-members-spec.js @@ -0,0 +1,117 @@ +// @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 { ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; +import { minimallyEncodeMemberInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; +import { joinThreadSubscription } from '../../types/subscription-types.js'; +import type { ThickMemberInfo } from '../../types/thread-types.js'; +import { updateTypes } from '../../types/update-types-enum.js'; +import type { ClientUpdateInfo } from '../../types/update-types.js'; +import { values } from '../../utils/objects.js'; +import { roleIsDefaultRole, userIsMember } from '../thread-utils.js'; + +const addMembersSpec: DMOperationSpec = Object.freeze({ + processDMOperation: async ( + dmOperation: DMAddMembersOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) => { + const { + editorID, + time, + messageID, + addedUserIDs, + threadInfo, + rawMessageInfos, + truncationStatus, + rawEntryInfos, + } = dmOperation; + const addMembersMessage = { + type: messageTypes.ADD_MEMBERS, + id: messageID, + threadID: threadInfo.id, + creatorID: editorID, + time, + addedUserIDs: [...addedUserIDs], + }; + + const viewerIsAdded = addedUserIDs.includes(viewerID); + const updateInfos: Array = []; + if (viewerIsAdded) { + updateInfos.push( + { + type: updateTypes.JOIN_THREAD, + id: uuid.v4(), + time, + threadInfo, + rawMessageInfos, + truncationStatus, + rawEntryInfos, + }, + { + type: updateTypes.UPDATE_THREAD_READ_STATUS, + id: uuid.v4(), + time, + threadID: threadInfo.id, + unread: true, + }, + ); + } else { + const currentThreadInfoOptional = utilities.getThreadInfo(threadInfo.id); + if (!currentThreadInfoOptional || !currentThreadInfoOptional.thick) { + // We can't perform this operation now. It should be queued for later. + return { + rawMessageInfos: [], + updateInfos: [], + }; + } + const currentThreadInfo: ThickRawThreadInfo = currentThreadInfoOptional; + const defaultRoleID = values(currentThreadInfo.roles).find(role => + roleIsDefaultRole(role), + )?.id; + invariant(defaultRoleID, 'Default role ID must exist'); + const { membershipPermissions } = createRoleAndPermissionForThickThreads( + currentThreadInfo.type, + currentThreadInfo.id, + defaultRoleID, + ); + const newMembers = addedUserIDs + .filter(userID => !userIsMember(currentThreadInfo, userID)) + .map(userID => + minimallyEncodeMemberInfo({ + id: userID, + role: defaultRoleID, + permissions: membershipPermissions, + isSender: editorID === viewerID, + subscription: joinThreadSubscription, + }), + ); + + const newThreadInfo = { + ...currentThreadInfo, + members: [...currentThreadInfo.members, ...newMembers], + }; + updateInfos.push({ + type: updateTypes.UPDATE_THREAD, + id: uuid.v4(), + time, + threadInfo: newThreadInfo, + }); + } + return { + rawMessageInfos: [addMembersMessage], + updateInfos, + }; + }, +}); + +export { addMembersSpec }; diff --git a/lib/shared/dm-ops/create-thread-spec.js b/lib/shared/dm-ops/create-thread-spec.js --- a/lib/shared/dm-ops/create-thread-spec.js +++ b/lib/shared/dm-ops/create-thread-spec.js @@ -9,7 +9,6 @@ makePermissionsBlob, getThickThreadRolePermissionsBlob, } from '../../permissions/thread-permissions.js'; -import { generatePendingThreadColor } from '../../shared/color-utils.js'; import type { DMCreateThreadOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { @@ -24,9 +23,11 @@ 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'; type CreateThickRawThreadInfoInput = { +threadID: string, @@ -38,6 +39,32 @@ +creatorID: string, +viewerID: string, }; + +function createRoleAndPermissionForThickThreads( + threadType: ThickThreadType, + threadID: string, + roleID: string, +): { +role: RoleInfo, +membershipPermissions: ThreadPermissionsInfo } { + const rolePermissions = getThickThreadRolePermissionsBlob(threadType); + const membershipPermissions = getAllThreadPermissions( + makePermissionsBlob(rolePermissions, null, threadID, threadType), + threadID, + ); + const role: RoleInfo = { + ...minimallyEncodeRoleInfo({ + id: roleID, + name: 'Members', + permissions: rolePermissions, + isDefault: true, + }), + specialRole: specialRoles.DEFAULT_ROLE, + }; + return { + membershipPermissions, + role, + }; +} + type MutableThickRawThreadInfo = { ...ThickRawThreadInfo }; function createThickRawThreadInfo( input: CreateThickRawThreadInfoInput, @@ -55,20 +82,8 @@ const color = generatePendingThreadColor(allMemberIDs); - const rolePermissions = getThickThreadRolePermissionsBlob(threadType); - const membershipPermissions = getAllThreadPermissions( - makePermissionsBlob(rolePermissions, null, threadID, threadType), - threadID, - ); - const role: RoleInfo = { - ...minimallyEncodeRoleInfo({ - id: roleID, - name: 'Members', - permissions: rolePermissions, - isDefault: true, - }), - specialRole: specialRoles.DEFAULT_ROLE, - }; + const { membershipPermissions, role } = + createRoleAndPermissionForThickThreads(threadType, threadID, roleID); return { thick: true, @@ -159,4 +174,8 @@ }, }); -export { createThickRawThreadInfo, createThreadSpec }; +export { + createThickRawThreadInfo, + createThreadSpec, + createRoleAndPermissionForThickThreads, +}; diff --git a/lib/shared/dm-ops/dm-op-spec.js b/lib/shared/dm-ops/dm-op-spec.js --- a/lib/shared/dm-ops/dm-op-spec.js +++ b/lib/shared/dm-ops/dm-op-spec.js @@ -2,10 +2,12 @@ import type { DMOperation, DMOperationResult } from '../../types/dm-ops.js'; import type { RawMessageInfo } from '../../types/message-types.js'; +import type { RawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; export type ProcessDMOperationUtilities = { // Needed to fetch sidebar source messages +fetchMessage: (messageID: string) => Promise, + +getThreadInfo: (threadID: string) => ?RawThreadInfo, }; export type DMOperationSpec = { diff --git a/lib/shared/dm-ops/dm-op-specs.js b/lib/shared/dm-ops/dm-op-specs.js --- a/lib/shared/dm-ops/dm-op-specs.js +++ b/lib/shared/dm-ops/dm-op-specs.js @@ -1,5 +1,6 @@ // @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'; @@ -16,4 +17,5 @@ [dmOperationTypes.SEND_TEXT_MESSAGE]: sendTextMessageSpec, [dmOperationTypes.SEND_REACTION_MESSAGE]: sendReactionMessageSpec, [dmOperationTypes.SEND_EDIT_MESSAGE]: sendEditMessageSpec, + [dmOperationTypes.ADD_MEMBERS]: addMembersSpec, }); diff --git a/lib/shared/dm-ops/process-dm-ops.js b/lib/shared/dm-ops/process-dm-ops.js --- a/lib/shared/dm-ops/process-dm-ops.js +++ b/lib/shared/dm-ops/process-dm-ops.js @@ -9,15 +9,23 @@ type DMOperation, processDMOpsActionType, } from '../../types/dm-ops.js'; -import { useDispatch } from '../../utils/redux-utils.js'; +import { useDispatch, useSelector } from '../../utils/redux-utils.js'; function useProcessDMOperation(): (dmOp: DMOperation) => Promise { const fetchMessage = useGetLatestMessageEdit(); + const threadInfos = useSelector(state => state.threadStore.threadInfos); + + const getThreadInfo = React.useCallback( + (id: string) => threadInfos[id], + [threadInfos], + ); + const utilities = React.useMemo( () => ({ fetchMessage, + getThreadInfo, }), - [fetchMessage], + [fetchMessage, getThreadInfo], ); const dispatch = useDispatch(); diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -1,14 +1,23 @@ // @flow -import type { TInterface, TUnion } from 'tcomb'; -import t from 'tcomb'; +import t, { type TInterface, type TUnion } from 'tcomb'; -import type { RawMessageInfo } from './message-types.js'; +import { type RawEntryInfo, rawEntryInfoValidator } from './entry-types.js'; +import type { + MessageTruncationStatus, + RawMessageInfo, +} from './message-types.js'; +import { + messageTruncationStatusValidator, + rawMessageInfoValidator, +} from './message-types.js'; +import type { ThickRawThreadInfo } from './minimally-encoded-thread-permissions-types.js'; import { type NonSidebarThickThreadType, nonSidebarThickThreadTypes, } from './thread-types-enum.js'; import type { ClientUpdateInfo } from './update-types.js'; +import { threadInfoValidator } from '../permissions/minimally-encoded-thread-permissions-validators.js'; import { values } from '../utils/objects.js'; import { tShape, tString, tUserID } from '../utils/validation-utils.js'; @@ -18,6 +27,7 @@ SEND_TEXT_MESSAGE: 'send_text_message', SEND_REACTION_MESSAGE: 'send_reaction_message', SEND_EDIT_MESSAGE: 'send_edit_message', + ADD_MEMBERS: 'add_members', }); export type DMOperationType = $Values; @@ -129,18 +139,44 @@ text: t.String, }); +export type DMAddMembersOperation = { + +type: 'add_members', + +editorID: string, + +time: number, + +messageID: string, + +addedUserIDs: $ReadOnlyArray, + +threadInfo: ThickRawThreadInfo, + +rawMessageInfos: $ReadOnlyArray, + +truncationStatus: MessageTruncationStatus, + +rawEntryInfos: $ReadOnlyArray, +}; +export const dmAddMembersOperationValidator: TInterface = + tShape({ + type: tString(dmOperationTypes.ADD_MEMBERS), + editorID: tUserID, + time: t.Number, + messageID: t.String, + addedUserIDs: t.list(tUserID), + threadInfo: threadInfoValidator, + rawMessageInfos: t.list(rawMessageInfoValidator), + truncationStatus: messageTruncationStatusValidator, + rawEntryInfos: t.list(rawEntryInfoValidator), + }); + export type DMOperation = | DMCreateThreadOperation | DMCreateSidebarOperation | DMSendTextMessageOperation | DMSendReactionMessageOperation - | DMSendEditMessageOperation; + | DMSendEditMessageOperation + | DMAddMembersOperation; export const dmOperationValidator: TUnion = t.union([ dmCreateThreadOperationValidator, dmCreateSidebarOperationValidator, dmSendTextMessageOperationValidator, dmSendReactionMessageOperationValidator, dmSendEditMessageOperationValidator, + dmAddMembersOperationValidator, ]); export type DMOperationResult = {