diff --git a/lib/shared/dm-ops/create-sidebar-spec.js b/lib/shared/dm-ops/create-sidebar-spec.js new file mode 100644 --- /dev/null +++ b/lib/shared/dm-ops/create-sidebar-spec.js @@ -0,0 +1,106 @@ +// @flow + +import uuid from 'uuid'; + +import { createThickRawThreadInfo } from './create-thread-spec.js'; +import type { + DMOperationSpec, + ProcessDMOperationUtilities, +} from './dm-op-spec.js'; +import { isInvalidSidebarSource } from '../../shared/message-utils.js'; +import type { DMCreateSidebarOperation } from '../../types/dm-ops.js'; +import { messageTypes } from '../../types/message-types-enum.js'; +import { + type RawMessageInfo, + messageTruncationStatus, +} from '../../types/message-types.js'; +import { threadTypes } from '../../types/thread-types-enum.js'; +import { updateTypes } from '../../types/update-types-enum.js'; + +export const createSidebarSpec: DMOperationSpec = + Object.freeze({ + processDMOperation: async ( + dmOperation: DMCreateSidebarOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) => { + const { + threadID, + creatorID, + time, + parentThreadID, + memberIDs, + sourceMessageID, + roleID, + newSidebarSourceMessageID, + newCreateSidebarMessageID, + } = dmOperation; + const allMemberIDs = [creatorID, ...memberIDs]; + + const rawThreadInfo = createThickRawThreadInfo({ + threadID, + threadType: threadTypes.THICK_SIDEBAR, + creationTime: time, + parentThreadID, + allMemberIDs, + roleID, + creatorID, + viewerID, + }); + + rawThreadInfo.sourceMessageID = sourceMessageID; + rawThreadInfo.containingThreadID = parentThreadID; + + 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 rawMessageInfos: Array = [ + { + type: messageTypes.SIDEBAR_SOURCE, + id: newSidebarSourceMessageID, + threadID, + creatorID, + time, + sourceMessage, + }, + { + type: messageTypes.CREATE_SIDEBAR, + id: newCreateSidebarMessageID, + threadID, + creatorID, + time: time + 1, + sourceMessageAuthorID: sourceMessage.creatorID, + initialThreadState: { + parentThreadID, + color: rawThreadInfo.color, + memberIDs: allMemberIDs, + }, + }, + ]; + + const threadJoinUpdateInfo = { + type: updateTypes.JOIN_THREAD, + id: uuid.v4(), + time, + threadInfo: rawThreadInfo, + rawMessageInfos, + truncationStatus: messageTruncationStatus.UNCHANGED, + rawEntryInfos: [], + }; + + return { + rawMessageInfos: [], // included in updateInfos below + updateInfos: [threadJoinUpdateInfo], + }; + }, + }); 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 @@ -1,14 +1,160 @@ // @flow +import uuid from 'uuid'; + import type { DMOperationSpec } from './dm-op-spec.js'; +import { specialRoles } from '../../permissions/special-roles.js'; +import { + getAllThreadPermissions, + makePermissionsBlob, + getThickThreadRolePermissionsBlob, +} from '../../permissions/thread-permissions.js'; +import { generatePendingThreadColor } from '../../shared/color-utils.js'; import type { DMCreateThreadOperation } from '../../types/dm-ops.js'; +import { messageTypes } from '../../types/message-types-enum.js'; +import { + type RawMessageInfo, + messageTruncationStatus, +} from '../../types/message-types.js'; +import { + type ThickRawThreadInfo, + type RoleInfo, + minimallyEncodeMemberInfo, + minimallyEncodeRoleInfo, + minimallyEncodeThreadCurrentUserInfo, +} from '../../types/minimally-encoded-thread-permissions-types.js'; +import { defaultThreadSubscription } from '../../types/subscription-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'; + +type CreateThickRawThreadInfoInput = { + +threadID: string, + +threadType: ThickThreadType, + +creationTime: number, + +parentThreadID?: ?string, + +allMemberIDs: $ReadOnlyArray, + +roleID: string, + +creatorID: string, + +viewerID: string, +}; +type MutableThickRawThreadInfo = { ...ThickRawThreadInfo }; +export function createThickRawThreadInfo( + input: CreateThickRawThreadInfoInput, +): MutableThickRawThreadInfo { + const { + threadID, + threadType, + creationTime, + parentThreadID, + allMemberIDs, + roleID, + creatorID, + viewerID, + } = input; + + 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, + }; + + return { + thick: true, + minimallyEncoded: true, + id: threadID, + type: threadType, + color, + creationTime, + parentThreadID, + members: allMemberIDs.map(memberID => + minimallyEncodeMemberInfo({ + id: memberID, + role: role.id, + permissions: membershipPermissions, + isSender: memberID === viewerID, + subscription: defaultThreadSubscription, + }), + ), + roles: { + [role.id]: role, + }, + currentUser: minimallyEncodeThreadCurrentUserInfo({ + role: role.id, + permissions: membershipPermissions, + subscription: defaultThreadSubscription, + unread: creatorID !== viewerID, + }), + repliesCount: 0, + }; +} export const createThreadSpec: DMOperationSpec = Object.freeze({ - processDMOperation: async () => { + processDMOperation: async ( + dmOperation: DMCreateThreadOperation, + viewerID: string, + ) => { + const { + threadID, + creatorID, + time, + threadType, + memberIDs, + roleID, + newMessageID, + } = dmOperation; + const allMemberIDs = [creatorID, ...memberIDs]; + + const rawThreadInfo = createThickRawThreadInfo({ + threadID, + threadType, + creationTime: time, + allMemberIDs, + roleID, + creatorID, + viewerID, + }); + + const rawMessageInfos: Array = [ + { + type: messageTypes.CREATE_THREAD, + id: newMessageID, + threadID, + creatorID, + time, + initialThreadState: { + type: threadType, + color: rawThreadInfo.color, + memberIDs: allMemberIDs, + }, + }, + ]; + + const threadJoinUpdateInfo = { + type: updateTypes.JOIN_THREAD, + id: uuid.v4(), + time, + threadInfo: rawThreadInfo, + rawMessageInfos, + truncationStatus: messageTruncationStatus.UNCHANGED, + rawEntryInfos: [], + }; + return { - rawMessageInfos: [], - updateInfos: [], + rawMessageInfos: [], // included in updateInfos below + updateInfos: [threadJoinUpdateInfo], }; }, }); 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 { createSidebarSpec } from './create-sidebar-spec.js'; import { createThreadSpec } from './create-thread-spec.js'; import type { DMOperationSpec } from './dm-op-spec.js'; import { sendTextMessageSpec } from './send-text-message-spec.js'; @@ -9,5 +10,6 @@ +[DMOperationType]: DMOperationSpec, } = Object.freeze({ [dmOperationTypes.CREATE_THREAD]: createThreadSpec, + [dmOperationTypes.CREATE_SIDEBAR]: createSidebarSpec, [dmOperationTypes.SEND_TEXT_MESSAGE]: sendTextMessageSpec, }); 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 @@ -4,13 +4,17 @@ import t from 'tcomb'; import type { RawMessageInfo } from './message-types.js'; -import { type ThickThreadType, thickThreadTypes } from './thread-types-enum.js'; +import { + type NonSidebarThickThreadType, + nonSidebarThickThreadTypes, +} 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', }); export type DMOperationType = $Values; @@ -20,10 +24,10 @@ +threadID: string, +creatorID: string, +time: number, - +threadType: ThickThreadType, - +parentThreadID?: ?string, + +threadType: NonSidebarThickThreadType, +memberIDs: $ReadOnlyArray, - +sourceMessageID?: ?string, + +roleID: string, + +newMessageID: string, }; export const dmCreateThreadOperationValidator: TInterface = tShape({ @@ -31,10 +35,36 @@ threadID: t.String, creatorID: tUserID, time: t.Number, - threadType: t.enums.of(values(thickThreadTypes)), - parentThreadID: t.maybe(t.String), + threadType: t.enums.of(values(nonSidebarThickThreadTypes)), memberIDs: t.list(tUserID), - sourceMessageID: t.maybe(t.String), + 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 = { @@ -55,7 +85,10 @@ text: t.String, }); -export type DMOperation = DMCreateThreadOperation | DMSendTextMessageOperation; +export type DMOperation = + | DMCreateThreadOperation + | DMCreateSidebarOperation + | DMSendTextMessageOperation; export type DMOperationResult = { rawMessageInfos: Array, diff --git a/lib/types/thread-types-enum.js b/lib/types/thread-types-enum.js --- a/lib/types/thread-types-enum.js +++ b/lib/types/thread-types-enum.js @@ -38,7 +38,7 @@ }); export type ThinThreadType = $Values; -export const thickThreadTypes = Object.freeze({ +export const nonSidebarThickThreadTypes = Object.freeze({ // local "thick" thread (outside of community). no parent, can only have // sidebar children LOCAL: 13, @@ -46,11 +46,25 @@ PERSONAL: 14, // canonical thread for each single user PRIVATE: 15, +}); +export type NonSidebarThickThreadType = $Values< + typeof nonSidebarThickThreadTypes, +>; + +export const sidebarThickThreadTypes = Object.freeze({ // has parent, not top-level (appears under parent in inbox), and visible to // all members of parent THICK_SIDEBAR: 16, }); -export type ThickThreadType = $Values; +export type SidebarThickThreadType = $Values; + +export const thickThreadTypes = Object.freeze({ + ...nonSidebarThickThreadTypes, + ...sidebarThickThreadTypes, +}); +export type ThickThreadType = + | NonSidebarThickThreadType + | SidebarThickThreadType; export type ThreadType = ThinThreadType | ThickThreadType;