diff --git a/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js b/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js --- a/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js +++ b/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js @@ -8,6 +8,7 @@ import type { DMAddViewerToThreadMembersOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { messageTruncationStatus } from '../../types/message-types.js'; +import { joinThreadSubscription } from '../../types/subscription-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; function createAddViewerToThreadMembersResults( @@ -28,7 +29,13 @@ const resultThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, - allMemberIDs: [...existingThreadDetails.allMemberIDs, ...addedUserIDs], + allMemberIDWithSubscriptions: [ + ...existingThreadDetails.allMemberIDWithSubscriptions, + ...addedUserIDs.map(id => ({ + id, + subscription: joinThreadSubscription, + })), + ], }, viewerID, ); diff --git a/lib/shared/dm-ops/change-thread-subscription.js b/lib/shared/dm-ops/change-thread-subscription.js new file mode 100644 --- /dev/null +++ b/lib/shared/dm-ops/change-thread-subscription.js @@ -0,0 +1,79 @@ +// @flow + +import invariant from 'invariant'; +import uuid from 'uuid'; + +import type { + ProcessDMOperationUtilities, + DMOperationSpec, +} from './dm-op-spec.js'; +import type { DMChangeThreadSubscriptionOperation } from '../../types/dm-ops.js'; +import { updateTypes } from '../../types/update-types-enum.js'; +import type { ClientUpdateInfo } from '../../types/update-types.js'; + +const changeThreadSubscriptionSpec: DMOperationSpec = + Object.freeze({ + processDMOperation: async ( + dmOperation: DMChangeThreadSubscriptionOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) => { + const { creatorID, threadID, subscription, time } = dmOperation; + + const threadInfo = utilities.threadInfos[threadID]; + invariant(threadInfo.thick, 'Thread should be thick'); + + const creatorMemberInfo = threadInfo.members.find( + member => member.id === creatorID, + ); + invariant(creatorMemberInfo, 'operation creator missing in thread'); + const updatedCreatorMemberInfo = { + ...creatorMemberInfo, + subscription, + }; + const otherMemberInfos = threadInfo.members.filter( + member => member.id !== creatorID, + ); + const membersUpdate = [...otherMemberInfos, updatedCreatorMemberInfo]; + + const threadInfoUpdate = { + ...threadInfo, + members: membersUpdate, + }; + const updateInfos: Array = [ + { + type: updateTypes.UPDATE_THREAD, + id: uuid.v4(), + time, + threadInfo: threadInfoUpdate, + }, + ]; + + return { updateInfos, rawMessageInfos: [] }; + }, + canBeProcessed( + dmOperation: DMChangeThreadSubscriptionOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) { + const { threadID, creatorID } = dmOperation; + if (!utilities.threadInfos[threadID]) { + return { + isProcessingPossible: false, + reason: { type: 'missing_thread', threadID }, + }; + } + + if ( + !utilities.threadInfos[threadID].members.find( + memberInfo => memberInfo.id === creatorID, + ) + ) { + return { isProcessingPossible: false, reason: { type: 'invalid' } }; + } + + return { isProcessingPossible: true }; + }, + }); + +export { changeThreadSubscriptionSpec }; diff --git a/lib/shared/dm-ops/create-sidebar-spec.js b/lib/shared/dm-ops/create-sidebar-spec.js --- a/lib/shared/dm-ops/create-sidebar-spec.js +++ b/lib/shared/dm-ops/create-sidebar-spec.js @@ -13,6 +13,7 @@ type RawMessageInfo, messageTruncationStatus, } from '../../types/message-types.js'; +import { joinThreadSubscription } from '../../types/subscription-types.js'; import { threadTypes } from '../../types/thread-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { isInvalidSidebarSource } from '../message-utils.js'; @@ -36,6 +37,10 @@ newCreateSidebarMessageID, } = dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; + const allMemberIDWithSubscriptions = allMemberIDs.map(id => ({ + id, + subscription: joinThreadSubscription, + })); const rawThreadInfo = createThickRawThreadInfo( { @@ -43,7 +48,7 @@ threadType: threadTypes.THICK_SIDEBAR, creationTime: time, parentThreadID, - allMemberIDs, + allMemberIDWithSubscriptions, roleID, creatorID, sourceMessageID, 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 @@ -67,7 +67,7 @@ threadType, creationTime, parentThreadID, - allMemberIDs, + allMemberIDWithSubscriptions, roleID, creatorID, name, @@ -80,7 +80,11 @@ pinnedCount, } = input; - const threadColor = color ?? generatePendingThreadColor(allMemberIDs); + const threadColor = + color ?? + generatePendingThreadColor( + allMemberIDWithSubscriptions.map(({ id }) => id), + ); const { membershipPermissions, role } = createRoleAndPermissionForThickThreads(threadType, threadID, roleID); @@ -93,14 +97,15 @@ color: threadColor, creationTime, parentThreadID, - members: allMemberIDs.map(memberID => - minimallyEncodeMemberInfo({ - id: memberID, - role: role.id, - permissions: membershipPermissions, - isSender: memberID === viewerID, - subscription: joinThreadSubscription, - }), + members: allMemberIDWithSubscriptions.map( + ({ id: memberID, subscription }) => + minimallyEncodeMemberInfo({ + id: memberID, + role: role.id, + permissions: membershipPermissions, + isSender: memberID === viewerID, + subscription: subscription, + }), ), roles: { [role.id]: role, @@ -142,13 +147,17 @@ newMessageID, } = dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; + const allMemberIDWithSubscriptions = allMemberIDs.map(id => ({ + id, + subscription: joinThreadSubscription, + })); const rawThreadInfo = createThickRawThreadInfo( { threadID, threadType, creationTime: time, - allMemberIDs, + allMemberIDWithSubscriptions, roleID, creatorID, }, 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 @@ -4,6 +4,7 @@ import { addViewerToThreadMembersSpec } from './add-viewer-to-thread-members-spec.js'; import { changeThreadSettingsAndAddViewerSpec } from './change-thread-settings-and-add-viewer-spec.js'; import { changeThreadSettingsSpec } from './change-thread-settings-spec.js'; +import { changeThreadSubscriptionSpec } from './change-thread-subscription.js'; import { createSidebarSpec } from './create-sidebar-spec.js'; import { createThreadSpec } from './create-thread-spec.js'; import type { DMOperationSpec } from './dm-op-spec.js'; @@ -31,4 +32,5 @@ [dmOperationTypes.CHANGE_THREAD_SETTINGS]: changeThreadSettingsSpec, [dmOperationTypes.CHANGE_THREAD_SETTINGS_AND_ADD_VIEWER]: changeThreadSettingsAndAddViewerSpec, + [dmOperationTypes.CHANGE_THREAD_SUBSCRIPTION]: changeThreadSubscriptionSpec, }); diff --git a/lib/shared/dm-ops/join-thread-spec.js b/lib/shared/dm-ops/join-thread-spec.js --- a/lib/shared/dm-ops/join-thread-spec.js +++ b/lib/shared/dm-ops/join-thread-spec.js @@ -57,7 +57,10 @@ const newThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, - allMemberIDs: [...existingThreadDetails.allMemberIDs, joinerID], + allMemberIDWithSubscriptions: [ + ...existingThreadDetails.allMemberIDWithSubscriptions, + { id: joinerID, subscription: joinThreadSubscription }, + ], }, viewerID, ); 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 @@ -5,6 +5,10 @@ import { clientAvatarValidator, type ClientAvatar } from './avatar-types.js'; import type { RawMessageInfo } from './message-types.js'; import type { OutboundP2PMessage } from './sqlite-types.js'; +import { + type ThreadSubscription, + threadSubscriptionValidator, +} from './subscription-types.js'; import { type NonSidebarThickThreadType, nonSidebarThickThreadTypes, @@ -29,15 +33,26 @@ CHANGE_THREAD_SETTINGS: 'change_thread_settings', CHANGE_THREAD_SETTINGS_AND_ADD_VIEWER: 'change_thread_settings_and_add_viewer', + CHANGE_THREAD_SUBSCRIPTION: 'change_thread_subscription', }); export type DMOperationType = $Values; +type MemberIDWithSubscription = { + +id: string, + +subscription: ThreadSubscription, +}; +export const memberIDWithSubscriptionValidator: TInterface = + tShape({ + id: tUserID, + subscription: threadSubscriptionValidator, + }); + export type CreateThickRawThreadInfoInput = { +threadID: string, +threadType: ThickThreadType, +creationTime: number, +parentThreadID?: ?string, - +allMemberIDs: $ReadOnlyArray, + +allMemberIDWithSubscriptions: $ReadOnlyArray, +roleID: string, +creatorID: string, +name?: ?string, @@ -55,7 +70,7 @@ threadType: thickThreadTypeValidator, creationTime: t.Number, parentThreadID: t.maybe(t.String), - allMemberIDs: t.list(tUserID), + allMemberIDWithSubscriptions: t.list(memberIDWithSubscriptionValidator), roleID: t.String, creatorID: tUserID, name: t.maybe(t.String), @@ -313,6 +328,22 @@ ...dmChangeThreadSettingsBaseValidatorShape, }); +export type DMChangeThreadSubscriptionOperation = { + +type: 'change_thread_subscription', + +time: number, + +threadID: string, + +creatorID: string, + +subscription: ThreadSubscription, +}; +export const dmChangeThreadSubscriptionOperationValidator: TInterface = + tShape({ + type: tString(dmOperationTypes.CHANGE_THREAD_SUBSCRIPTION), + time: t.Number, + threadID: t.String, + creatorID: tUserID, + subscription: threadSubscriptionValidator, + }); + export type DMOperation = | DMCreateThreadOperation | DMCreateSidebarOperation @@ -325,7 +356,8 @@ | DMLeaveThreadOperation | DMRemoveMembersOperation | DMChangeThreadSettingsOperation - | DMChangeThreadSettingsAndAddViewerOperation; + | DMChangeThreadSettingsAndAddViewerOperation + | DMChangeThreadSubscriptionOperation; export const dmOperationValidator: TUnion = t.union([ dmCreateThreadOperationValidator, dmCreateSidebarOperationValidator, @@ -339,6 +371,7 @@ dmRemoveMembersOperationValidator, dmChangeThreadSettingsOperationValidator, dmChangeThreadSettingsAndAddViewerOperationValidator, + dmChangeThreadSubscriptionOperationValidator, ]); export type DMOperationResult = {