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
@@ -3,10 +3,7 @@
 import invariant from 'invariant';
 import uuid from 'uuid';
 
-import {
-  createRoleAndPermissionForThickThreads,
-  createThickRawThreadInfo,
-} from './create-thread-spec.js';
+import { createRoleAndPermissionForThickThreads } from './create-thread-spec.js';
 import type {
   DMOperationSpec,
   ProcessDMOperationUtilities,
@@ -14,10 +11,7 @@
 import { createRepliesCountUpdate } from './dm-op-utils.js';
 import type { DMAddMembersOperation } from '../../types/dm-ops.js';
 import { messageTypes } from '../../types/message-types-enum.js';
-import {
-  messageTruncationStatus,
-  type RawMessageInfo,
-} from '../../types/message-types.js';
+import { type RawMessageInfo } from '../../types/message-types.js';
 import {
   minimallyEncodeMemberInfo,
   type ThickRawThreadInfo,
@@ -38,104 +32,62 @@
   +updateInfos: Array<ClientUpdateInfo>,
   +threadInfo: ?ThickRawThreadInfo,
 } {
-  const { editorID, time, messageID, addedUserIDs, existingThreadDetails } =
-    dmOperation;
+  const { editorID, time, messageID, addedUserIDs, threadID } = dmOperation;
   const addMembersMessage = {
     type: messageTypes.ADD_MEMBERS,
     id: messageID,
-    threadID: existingThreadDetails.threadID,
+    threadID,
     creatorID: editorID,
     time,
     addedUserIDs: [...addedUserIDs],
   };
 
-  const viewerIsAdded = addedUserIDs.includes(viewerID);
-  const updateInfos: Array<ClientUpdateInfo> = [];
-  const rawMessageInfos: Array<RawMessageInfo> = [];
-  let resultThreadInfo: ?ThickRawThreadInfo;
-  if (viewerIsAdded) {
-    const newThread = createThickRawThreadInfo(
-      {
-        ...existingThreadDetails,
-        allMemberIDs: [...existingThreadDetails.allMemberIDs, ...addedUserIDs],
-      },
-      viewerID,
-    );
-    resultThreadInfo = newThread;
-    updateInfos.push({
-      type: updateTypes.JOIN_THREAD,
-      id: uuid.v4(),
-      time,
-      threadInfo: newThread,
-      rawMessageInfos: [addMembersMessage],
-      truncationStatus: messageTruncationStatus.EXHAUSTIVE,
-      rawEntryInfos: [],
-    });
-    const repliesCountUpdate = createRepliesCountUpdate(newThread, [
-      addMembersMessage,
-    ]);
-    if (
-      repliesCountUpdate &&
-      repliesCountUpdate.type === updateTypes.UPDATE_THREAD
-    ) {
-      updateInfos.push(repliesCountUpdate);
-      resultThreadInfo.repliesCount =
-        repliesCountUpdate.threadInfo.repliesCount;
-    }
-  } else {
-    const currentThreadInfoOptional =
-      utilities.threadInfos[existingThreadDetails.threadID];
-    if (!currentThreadInfoOptional || !currentThreadInfoOptional.thick) {
-      // We can't perform this operation now. It should be queued for later.
-      return {
-        rawMessageInfos: [],
-        updateInfos: [],
-        threadInfo: null,
-      };
-    }
-    const currentThreadInfo: ThickRawThreadInfo = currentThreadInfoOptional;
-    const defaultRoleID = values(currentThreadInfo.roles).find(role =>
-      roleIsDefaultRole(role),
-    )?.id;
-    invariant(defaultRoleID, 'Default role ID must exist');
-    const { membershipPermissions } = createRoleAndPermissionForThickThreads(
-      currentThreadInfo.type,
-      currentThreadInfo.id,
-      defaultRoleID,
-    );
-    const newMembers = addedUserIDs
-      .filter(userID => !userIsMember(currentThreadInfo, userID))
-      .map(userID =>
-        minimallyEncodeMemberInfo<ThickMemberInfo>({
-          id: userID,
-          role: defaultRoleID,
-          permissions: membershipPermissions,
-          isSender: editorID === viewerID,
-          subscription: joinThreadSubscription,
-        }),
-      );
-
-    const newThreadInfo = {
-      ...currentThreadInfo,
-      members: [...currentThreadInfo.members, ...newMembers],
+  const currentThreadInfo = utilities.threadInfos[threadID];
+  if (!currentThreadInfo.thick) {
+    return {
+      rawMessageInfos: [],
+      updateInfos: [],
+      threadInfo: null,
     };
-    resultThreadInfo = newThreadInfo;
-    const updateWithRepliesCount = createRepliesCountUpdate(newThreadInfo, [
-      addMembersMessage,
-    ]);
-    updateInfos.push(
-      updateWithRepliesCount ?? {
-        type: updateTypes.UPDATE_THREAD,
-        id: uuid.v4(),
-        time,
-        threadInfo: newThreadInfo,
-      },
-    );
-    rawMessageInfos.push(addMembersMessage);
   }
+  const defaultRoleID = values(currentThreadInfo.roles).find(role =>
+    roleIsDefaultRole(role),
+  )?.id;
+  invariant(defaultRoleID, 'Default role ID must exist');
+  const { membershipPermissions } = createRoleAndPermissionForThickThreads(
+    currentThreadInfo.type,
+    currentThreadInfo.id,
+    defaultRoleID,
+  );
+  const newMembers = addedUserIDs
+    .filter(userID => !userIsMember(currentThreadInfo, userID))
+    .map(userID =>
+      minimallyEncodeMemberInfo<ThickMemberInfo>({
+        id: userID,
+        role: defaultRoleID,
+        permissions: membershipPermissions,
+        isSender: editorID === viewerID,
+        subscription: joinThreadSubscription,
+      }),
+    );
+
+  const resultThreadInfo = {
+    ...currentThreadInfo,
+    members: [...currentThreadInfo.members, ...newMembers],
+  };
+  const updateWithRepliesCount = createRepliesCountUpdate(resultThreadInfo, [
+    addMembersMessage,
+  ]);
+  const update = updateWithRepliesCount ?? {
+    type: updateTypes.UPDATE_THREAD,
+    id: uuid.v4(),
+    time,
+    threadInfo: resultThreadInfo,
+  };
+
   return {
-    rawMessageInfos,
-    updateInfos,
+    rawMessageInfos: [addMembersMessage],
+    updateInfos: [update],
     threadInfo: resultThreadInfo,
   };
 }
@@ -158,17 +110,14 @@
     viewerID: string,
     utilities: ProcessDMOperationUtilities,
   ) {
-    if (
-      utilities.threadInfos[dmOperation.existingThreadDetails.threadID] ||
-      dmOperation.addedUserIDs.includes(viewerID)
-    ) {
+    if (utilities.threadInfos[dmOperation.threadID]) {
       return { isProcessingPossible: true };
     }
     return {
       isProcessingPossible: false,
       reason: {
         type: 'missing_thread',
-        threadID: dmOperation.existingThreadDetails.threadID,
+        threadID: dmOperation.threadID,
       },
     };
   },
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
new file mode 100644
--- /dev/null
+++ b/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js
@@ -0,0 +1,99 @@
+// @flow
+
+import uuid from 'uuid';
+
+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 { 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,
+} {
+  const { editorID, time, messageID, addedUserIDs, existingThreadDetails } =
+    dmOperation;
+  const addMembersMessage = {
+    type: messageTypes.ADD_MEMBERS,
+    id: messageID,
+    threadID: existingThreadDetails.threadID,
+    creatorID: editorID,
+    time,
+    addedUserIDs: [...addedUserIDs],
+  };
+
+  const updateInfos: Array<ClientUpdateInfo> = [];
+
+  const resultThreadInfo = createThickRawThreadInfo(
+    {
+      ...existingThreadDetails,
+      allMemberIDs: [...existingThreadDetails.allMemberIDs, ...addedUserIDs],
+    },
+    viewerID,
+  );
+  updateInfos.push({
+    type: updateTypes.JOIN_THREAD,
+    id: uuid.v4(),
+    time,
+    threadInfo: resultThreadInfo,
+    rawMessageInfos: [addMembersMessage],
+    truncationStatus: messageTruncationStatus.EXHAUSTIVE,
+    rawEntryInfos: [],
+  });
+  const repliesCountUpdate = createRepliesCountUpdate(resultThreadInfo, [
+    addMembersMessage,
+  ]);
+  if (
+    repliesCountUpdate &&
+    repliesCountUpdate.type === updateTypes.UPDATE_THREAD
+  ) {
+    updateInfos.push(repliesCountUpdate);
+    resultThreadInfo.repliesCount = repliesCountUpdate.threadInfo.repliesCount;
+  }
+  return {
+    rawMessageInfos: [],
+    updateInfos,
+    threadInfo: resultThreadInfo,
+  };
+}
+
+const addViewerToThreadMembersSpec: DMOperationSpec<DMAddViewerToThreadMembersOperation> =
+  Object.freeze({
+    processDMOperation: async (
+      dmOperation: DMAddViewerToThreadMembersOperation,
+      viewerID: string,
+    ) => {
+      const { rawMessageInfos, updateInfos } =
+        createAddViewerToThreadMembersResults(dmOperation, viewerID);
+      return { rawMessageInfos, updateInfos };
+    },
+    canBeProcessed(
+      dmOperation: DMAddViewerToThreadMembersOperation,
+      viewerID: string,
+    ) {
+      if (dmOperation.addedUserIDs.includes(viewerID)) {
+        return { isProcessingPossible: true };
+      }
+      console.log('Invalid DM operation', dmOperation);
+      return {
+        isProcessingPossible: false,
+        reason: {
+          type: 'invalid',
+        },
+      };
+    },
+  });
+
+export { addViewerToThreadMembersSpec, createAddViewerToThreadMembersResults };
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,10 +3,11 @@
 import invariant from 'invariant';
 import uuid from 'uuid';
 
+import { createAddNewMembersResults } from './add-members-spec.js';
 import {
-  addMembersSpec,
-  createAddNewMembersResults,
-} from './add-members-spec.js';
+  addViewerToThreadMembersSpec,
+  createAddViewerToThreadMembersResults,
+} from './add-viewer-to-thread-members-spec.js';
 import type {
   DMOperationSpec,
   ProcessDMOperationUtilities,
@@ -23,6 +24,7 @@
 
 function createAddMembersOperation(
   dmOperation: DMChangeThreadSettingsOperation,
+  viewerID: string,
 ) {
   const { editorID, time, messageIDsPrefix, changes, existingThreadDetails } =
     dmOperation;
@@ -30,16 +32,39 @@
     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,
+    };
+  }
   return {
     type: 'add_members',
     editorID,
     time,
     messageID: `${messageIDsPrefix}/add_members`,
     addedUserIDs: newMemberIDs,
-    existingThreadDetails,
+    threadID: existingThreadDetails.threadID,
   };
 }
 
+function processAddMembersOperation(
+  dmOperation: DMChangeThreadSettingsOperation,
+  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);
+  }
+}
+
 const changeThreadSettingsSpec: DMOperationSpec<DMChangeThreadSettingsOperation> =
   Object.freeze({
     processDMOperation: async (
@@ -63,9 +88,8 @@
       const rawMessageInfos: Array<RawMessageInfo> = [];
 
       if (changes.newMemberIDs && changes.newMemberIDs.length > 0) {
-        const addMembersOperation = createAddMembersOperation(dmOperation);
-        const addMembersResult = createAddNewMembersResults(
-          addMembersOperation,
+        const addMembersResult = processAddMembersOperation(
+          dmOperation,
           viewerID,
           utilities,
         );
@@ -150,11 +174,25 @@
       viewerID: string,
       utilities: ProcessDMOperationUtilities,
     ) {
-      return addMembersSpec.canBeProcessed(
-        createAddMembersOperation(dmOperation),
-        viewerID,
-        utilities,
-      );
+      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]
+      ) {
+        return { isProcessingPossible: true };
+      }
+      return {
+        isProcessingPossible: false,
+        reason: {
+          type: 'missing_thread',
+          threadID: dmOperation.existingThreadDetails.threadID,
+        },
+      };
     },
   });
 
