diff --git a/lib/shared/dm-ops/create-sidebar-spec.js b/lib/shared/dm-ops/create-sidebar-spec.js index eb1852d56..f9714d70d 100644 --- a/lib/shared/dm-ops/create-sidebar-spec.js +++ b/lib/shared/dm-ops/create-sidebar-spec.js @@ -1,168 +1,168 @@ // @flow import uuid from 'uuid'; import { createThickRawThreadInfo } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.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 { generatePendingThreadColor } from '../color-utils.js'; import { isInvalidSidebarSource, rawMessageInfoFromMessageData, } from '../message-utils.js'; async function createMessageDatasFromDMOperation( dmOperation: DMCreateSidebarOperation, utilities: ProcessDMOperationUtilities, threadColor?: string, ) { const { threadID, creatorID, time, parentThreadID, memberIDs, sourceMessageID, } = dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; const color = threadColor ?? generatePendingThreadColor(allMemberIDs); 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 sidebarSourceMessageData = { type: messageTypes.SIDEBAR_SOURCE, threadID, creatorID, time, sourceMessage: sourceMessage, }; const createSidebarMessageData = { type: messageTypes.CREATE_SIDEBAR, threadID, creatorID, time: time + 1, sourceMessageAuthorID: sourceMessage.creatorID, initialThreadState: { parentThreadID, color, memberIDs: allMemberIDs, }, }; return { sidebarSourceMessageData, createSidebarMessageData, }; } const createSidebarSpec: DMOperationSpec = Object.freeze({ notificationsCreationData: async ( dmOperation: DMCreateSidebarOperation, utilities: ProcessDMOperationUtilities, ) => { const { sidebarSourceMessageData, createSidebarMessageData } = await createMessageDatasFromDMOperation(dmOperation, utilities); return { messageDatas: [sidebarSourceMessageData, createSidebarMessageData], }; }, 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, + unread: creatorID !== viewerID, sourceMessageID, containingThreadID: parentThreadID, }, viewerID, ); const { sidebarSourceMessageData, createSidebarMessageData } = await createMessageDatasFromDMOperation( dmOperation, utilities, rawThreadInfo.color, ); const sidebarSourceMessageInfo = rawMessageInfoFromMessageData( sidebarSourceMessageData, newSidebarSourceMessageID, ); const createSidebarMessageInfo = rawMessageInfoFromMessageData( createSidebarMessageData, newCreateSidebarMessageID, ); const rawMessageInfos: Array = [ sidebarSourceMessageInfo, createSidebarMessageInfo, ]; 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], }; }, canBeProcessed() { return { isProcessingPossible: true }; }, supportsAutoRetry: true, }); export { createSidebarSpec }; diff --git a/lib/shared/dm-ops/create-thread-spec.js b/lib/shared/dm-ops/create-thread-spec.js index 208607eaa..7723e5da5 100644 --- a/lib/shared/dm-ops/create-thread-spec.js +++ b/lib/shared/dm-ops/create-thread-spec.js @@ -1,209 +1,209 @@ // @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 type { CreateThickRawThreadInfoInput, DMCreateThreadOperation, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { 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 { 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'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; 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, viewerID: string, ): MutableThickRawThreadInfo { const { threadID, threadType, creationTime, parentThreadID, allMemberIDs, roleID, - creatorID, + unread, name, avatar, description, color, containingThreadID, sourceMessageID, repliesCount, pinnedCount, } = input; const threadColor = color ?? generatePendingThreadColor(allMemberIDs); const { membershipPermissions, role } = createRoleAndPermissionForThickThreads(threadType, threadID, roleID); const newThread: MutableThickRawThreadInfo = { thick: true, minimallyEncoded: true, id: threadID, type: threadType, 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, + unread, }), repliesCount: repliesCount ?? 0, name, avatar, description, containingThreadID, }; if (sourceMessageID) { newThread.sourceMessageID = sourceMessageID; } if (pinnedCount) { newThread.pinnedCount = pinnedCount; } return newThread; } function createMessageDataFromDMOperation( dmOperation: DMCreateThreadOperation, ) { const { threadID, creatorID, time, threadType, memberIDs } = dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; const color = generatePendingThreadColor(allMemberIDs); return { type: messageTypes.CREATE_THREAD, threadID, creatorID, time, initialThreadState: { type: threadType, color, memberIDs: allMemberIDs, }, }; } const createThreadSpec: DMOperationSpec = Object.freeze({ notificationsCreationData: async (dmOperation: DMCreateThreadOperation) => { const messageData = createMessageDataFromDMOperation(dmOperation); return { messageDatas: [messageData] }; }, 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, + unread: creatorID !== viewerID, }, viewerID, ); const messageData = createMessageDataFromDMOperation(dmOperation); const rawMessageInfos = [ rawMessageInfoFromMessageData(messageData, newMessageID), ]; 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], }; }, canBeProcessed() { return { isProcessingPossible: true }; }, supportsAutoRetry: true, }); export { createThickRawThreadInfo, createThreadSpec, createRoleAndPermissionForThickThreads, }; diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js index 54d362a73..c7015e343 100644 --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -1,409 +1,409 @@ // @flow import t, { type TInterface, type TUnion, type TStructProps } from 'tcomb'; import { clientAvatarValidator, type ClientAvatar } from './avatar-types.js'; import type { RawMessageInfo } from './message-types.js'; import type { NotificationsCreationData } from './notif-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 { tColor, 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', ADD_VIEWER_TO_THREAD_MEMBERS: 'add_viewer_to_thread_members', JOIN_THREAD: 'join_thread', LEAVE_THREAD: 'leave_thread', REMOVE_MEMBERS: 'remove_members', CHANGE_THREAD_SETTINGS: 'change_thread_settings', CHANGE_THREAD_SETTINGS_AND_ADD_VIEWER: 'change_thread_settings_and_add_viewer', }); export type DMOperationType = $Values; export type CreateThickRawThreadInfoInput = { +threadID: string, +threadType: ThickThreadType, +creationTime: number, +parentThreadID?: ?string, +allMemberIDs: $ReadOnlyArray, +roleID: string, - +creatorID: string, + +unread: boolean, +name?: ?string, +avatar?: ?ClientAvatar, +description?: ?string, +color?: ?string, +containingThreadID?: ?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, + unread: t.Boolean, name: t.maybe(t.String), avatar: t.maybe(clientAvatarValidator), description: t.maybe(t.String), color: t.maybe(t.String), containingThreadID: 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, }); type DMAddMembersBase = { +editorID: string, +time: number, +messageID: string, +addedUserIDs: $ReadOnlyArray, }; const dmAddMembersBaseValidatorShape = { editorID: tUserID, time: t.Number, messageID: t.String, addedUserIDs: t.list(tUserID), }; export type DMAddMembersOperation = $ReadOnly<{ +type: 'add_members', +threadID: string, ...DMAddMembersBase, }>; export const dmAddMembersOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_MEMBERS), threadID: t.String, ...dmAddMembersBaseValidatorShape, }); export type DMAddViewerToThreadMembersOperation = $ReadOnly<{ +type: 'add_viewer_to_thread_members', +existingThreadDetails: CreateThickRawThreadInfoInput, ...DMAddMembersBase, }>; export const dmAddViewerToThreadMembersValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_VIEWER_TO_THREAD_MEMBERS), existingThreadDetails: createThickRawThreadInfoInputValidator, ...dmAddMembersBaseValidatorShape, }); export type DMJoinThreadOperation = { +type: 'join_thread', +joinerID: string, +time: number, +messageID: string, +existingThreadDetails: CreateThickRawThreadInfoInput, }; export const dmJoinThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.JOIN_THREAD), joinerID: 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 DMThreadSettingsChangesBase = { +name?: string, +description?: string, +color?: string, +avatar?: ClientAvatar, }; type DMThreadSettingsChanges = $ReadOnly<{ ...DMThreadSettingsChangesBase, +newMemberIDs?: $ReadOnlyArray, }>; type DMChangeThreadSettingsBase = { +editorID: string, +time: number, +changes: DMThreadSettingsChanges, +messageIDsPrefix: string, }; const dmChangeThreadSettingsBaseValidatorShape: TStructProps = { editorID: tUserID, time: t.Number, changes: tShape({ name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), newMemberIDs: t.maybe(t.list(tUserID)), avatar: t.maybe(clientAvatarValidator), }), messageIDsPrefix: t.String, }; export type DMChangeThreadSettingsOperation = $ReadOnly<{ +type: 'change_thread_settings', +threadID: string, ...DMChangeThreadSettingsBase, }>; export const dmChangeThreadSettingsOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS), threadID: t.String, ...dmChangeThreadSettingsBaseValidatorShape, }); export type DMChangeThreadSettingsAndAddViewerOperation = $ReadOnly<{ +type: 'change_thread_settings_and_add_viewer', +existingThreadDetails: CreateThickRawThreadInfoInput, ...DMChangeThreadSettingsBase, }>; export const dmChangeThreadSettingsAndAddViewerOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS_AND_ADD_VIEWER), existingThreadDetails: createThickRawThreadInfoInputValidator, ...dmChangeThreadSettingsBaseValidatorShape, }); export type DMOperation = | DMCreateThreadOperation | DMCreateSidebarOperation | DMSendTextMessageOperation | DMSendReactionMessageOperation | DMSendEditMessageOperation | DMAddMembersOperation | DMAddViewerToThreadMembersOperation | DMJoinThreadOperation | DMLeaveThreadOperation | DMRemoveMembersOperation | DMChangeThreadSettingsOperation | DMChangeThreadSettingsAndAddViewerOperation; export const dmOperationValidator: TUnion = t.union([ dmCreateThreadOperationValidator, dmCreateSidebarOperationValidator, dmSendTextMessageOperationValidator, dmSendReactionMessageOperationValidator, dmSendEditMessageOperationValidator, dmAddMembersOperationValidator, dmAddViewerToThreadMembersValidator, dmJoinThreadOperationValidator, dmLeaveThreadOperationValidator, dmRemoveMembersOperationValidator, dmChangeThreadSettingsOperationValidator, dmChangeThreadSettingsAndAddViewerOperationValidator, ]); export type DMOperationResult = { rawMessageInfos: Array, updateInfos: Array, }; export const processDMOpsActionType = 'PROCESS_DM_OPS'; export type ProcessDMOpsPayload = { +rawMessageInfos: $ReadOnlyArray, +updateInfos: $ReadOnlyArray, +outboundP2PMessages: ?$ReadOnlyArray, // For messages that could be retried from UI, we need to bind DM `messageID` // with `outboundP2PMessages` to keep track of whether all P2P messages // were queued on Tunnelbroker. +messageIDWithoutAutoRetry: ?string, +notificationsCreationData: ?NotificationsCreationData, }; export const queueDMOpsActionType = 'QUEUE_DM_OPS'; export type QueueDMOpsPayload = { +operation: DMOperation, +threadID: string, +timestamp: number, }; export const pruneDMOpsQueueActionType = 'PRUNE_DM_OPS_QUEUE'; export type PruneDMOpsQueuePayload = { +pruneMaxTimestamp: number, }; export const clearQueuedThreadDMOpsActionType = 'CLEAR_QUEUED_THREAD_DM_OPS'; export type ClearQueuedThreadDMOpsPayload = { +threadID: string, }; export type QueuedDMOperations = { +operations: { +[threadID: string]: $ReadOnlyArray<{ +operation: DMOperation, +timestamp: number, }>, }, }; export type SendDMStartedPayload = { +messageID: string, }; export type SendDMOpsSuccessPayload = { +messageID: string, +outboundP2PMessageIDs: $ReadOnlyArray, }; export const sendDMActionTypes = Object.freeze({ started: 'SEND_DM_STARTED', success: 'SEND_DM_SUCCESS', failed: 'SEND_DM_FAILED', });