diff --git a/lib/shared/dm-ops/add-members-spec.js b/lib/shared/dm-ops/add-members-spec.js
--- a/lib/shared/dm-ops/add-members-spec.js
+++ b/lib/shared/dm-ops/add-members-spec.js
@@ -23,15 +23,17 @@
 import { values } from '../../utils/objects.js';
 import { roleIsDefaultRole, userIsMember } from '../thread-utils.js';
 
+export type AddMembersResult = {
+  rawMessageInfos: Array<RawMessageInfo>,
+  updateInfos: Array<ClientUpdateInfo>,
+  threadInfo: ?ThickRawThreadInfo,
+};
+
 function createAddNewMembersResults(
   dmOperation: DMAddMembersOperation,
   viewerID: string,
   utilities: ProcessDMOperationUtilities,
-): {
-  +rawMessageInfos: Array<RawMessageInfo>,
-  +updateInfos: Array<ClientUpdateInfo>,
-  +threadInfo: ?ThickRawThreadInfo,
-} {
+): AddMembersResult {
   const { editorID, time, messageID, addedUserIDs, threadID } = dmOperation;
   const addMembersMessage = {
     type: messageTypes.ADD_MEMBERS,
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
@@ -2,27 +2,20 @@
 
 import uuid from 'uuid';
 
+import type { AddMembersResult } from './add-members-spec.js';
 import { createThickRawThreadInfo } from './create-thread-spec.js';
 import type { DMOperationSpec } from './dm-op-spec.js';
 import { createRepliesCountUpdate } from './dm-op-utils.js';
 import type { DMAddViewerToThreadMembersOperation } from '../../types/dm-ops.js';
 import { messageTypes } from '../../types/message-types-enum.js';
-import {
-  messageTruncationStatus,
-  type RawMessageInfo,
-} from '../../types/message-types.js';
-import { type ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js';
+import { messageTruncationStatus } from '../../types/message-types.js';
 import { updateTypes } from '../../types/update-types-enum.js';
 import type { ClientUpdateInfo } from '../../types/update-types.js';
 
 function createAddViewerToThreadMembersResults(
   dmOperation: DMAddViewerToThreadMembersOperation,
   viewerID: string,
-): {
-  +rawMessageInfos: Array<RawMessageInfo>,
-  +updateInfos: Array<ClientUpdateInfo>,
-  +threadInfo: ?ThickRawThreadInfo,
-} {
+): AddMembersResult {
   const { editorID, time, messageID, addedUserIDs, existingThreadDetails } =
     dmOperation;
   const addMembersMessage = {
diff --git a/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js b/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js
new file mode 100644
--- /dev/null
+++ b/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js
@@ -0,0 +1,77 @@
+// @flow
+
+import {
+  addViewerToThreadMembersSpec,
+  createAddViewerToThreadMembersResults,
+} from './add-viewer-to-thread-members-spec.js';
+import { processChangeSettingsOperation } from './change-thread-settings-spec.js';
+import type {
+  DMOperationSpec,
+  ProcessDMOperationUtilities,
+} from './dm-op-spec.js';
+import type { DMChangeThreadSettingsAndAddViewerOperation } from '../../types/dm-ops.js';
+
+function createAddViewerAndMembersOperation(
+  dmOperation: DMChangeThreadSettingsAndAddViewerOperation,
+) {
+  const { editorID, time, messageIDsPrefix, changes, existingThreadDetails } =
+    dmOperation;
+  const newMemberIDs =
+    changes.newMemberIDs && changes.newMemberIDs.length > 0
+      ? [...new Set(changes.newMemberIDs)]
+      : [];
+  return {
+    type: 'add_viewer_to_thread_members',
+    editorID,
+    time,
+    messageID: `${messageIDsPrefix}/add_members`,
+    addedUserIDs: newMemberIDs,
+    existingThreadDetails,
+  };
+}
+
+function processAddViewerToThreadMembersOperation(
+  dmOperation: DMChangeThreadSettingsAndAddViewerOperation,
+  viewerID: string,
+) {
+  const operation = createAddViewerAndMembersOperation(dmOperation);
+  if (operation.addedUserIDs.length === 0) {
+    return null;
+  }
+  return createAddViewerToThreadMembersResults(operation, viewerID);
+}
+
+const changeThreadSettingsAndAddViewerSpec: DMOperationSpec<DMChangeThreadSettingsAndAddViewerOperation> =
+  Object.freeze({
+    processDMOperation: async (
+      dmOperation: DMChangeThreadSettingsAndAddViewerOperation,
+      viewerID: string,
+      utilities: ProcessDMOperationUtilities,
+    ) => {
+      const addMembersResult = processAddViewerToThreadMembersOperation(
+        dmOperation,
+        viewerID,
+      );
+
+      return processChangeSettingsOperation(
+        dmOperation,
+        viewerID,
+        utilities,
+        addMembersResult,
+      );
+    },
+    canBeProcessed(
+      dmOperation: DMChangeThreadSettingsAndAddViewerOperation,
+      viewerID: string,
+      utilities: ProcessDMOperationUtilities,
+    ) {
+      const operation = createAddViewerAndMembersOperation(dmOperation);
+      return addViewerToThreadMembersSpec.canBeProcessed(
+        operation,
+        viewerID,
+        utilities,
+      );
+    },
+  });
+
+export { changeThreadSettingsAndAddViewerSpec };
diff --git a/lib/shared/dm-ops/change-thread-settings-spec.js b/lib/shared/dm-ops/change-thread-settings-spec.js
--- a/lib/shared/dm-ops/change-thread-settings-spec.js
+++ b/lib/shared/dm-ops/change-thread-settings-spec.js
@@ -3,17 +3,20 @@
 import invariant from 'invariant';
 import uuid from 'uuid';
 
-import { createAddNewMembersResults } from './add-members-spec.js';
 import {
-  addViewerToThreadMembersSpec,
-  createAddViewerToThreadMembersResults,
-} from './add-viewer-to-thread-members-spec.js';
+  type AddMembersResult,
+  createAddNewMembersResults,
+} from './add-members-spec.js';
 import type {
   DMOperationSpec,
   ProcessDMOperationUtilities,
 } from './dm-op-spec.js';
 import { createRepliesCountUpdate } from './dm-op-utils.js';
-import type { DMChangeThreadSettingsOperation } from '../../types/dm-ops.js';
+import type {
+  DMChangeThreadSettingsAndAddViewerOperation,
+  DMChangeThreadSettingsOperation,
+  DMOperationResult,
+} from '../../types/dm-ops.js';
 import { messageTypes } from '../../types/message-types-enum.js';
 import type { RawMessageInfo } from '../../types/message-types.js';
 import type { RawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js';
@@ -22,178 +25,164 @@
 import type { ClientUpdateInfo } from '../../types/update-types.js';
 import { values } from '../../utils/objects.js';
 
-function createAddMembersOperation(
+function processAddMembersOperation(
   dmOperation: DMChangeThreadSettingsOperation,
   viewerID: string,
+  utilities: ProcessDMOperationUtilities,
 ) {
-  const { editorID, time, messageIDsPrefix, changes, existingThreadDetails } =
-    dmOperation;
+  const { editorID, time, messageIDsPrefix, changes, threadID } = dmOperation;
   const newMemberIDs =
     changes.newMemberIDs && changes.newMemberIDs.length > 0
       ? [...new Set(changes.newMemberIDs)]
       : [];
-  if (newMemberIDs.includes(viewerID)) {
-    return {
-      type: 'add_viewer_to_thread_members',
-      editorID,
-      time,
-      messageID: `${messageIDsPrefix}/add_members`,
-      addedUserIDs: newMemberIDs,
-      existingThreadDetails,
-    };
+  if (!changes.newMemberIDs || changes.newMemberIDs.length === 0) {
+    return null;
   }
-  return {
+  const operation = {
     type: 'add_members',
     editorID,
     time,
     messageID: `${messageIDsPrefix}/add_members`,
     addedUserIDs: newMemberIDs,
-    threadID: existingThreadDetails.threadID,
+    threadID,
   };
+  return createAddNewMembersResults(operation, viewerID, utilities);
 }
 
-function processAddMembersOperation(
-  dmOperation: DMChangeThreadSettingsOperation,
+function processChangeSettingsOperation(
+  dmOperation:
+    | DMChangeThreadSettingsOperation
+    | DMChangeThreadSettingsAndAddViewerOperation,
   viewerID: string,
   utilities: ProcessDMOperationUtilities,
-) {
-  const operation = createAddMembersOperation(dmOperation, viewerID);
-  if (operation.type === 'add_viewer_to_thread_members') {
-    return createAddViewerToThreadMembersResults(operation, viewerID);
-  } else {
-    return createAddNewMembersResults(operation, viewerID, utilities);
+  addMembersResult: ?AddMembersResult,
+): DMOperationResult {
+  const { editorID, time, changes, messageIDsPrefix } = dmOperation;
+  const { name, description, color, avatar } = changes;
+  const threadID =
+    dmOperation.type === 'change_thread_settings'
+      ? dmOperation.threadID
+      : dmOperation.existingThreadDetails.threadID;
+
+  let threadInfoToUpdate: ?(RawThreadInfo | LegacyRawThreadInfo) =
+    utilities.threadInfos[threadID];
+  const updateInfos: Array<ClientUpdateInfo> = [];
+  const rawMessageInfos: Array<RawMessageInfo> = [];
+
+  if (addMembersResult) {
+    if (addMembersResult.threadInfo) {
+      threadInfoToUpdate = addMembersResult.threadInfo;
+    }
+    updateInfos.push(...addMembersResult.updateInfos);
+    rawMessageInfos.push(...addMembersResult.rawMessageInfos);
   }
-}
 
-const changeThreadSettingsSpec: DMOperationSpec<DMChangeThreadSettingsOperation> =
-  Object.freeze({
-    processDMOperation: async (
-      dmOperation: DMChangeThreadSettingsOperation,
-      viewerID: string,
-      utilities: ProcessDMOperationUtilities,
-    ) => {
-      const {
-        editorID,
-        time,
-        changes,
-        messageIDsPrefix,
-        existingThreadDetails,
-      } = dmOperation;
-      const { name, description, color, avatar } = changes;
-      const threadID = existingThreadDetails.threadID;
-
-      let threadInfoToUpdate: ?(RawThreadInfo | LegacyRawThreadInfo) =
-        utilities.threadInfos[threadID];
-      const updateInfos: Array<ClientUpdateInfo> = [];
-      const rawMessageInfos: Array<RawMessageInfo> = [];
-
-      if (changes.newMemberIDs && changes.newMemberIDs.length > 0) {
-        const addMembersResult = processAddMembersOperation(
-          dmOperation,
-          viewerID,
-          utilities,
-        );
-        if (addMembersResult.threadInfo) {
-          threadInfoToUpdate = addMembersResult.threadInfo;
-        }
-        updateInfos.push(...addMembersResult.updateInfos);
-        rawMessageInfos.push(...addMembersResult.rawMessageInfos);
-      }
+  invariant(threadInfoToUpdate?.thick, 'Thread should be thick');
 
-      invariant(threadInfoToUpdate?.thick, 'Thread should be thick');
+  const changedFields: { [string]: string | number } = {};
 
-      const changedFields: { [string]: string | number } = {};
+  if (name !== undefined && name !== null) {
+    changedFields.name = name;
+    threadInfoToUpdate = {
+      ...threadInfoToUpdate,
+      name,
+    };
+  }
 
-      if (name !== undefined && name !== null) {
-        changedFields.name = name;
-        threadInfoToUpdate = {
-          ...threadInfoToUpdate,
-          name,
-        };
-      }
+  if (description !== undefined && description !== null) {
+    changedFields.description = description;
+    threadInfoToUpdate = {
+      ...threadInfoToUpdate,
+      description,
+    };
+  }
 
-      if (description !== undefined && description !== null) {
-        changedFields.description = description;
-        threadInfoToUpdate = {
-          ...threadInfoToUpdate,
-          description,
-        };
-      }
+  if (color) {
+    changedFields.color = color;
+    threadInfoToUpdate = {
+      ...threadInfoToUpdate,
+      color,
+    };
+  }
 
-      if (color) {
-        changedFields.color = color;
-        threadInfoToUpdate = {
-          ...threadInfoToUpdate,
-          color,
-        };
-      }
+  if (avatar) {
+    changedFields.avatar = JSON.stringify(avatar);
+    threadInfoToUpdate = {
+      ...threadInfoToUpdate,
+      avatar,
+    };
+  }
 
-      if (avatar) {
-        changedFields.avatar = JSON.stringify(avatar);
-        threadInfoToUpdate = {
-          ...threadInfoToUpdate,
-          avatar,
-        };
-      }
+  for (const fieldName in changedFields) {
+    const newValue = changedFields[fieldName];
+    rawMessageInfos.push({
+      type: messageTypes.CHANGE_SETTINGS,
+      threadID,
+      creatorID: editorID,
+      time,
+      field: fieldName,
+      value: newValue,
+      id: `${messageIDsPrefix}/${fieldName}`,
+    });
+  }
 
-      for (const fieldName in changedFields) {
-        const newValue = changedFields[fieldName];
-        rawMessageInfos.push({
-          type: messageTypes.CHANGE_SETTINGS,
-          threadID,
-          creatorID: editorID,
-          time,
-          field: fieldName,
-          value: newValue,
-          id: `${messageIDsPrefix}/${fieldName}`,
-        });
-      }
+  const repliesCountUpdate = createRepliesCountUpdate(
+    threadInfoToUpdate,
+    rawMessageInfos,
+  );
+  if (repliesCountUpdate) {
+    updateInfos.push(repliesCountUpdate);
+  } else if (values(changedFields).length > 0) {
+    updateInfos.push({
+      type: updateTypes.UPDATE_THREAD,
+      id: uuid.v4(),
+      time,
+      threadInfo: threadInfoToUpdate,
+    });
+  }
 
-      const repliesCountUpdate = createRepliesCountUpdate(
-        threadInfoToUpdate,
-        rawMessageInfos,
+  return {
+    rawMessageInfos,
+    updateInfos,
+  };
+}
+
+const changeThreadSettingsSpec: DMOperationSpec<DMChangeThreadSettingsOperation> =
+  Object.freeze({
+    processDMOperation: async (
+      dmOperation: DMChangeThreadSettingsOperation,
+      viewerID: string,
+      utilities: ProcessDMOperationUtilities,
+    ) => {
+      const addMembersResult = processAddMembersOperation(
+        dmOperation,
+        viewerID,
+        utilities,
       );
-      if (repliesCountUpdate) {
-        updateInfos.push(repliesCountUpdate);
-      } else if (values(changedFields).length > 0) {
-        updateInfos.push({
-          type: updateTypes.UPDATE_THREAD,
-          id: uuid.v4(),
-          time,
-          threadInfo: threadInfoToUpdate,
-        });
-      }
 
-      return {
-        rawMessageInfos,
-        updateInfos,
-      };
+      return processChangeSettingsOperation(
+        dmOperation,
+        viewerID,
+        utilities,
+        addMembersResult,
+      );
     },
     canBeProcessed(
       dmOperation: DMChangeThreadSettingsOperation,
       viewerID: string,
       utilities: ProcessDMOperationUtilities,
     ) {
-      const operation = createAddMembersOperation(dmOperation, viewerID);
-      if (operation.type === 'add_viewer_to_thread_members') {
-        return addViewerToThreadMembersSpec.canBeProcessed(
-          operation,
-          viewerID,
-          utilities,
-        );
-      } else if (
-        utilities.threadInfos[dmOperation.existingThreadDetails.threadID]
-      ) {
+      if (utilities.threadInfos[dmOperation.threadID]) {
         return { isProcessingPossible: true };
       }
       return {
         isProcessingPossible: false,
         reason: {
           type: 'missing_thread',
-          threadID: dmOperation.existingThreadDetails.threadID,
+          threadID: dmOperation.threadID,
         },
       };
     },
   });
 
-export { changeThreadSettingsSpec };
+export { changeThreadSettingsSpec, processChangeSettingsOperation };
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
@@ -2,6 +2,7 @@
 
 import { addMembersSpec } from './add-members-spec.js';
 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 { createSidebarSpec } from './create-sidebar-spec.js';
 import { createThreadSpec } from './create-thread-spec.js';
@@ -28,4 +29,6 @@
   [dmOperationTypes.LEAVE_THREAD]: leaveThreadSpec,
   [dmOperationTypes.REMOVE_MEMBERS]: removeMembersSpec,
   [dmOperationTypes.CHANGE_THREAD_SETTINGS]: changeThreadSettingsSpec,
+  [dmOperationTypes.CHANGE_THREAD_SETTINGS_AND_ADD_VIEWER]:
+    changeThreadSettingsAndAddViewerSpec,
 });
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
@@ -1,6 +1,6 @@
 // @flow
 
-import t, { type TInterface, type TUnion } from 'tcomb';
+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';
@@ -27,6 +27,8 @@
   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<typeof dmOperationTypes>;
 
@@ -261,8 +263,7 @@
     removedUserIDs: t.list(tUserID),
   });
 
-export type DMChangeThreadSettingsOperation = {
-  +type: 'change_thread_settings',
+type DMChangeThreadSettingsBase = {
   +editorID: string,
   +time: number,
   +changes: {
@@ -273,11 +274,9 @@
     +avatar?: ClientAvatar,
   },
   +messageIDsPrefix: string,
-  +existingThreadDetails: CreateThickRawThreadInfoInput,
 };
-export const dmChangeThreadSettingsOperationValidator: TInterface<DMChangeThreadSettingsOperation> =
-  tShape<DMChangeThreadSettingsOperation>({
-    type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS),
+const dmChangeThreadSettingsBaseValidatorShape: TStructProps<DMChangeThreadSettingsBase> =
+  {
     editorID: tUserID,
     time: t.Number,
     changes: tShape({
@@ -288,7 +287,30 @@
       avatar: t.maybe(clientAvatarValidator),
     }),
     messageIDsPrefix: t.String,
+  };
+
+export type DMChangeThreadSettingsOperation = $ReadOnly<{
+  +type: 'change_thread_settings',
+  +threadID: string,
+  ...DMChangeThreadSettingsBase,
+}>;
+export const dmChangeThreadSettingsOperationValidator: TInterface<DMChangeThreadSettingsOperation> =
+  tShape<DMChangeThreadSettingsOperation>({
+    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<DMChangeThreadSettingsAndAddViewerOperation> =
+  tShape<DMChangeThreadSettingsAndAddViewerOperation>({
+    type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS_AND_ADD_VIEWER),
     existingThreadDetails: createThickRawThreadInfoInputValidator,
+    ...dmChangeThreadSettingsBaseValidatorShape,
   });
 
 export type DMOperation =
@@ -302,7 +324,8 @@
   | DMJoinThreadOperation
   | DMLeaveThreadOperation
   | DMRemoveMembersOperation
-  | DMChangeThreadSettingsOperation;
+  | DMChangeThreadSettingsOperation
+  | DMChangeThreadSettingsAndAddViewerOperation;
 export const dmOperationValidator: TUnion<DMOperation> = t.union([
   dmCreateThreadOperationValidator,
   dmCreateSidebarOperationValidator,
@@ -315,6 +338,7 @@
   dmLeaveThreadOperationValidator,
   dmRemoveMembersOperationValidator,
   dmChangeThreadSettingsOperationValidator,
+  dmChangeThreadSettingsAndAddViewerOperationValidator,
 ]);
 
 export type DMOperationResult = {