diff --git a/lib/shared/dm-ops/dm-op-spec.js b/lib/shared/dm-ops/dm-op-spec.js
--- a/lib/shared/dm-ops/dm-op-spec.js
+++ b/lib/shared/dm-ops/dm-op-spec.js
@@ -24,6 +24,8 @@
     | { +isProcessingPossible: true }
     | {
         +isProcessingPossible: false,
-        +reason: { +type: 'missing_thread', +threadID: string },
+        +reason:
+          | { +type: 'missing_thread', +threadID: string }
+          | { +type: 'invalid' },
       },
 };
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
@@ -1,6 +1,7 @@
 // @flow
 
 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 { createSidebarSpec } from './create-sidebar-spec.js';
 import { createThreadSpec } from './create-thread-spec.js';
@@ -22,6 +23,7 @@
   [dmOperationTypes.SEND_REACTION_MESSAGE]: sendReactionMessageSpec,
   [dmOperationTypes.SEND_EDIT_MESSAGE]: sendEditMessageSpec,
   [dmOperationTypes.ADD_MEMBERS]: addMembersSpec,
+  [dmOperationTypes.ADD_VIEWER_TO_THREAD_MEMBERS]: addViewerToThreadMembersSpec,
   [dmOperationTypes.JOIN_THREAD]: joinThreadSpec,
   [dmOperationTypes.LEAVE_THREAD]: leaveThreadSpec,
   [dmOperationTypes.REMOVE_MEMBERS]: removeMembersSpec,
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
@@ -22,6 +22,7 @@
   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',
