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<DMChangeThreadSubscriptionOperation> =
+  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<ClientUpdateInfo> = [
+        {
+          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<ThickMemberInfo>({
-        id: memberID,
-        role: role.id,
-        permissions: membershipPermissions,
-        isSender: memberID === viewerID,
-        subscription: joinThreadSubscription,
-      }),
+    members: allMemberIDsWithSubscriptions.map(
+      ({ id: memberID, subscription }) =>
+        minimallyEncodeMemberInfo<ThickMemberInfo>({
+          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<string>, 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<typeof dmOperationTypes>;
 
+type MemberIDWithSubscription = {
+  +id: string,
+  +subscription: ThreadSubscription,
+};
+export const memberIDWithSubscriptionValidator: TInterface<MemberIDWithSubscription> =
+  tShape<MemberIDWithSubscription>({
+    id: tUserID,
+    subscription: threadSubscriptionValidator,
+  });
+
 export type CreateThickRawThreadInfoInput = {
   +threadID: string,
   +threadType: ThickThreadType,
   +creationTime: number,
   +parentThreadID?: ?string,
-  +allMemberIDs: $ReadOnlyArray<string>,
+  +allMemberIDsWithSubscriptions: $ReadOnlyArray<MemberIDWithSubscription>,
   +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<DMChangeThreadSubscriptionOperation> =
+  tShape<DMChangeThreadSubscriptionOperation>({
+    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<DMOperation> = t.union([
   dmCreateThreadOperationValidator,
   dmCreateSidebarOperationValidator,
@@ -316,6 +348,7 @@
   dmLeaveThreadOperationValidator,
   dmRemoveMembersOperationValidator,
   dmChangeThreadSettingsOperationValidator,
+  dmChangeThreadSubscriptionOperationValidator,
 ]);
 
 export type DMOperationResult = {