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 @@ -9,6 +9,7 @@ import { messageTypes } from '../../types/message-types-enum.js'; import { messageTruncationStatus } from '../../types/message-types.js'; import type { AddMembersMessageData } from '../../types/messages/add-members.js'; +import { joinThreadSubscription } from '../../types/subscription-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; @@ -40,7 +41,13 @@ const resultThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, - allMemberIDs: [...existingThreadDetails.allMemberIDs, ...addedUserIDs], + allMemberIDsWithSubscriptions: [ + ...existingThreadDetails.allMemberIDsWithSubscriptions, + ...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,80 @@ +// @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 }; + }, + supportsAutoRetry: 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 { generatePendingThreadColor } from '../color-utils.js'; @@ -107,6 +108,10 @@ newCreateSidebarMessageID, } = dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; + const allMemberIDsWithSubscriptions = allMemberIDs.map(id => ({ + id, + subscription: joinThreadSubscription, + })); const rawThreadInfo = createThickRawThreadInfo( { @@ -114,7 +119,7 @@ threadType: threadTypes.THICK_SIDEBAR, creationTime: time, parentThreadID, - allMemberIDs, + allMemberIDsWithSubscriptions, roleID, unread: creatorID !== viewerID, 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 @@ -65,7 +65,7 @@ threadType, creationTime, parentThreadID, - allMemberIDs, + allMemberIDsWithSubscriptions, roleID, unread, name, @@ -78,7 +78,11 @@ pinnedCount, } = input; - const threadColor = color ?? generatePendingThreadColor(allMemberIDs); + const threadColor = + color ?? + generatePendingThreadColor( + allMemberIDsWithSubscriptions.map(({ id }) => id), + ); const { membershipPermissions, role } = createRoleAndPermissionForThickThreads(threadType, threadID, roleID); @@ -91,14 +95,15 @@ color: threadColor, creationTime, parentThreadID, - members: allMemberIDs.map(memberID => - minimallyEncodeMemberInfo({ - id: memberID, - role: role.id, - permissions: membershipPermissions, - isSender: memberID === viewerID, - subscription: joinThreadSubscription, - }), + members: allMemberIDsWithSubscriptions.map( + ({ id: memberID, subscription }) => + minimallyEncodeMemberInfo({ + id: memberID, + role: role.id, + permissions: membershipPermissions, + isSender: memberID === viewerID, + subscription, + }), ), roles: { [role.id]: role, @@ -163,13 +168,17 @@ newMessageID, } = dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; + const allMemberIDsWithSubscriptions = allMemberIDs.map(id => ({ + id, + subscription: joinThreadSubscription, + })); const rawThreadInfo = createThickRawThreadInfo( { threadID, threadType, creationTime: time, - allMemberIDs, + allMemberIDsWithSubscriptions, roleID, unread: creatorID !== viewerID, }, 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 @@ -3,6 +3,7 @@ import { addMembersSpec } from './add-members-spec.js'; import { addViewerToThreadMembersSpec } from './add-viewer-to-thread-members-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'; @@ -28,4 +29,5 @@ [dmOperationTypes.LEAVE_THREAD]: leaveThreadSpec, [dmOperationTypes.REMOVE_MEMBERS]: removeMembersSpec, [dmOperationTypes.CHANGE_THREAD_SETTINGS]: changeThreadSettingsSpec, + [dmOperationTypes.CHANGE_THREAD_SUBSCRIPTION]: changeThreadSubscriptionSpec, }); diff --git a/lib/shared/dm-ops/dm-op-utils.js b/lib/shared/dm-ops/dm-op-utils.js --- a/lib/shared/dm-ops/dm-op-utils.js +++ b/lib/shared/dm-ops/dm-op-utils.js @@ -12,7 +12,10 @@ DMAddViewerToThreadMembersOperation, DMOperation, } from '../../types/dm-ops.js'; -import type { ThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; +import type { + ThickRawThreadInfo, + ThreadInfo, +} from '../../types/minimally-encoded-thread-permissions-types.js'; import type { InboundActionMetadata } from '../../types/redux-types.js'; import { outboundP2PMessageStatuses, @@ -130,7 +133,7 @@ } function getCreateThickRawThreadInfoInputFromThreadInfo( - threadInfo: ThreadInfo, + threadInfo: ThickRawThreadInfo, ): CreateThickRawThreadInfoInput { const roleID = Object.keys(threadInfo.roles).pop(); const thickThreadType = assertThickThreadType(threadInfo.type); @@ -139,7 +142,12 @@ threadType: thickThreadType, creationTime: threadInfo.creationTime, parentThreadID: threadInfo.parentThreadID, - allMemberIDs: threadInfo.members.map(member => member.id), + allMemberIDsWithSubscriptions: threadInfo.members.map( + ({ id, subscription }) => ({ + id, + subscription, + }), + ), roleID, unread: !!threadInfo.currentUser.unread, name: threadInfo.name, @@ -165,8 +173,10 @@ return React.useCallback( async (newMemberIDs: $ReadOnlyArray, threadInfo: ThreadInfo) => { + const rawThreadInfo = threadInfos[threadInfo.id]; + invariant(rawThreadInfo.thick, 'thread should be thick'); const existingThreadDetails = - getCreateThickRawThreadInfoInputFromThreadInfo(threadInfo); + getCreateThickRawThreadInfoInputFromThreadInfo(rawThreadInfo); invariant(viewerID, 'viewerID should be set'); const addViewerToThreadMembersOperation: DMAddViewerToThreadMembersOperation = 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 @@ -69,7 +69,10 @@ const newThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, - allMemberIDs: [...existingThreadDetails.allMemberIDs, joinerID], + allMemberIDsWithSubscriptions: [ + ...existingThreadDetails.allMemberIDsWithSubscriptions, + { 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 @@ -6,6 +6,10 @@ import type { RawMessageInfo } from './message-types.js'; import type { NotificationsCreationData } from './notif-types.js'; import type { OutboundP2PMessage } from './sqlite-types.js'; +import { + type ThreadSubscription, + threadSubscriptionValidator, +} from './subscription-types.js'; import { type NonSidebarThickThreadType, nonSidebarThickThreadTypes, @@ -28,15 +32,26 @@ LEAVE_THREAD: 'leave_thread', REMOVE_MEMBERS: 'remove_members', CHANGE_THREAD_SETTINGS: 'change_thread_settings', + 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, + +allMemberIDsWithSubscriptions: $ReadOnlyArray, +roleID: string, +unread: boolean, +name?: ?string, @@ -54,7 +69,7 @@ threadType: thickThreadTypeValidator, creationTime: t.Number, parentThreadID: t.maybe(t.String), - allMemberIDs: t.list(tUserID), + allMemberIDsWithSubscriptions: t.list(memberIDWithSubscriptionValidator), roleID: t.String, unread: t.Boolean, name: t.maybe(t.String), @@ -292,6 +307,22 @@ messageIDsPrefix: t.String, }); +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 @@ -303,7 +334,8 @@ | DMJoinThreadOperation | DMLeaveThreadOperation | DMRemoveMembersOperation - | DMChangeThreadSettingsOperation; + | DMChangeThreadSettingsOperation + | DMChangeThreadSubscriptionOperation; export const dmOperationValidator: TUnion = t.union([ dmCreateThreadOperationValidator, dmCreateSidebarOperationValidator, @@ -316,6 +348,7 @@ dmLeaveThreadOperationValidator, dmRemoveMembersOperationValidator, dmChangeThreadSettingsOperationValidator, + dmChangeThreadSubscriptionOperationValidator, ]); export type DMOperationResult = {