@@ -173,22 +174,41 @@
     text: t.String,
   });
 
-export type DMAddMembersOperation = {
-  +type: 'add_members',
+type DMAddMembersBase = {
   +editorID: string,
   +time: number,
   +messageID: string,
   +addedUserIDs: $ReadOnlyArray<string>,
-  +existingThreadDetails: CreateThickRawThreadInfoInput,
 };
+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<DMAddMembersOperation> =
   tShape<DMAddMembersOperation>({
     type: tString(dmOperationTypes.ADD_MEMBERS),
-    editorID: tUserID,
-    time: t.Number,
-    messageID: t.String,
-    addedUserIDs: t.list(tUserID),
+    threadID: t.String,
+    ...dmAddMembersBaseValidatorShape,
+  });
+
+export type DMAddViewerToThreadMembersOperation = $ReadOnly<{
+  +type: 'add_viewer_to_thread_members',
+  +existingThreadDetails: CreateThickRawThreadInfoInput,
+  ...DMAddMembersBase,
+}>;
+export const dmAddViewerToThreadMembersValidator: TInterface<DMAddViewerToThreadMembersOperation> =
+  tShape<DMAddViewerToThreadMembersOperation>({
+    type: tString(dmOperationTypes.ADD_VIEWER_TO_THREAD_MEMBERS),
     existingThreadDetails: createThickRawThreadInfoInputValidator,
+    ...dmAddMembersBaseValidatorShape,
   });
 
 export type DMJoinThreadOperation = {
@@ -278,6 +298,7 @@
   | DMSendReactionMessageOperation
   | DMSendEditMessageOperation
   | DMAddMembersOperation
+  | DMAddViewerToThreadMembersOperation
   | DMJoinThreadOperation
   | DMLeaveThreadOperation
   | DMRemoveMembersOperation
@@ -289,6 +310,7 @@
   dmSendReactionMessageOperationValidator,
   dmSendEditMessageOperationValidator,
   dmAddMembersOperationValidator,
+  dmAddViewerToThreadMembersValidator,
   dmJoinThreadOperationValidator,
   dmLeaveThreadOperationValidator,
   dmRemoveMembersOperationValidator,