diff --git a/lib/shared/dm-ops/create-sidebar-spec.js b/lib/shared/dm-ops/create-sidebar-spec.js new file mode 100644 index 000000000..185803a05 --- /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 index 6b776d480..7431be99b 100644 --- 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 index cd88e2160..1df0e52ad 100644 --- a/lib/shared/dm-ops/dm-op-specs.js +++ b/lib/shared/dm-ops/dm-op-specs.js @@ -1,13 +1,15 @@ // @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'; 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, }); diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js index 84867e583..62b851c40 100644 --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -1,63 +1,96 @@ // @flow import type { TInterface } from 'tcomb'; 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; export type DMCreateThreadOperation = { +type: 'create_thread', +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({ type: tString(dmOperationTypes.CREATE_THREAD), 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 = { +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 DMOperation = DMCreateThreadOperation | DMSendTextMessageOperation; +export type DMOperation = + | DMCreateThreadOperation + | DMCreateSidebarOperation + | DMSendTextMessageOperation; export type DMOperationResult = { rawMessageInfos: Array, updateInfos: Array, }; diff --git a/lib/types/thread-types-enum.js b/lib/types/thread-types-enum.js index e250c2279..4c7a5a0a2 100644 --- a/lib/types/thread-types-enum.js +++ b/lib/types/thread-types-enum.js @@ -1,147 +1,161 @@ // @flow import invariant from 'invariant'; import type { TRefinement } from 'tcomb'; import { values } from '../utils/objects.js'; import { tNumEnum } from '../utils/validation-utils.js'; export const thinThreadTypes = Object.freeze({ //OPEN: 0, (DEPRECATED) //CLOSED: 1, (DEPRECATED) //SECRET: 2, (DEPRECATED) // has parent, not top-level (appears under parent in inbox), and visible to // all members of parent SIDEBAR: 5, // canonical thread for each pair of users. represents the friendship // created under GENESIS. being deprecated in favor of PERSONAL GENESIS_PERSONAL: 6, // canonical thread for each single user // created under GENESIS. being deprecated in favor of PRIVATE GENESIS_PRIVATE: 7, // aka "org". no parent, top-level, has admin COMMUNITY_ROOT: 8, // like COMMUNITY_ROOT, but members aren't voiced COMMUNITY_ANNOUNCEMENT_ROOT: 9, // an open subthread. has parent, top-level (not sidebar), and visible to all // members of parent. root ancestor is a COMMUNITY_ROOT COMMUNITY_OPEN_SUBTHREAD: 3, // like COMMUNITY_SECRET_SUBTHREAD, but members aren't voiced COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD: 10, // a secret subthread. optional parent, top-level (not sidebar), visible only // to its members. root ancestor is a COMMUNITY_ROOT COMMUNITY_SECRET_SUBTHREAD: 4, // like COMMUNITY_SECRET_SUBTHREAD, but members aren't voiced COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD: 11, // like COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, but you can't leave GENESIS: 12, }); 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, // canonical thread for each pair of users. represents the friendship 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; export const threadTypes = Object.freeze({ ...thinThreadTypes, ...thickThreadTypes, }); const thickThreadTypesSet = new Set(Object.values(thickThreadTypes)); export function threadTypeIsThick(threadType: ThreadType): boolean { return thickThreadTypesSet.has(threadType); } export function assertThinThreadType(threadType: number): ThinThreadType { invariant( threadType === 3 || threadType === 4 || threadType === 5 || threadType === 6 || threadType === 7 || threadType === 8 || threadType === 9 || threadType === 10 || threadType === 11 || threadType === 12, 'number is not ThinThreadType enum', ); return threadType; } export function assertThickThreadType(threadType: number): ThickThreadType { invariant( threadType === 13 || threadType === 14 || threadType === 15 || threadType === 16, 'number is not ThickThreadType enum', ); return threadType; } export function assertThreadType(threadType: number): ThreadType { invariant( threadType === 3 || threadType === 4 || threadType === 5 || threadType === 6 || threadType === 7 || threadType === 8 || threadType === 9 || threadType === 10 || threadType === 11 || threadType === 12 || threadType === 13 || threadType === 14 || threadType === 15 || threadType === 16, 'number is not ThreadType enum', ); return threadType; } export const threadTypeValidator: TRefinement = tNumEnum( values(threadTypes), ); export const communityThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.COMMUNITY_ROOT, threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, threadTypes.GENESIS, ]); export const announcementThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, ]); export const communitySubthreads: $ReadOnlyArray = Object.freeze([ threadTypes.COMMUNITY_OPEN_SUBTHREAD, threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, threadTypes.COMMUNITY_SECRET_SUBTHREAD, threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, ]); export function threadTypeIsCommunityRoot(threadType: ThreadType): boolean { return communityThreadTypes.includes(threadType); } export function threadTypeIsAnnouncementThread( threadType: ThreadType, ): boolean { return announcementThreadTypes.includes(threadType); }