diff --git a/lib/shared/dm-ops/create-sidebar-spec.js b/lib/shared/dm-ops/create-sidebar-spec.js index 52bd287fe..fd7acd46d 100644 --- a/lib/shared/dm-ops/create-sidebar-spec.js +++ b/lib/shared/dm-ops/create-sidebar-spec.js @@ -1,108 +1,109 @@ // @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'; +import { isInvalidSidebarSource } from '../message-utils.js'; 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, + const rawThreadInfo = createThickRawThreadInfo( + { + threadID, + threadType: threadTypes.THICK_SIDEBAR, + creationTime: time, + parentThreadID, + allMemberIDs, + roleID, + creatorID, + sourceMessageID, + containingThreadID: parentThreadID, + }, 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.EXHAUSTIVE, rawEntryInfos: [], }; return { rawMessageInfos: [], // included in updateInfos below updateInfos: [threadJoinUpdateInfo], }; }, }); export { createSidebarSpec }; diff --git a/lib/shared/dm-ops/create-thread-spec.js b/lib/shared/dm-ops/create-thread-spec.js index 47dea4fb5..d0daba92c 100644 --- a/lib/shared/dm-ops/create-thread-spec.js +++ b/lib/shared/dm-ops/create-thread-spec.js @@ -1,162 +1,177 @@ // @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 type { + CreateThickRawThreadInfoInput, + 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 { joinThreadSubscription } 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'; +import { generatePendingThreadColor } from '../color-utils.js'; -type CreateThickRawThreadInfoInput = { - +threadID: string, - +threadType: ThickThreadType, - +creationTime: number, - +parentThreadID?: ?string, - +allMemberIDs: $ReadOnlyArray, - +roleID: string, - +creatorID: string, - +viewerID: string, -}; type MutableThickRawThreadInfo = { ...ThickRawThreadInfo }; function createThickRawThreadInfo( input: CreateThickRawThreadInfoInput, + viewerID: string, ): MutableThickRawThreadInfo { const { threadID, threadType, creationTime, parentThreadID, allMemberIDs, roleID, creatorID, - viewerID, + name, + avatar, + description, + color, + containingThreadID, + community, + sourceMessageID, + repliesCount, + pinnedCount, } = input; - const color = generatePendingThreadColor(allMemberIDs); + const threadColor = 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 { + const newThread: MutableThickRawThreadInfo = { thick: true, minimallyEncoded: true, id: threadID, type: threadType, - color, + color: threadColor, creationTime, parentThreadID, members: allMemberIDs.map(memberID => minimallyEncodeMemberInfo({ id: memberID, role: role.id, permissions: membershipPermissions, isSender: memberID === viewerID, subscription: joinThreadSubscription, }), ), roles: { [role.id]: role, }, currentUser: minimallyEncodeThreadCurrentUserInfo({ role: role.id, permissions: membershipPermissions, subscription: joinThreadSubscription, unread: creatorID !== viewerID, }), - repliesCount: 0, + repliesCount: repliesCount ?? 0, + name, + avatar, + description, + containingThreadID, + community, }; + if (sourceMessageID) { + newThread.sourceMessageID = sourceMessageID; + } + if (pinnedCount) { + newThread.pinnedCount = pinnedCount; + } + return newThread; } const createThreadSpec: DMOperationSpec = Object.freeze({ 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, + 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.EXHAUSTIVE, rawEntryInfos: [], }; return { rawMessageInfos: [], // included in updateInfos below updateInfos: [threadJoinUpdateInfo], }; }, }); export { createThickRawThreadInfo, createThreadSpec }; diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js index 92f1646a0..cdb78a14f 100644 --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -1,163 +1,204 @@ // @flow import type { TInterface, TUnion } from 'tcomb'; import t 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', }); 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 DMOperation = | DMCreateThreadOperation | DMCreateSidebarOperation | DMSendTextMessageOperation | DMSendReactionMessageOperation | DMSendEditMessageOperation; export const dmOperationValidator: TUnion = t.union([ dmCreateThreadOperationValidator, dmCreateSidebarOperationValidator, dmSendTextMessageOperationValidator, dmSendReactionMessageOperationValidator, dmSendEditMessageOperationValidator, ]); 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, }; diff --git a/lib/types/thread-types-enum.js b/lib/types/thread-types-enum.js index 79edd6d4b..6490b1d6d 100644 --- a/lib/types/thread-types-enum.js +++ b/lib/types/thread-types-enum.js @@ -1,170 +1,173 @@ // @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 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 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 const thickThreadTypeValidator: TRefinement = tNumEnum( + values(thickThreadTypes), +); 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 const sidebarThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.SIDEBAR, threadTypes.THICK_SIDEBAR, ]); export function threadTypeIsCommunityRoot(threadType: ThreadType): boolean { return communityThreadTypes.includes(threadType); } export function threadTypeIsAnnouncementThread( threadType: ThreadType, ): boolean { return announcementThreadTypes.includes(threadType); } export function threadTypeIsSidebar(threadType: ThreadType): boolean { return sidebarThreadTypes.includes(threadType); }