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
@@ -16,7 +16,9 @@
 import type { RawMessageInfo } from '../../types/message-types.js';
 import type { AddMembersMessageData } from '../../types/messages/add-members.js';
 import { minimallyEncodeMemberInfo } from '../../types/minimally-encoded-thread-permissions-types.js';
+import type { ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js';
 import { joinThreadSubscription } from '../../types/subscription-types.js';
+import type { ThreadPermissionsInfo } from '../../types/thread-permission-types.js';
 import type { ThickMemberInfo } from '../../types/thread-types.js';
 import { updateTypes } from '../../types/update-types-enum.js';
 import { values } from '../../utils/objects.js';
@@ -41,6 +43,46 @@
   return { messageData, rawMessageInfo };
 }
 
+function createPermissionsForNewMembers(
+  threadInfo: ThickRawThreadInfo,
+  utilities: ProcessDMOperationUtilities,
+): {
+  +membershipPermissions: ThreadPermissionsInfo,
+  +roleID: string,
+} {
+  const defaultRoleID = values(threadInfo.roles).find(role =>
+    roleIsDefaultRole(role),
+  )?.id;
+  invariant(defaultRoleID, 'Default role ID must exist');
+
+  const { parentThreadID } = threadInfo;
+  const parentThreadInfo = parentThreadID
+    ? utilities.threadInfos[parentThreadID]
+    : null;
+  if (parentThreadID && !parentThreadInfo) {
+    console.log(
+      `Parent thread with ID ${parentThreadID} was expected while adding ` +
+        'thread members but is missing from the store',
+    );
+  }
+  invariant(
+    !parentThreadInfo || parentThreadInfo.thick,
+    'Parent thread should be thick',
+  );
+
+  const { membershipPermissions } = createRoleAndPermissionForThickThreads(
+    threadInfo.type,
+    threadInfo.id,
+    defaultRoleID,
+    parentThreadInfo,
+  );
+
+  return {
+    membershipPermissions,
+    roleID: defaultRoleID,
+  };
+}
+
 const addMembersSpec: DMOperationSpec<DMAddMembersOperation> = Object.freeze({
   notificationsCreationData: async (dmOperation: DMAddMembersOperation) => {
     return {
@@ -69,31 +111,9 @@
       };
     }
 
-    const defaultRoleID = values(currentThreadInfo.roles).find(role =>
-      roleIsDefaultRole(role),
-    )?.id;
-    invariant(defaultRoleID, 'Default role ID must exist');
-
-    const parentThreadID = currentThreadInfo.parentThreadID;
-    const parentThreadInfo = parentThreadID
-      ? utilities.threadInfos[parentThreadID]
-      : null;
-    if (parentThreadID && !parentThreadInfo) {
-      console.log(
-        `Parent thread with ID ${parentThreadID} was expected while adding ` +
-          'thread members but is missing from the store',
-      );
-    }
-    invariant(
-      !parentThreadInfo || parentThreadInfo.thick,
-      'Parent thread should be thick',
-    );
-
-    const { membershipPermissions } = createRoleAndPermissionForThickThreads(
-      currentThreadInfo.type,
-      currentThreadInfo.id,
-      defaultRoleID,
-      parentThreadInfo,
+    const { membershipPermissions, roleID } = createPermissionsForNewMembers(
+      currentThreadInfo,
+      utilities,
     );
 
     const memberTimestamps = { ...currentThreadInfo.timestamps.members };
@@ -122,7 +142,7 @@
       newMembers.push(
         minimallyEncodeMemberInfo<ThickMemberInfo>({
           id: userID,
-          role: defaultRoleID,
+          role: roleID,
           permissions: membershipPermissions,
           isSender: editorID === viewerID,
           subscription: joinThreadSubscription,
@@ -175,4 +195,5 @@
 export {
   addMembersSpec,
   createAddNewMembersMessageDataWithInfoFromDMOperation,
+  createPermissionsForNewMembers,
 };
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,6 +2,7 @@
 
 import uuid from 'uuid';
 
+import { createPermissionsForNewMembers } from './add-members-spec.js';
 import { createThickRawThreadInfo } from './create-thread-spec.js';
 import type {
   DMOperationSpec,
@@ -15,7 +16,12 @@
 import type { RawMessageInfo } from '../../types/message-types.js';
 import { messageTruncationStatus } from '../../types/message-types.js';
 import type { AddMembersMessageData } from '../../types/messages/add-members.js';
+import {
+  minimallyEncodeMemberInfo,
+  minimallyEncodeThreadCurrentUserInfo,
+} from '../../types/minimally-encoded-thread-permissions-types.js';
 import { joinThreadSubscription } from '../../types/subscription-types.js';
+import type { ThickMemberInfo } from '../../types/thread-types.js';
 import { updateTypes } from '../../types/update-types-enum.js';
 import { rawMessageInfoFromMessageData } from '../message-utils.js';
 import { userIsMember } from '../thread-utils.js';
@@ -56,7 +62,7 @@
       dmOperation: DMAddViewerToThreadMembersOperation,
       utilities: ProcessDMOperationUtilities,
     ) => {
-      const { time, messageID, addedUserIDs, existingThreadDetails } =
+      const { time, messageID, addedUserIDs, existingThreadDetails, editorID } =
         dmOperation;
       const { threadInfos } = utilities;
 
@@ -100,6 +106,54 @@
         }
       }
 
+      if (currentThreadInfo) {
+        const { membershipPermissions, roleID } =
+          createPermissionsForNewMembers(currentThreadInfo, utilities);
+
+        const newMemberInfos = newMembers.map(userID =>
+          minimallyEncodeMemberInfo<ThickMemberInfo>({
+            id: userID,
+            role: roleID,
+            permissions: membershipPermissions,
+            isSender: editorID === utilities.viewerID,
+            subscription: joinThreadSubscription,
+          }),
+        );
+
+        const resultThreadInfo = {
+          ...currentThreadInfo,
+          members: [...currentThreadInfo.members, ...newMemberInfos],
+          currentUser: minimallyEncodeThreadCurrentUserInfo({
+            role: roleID,
+            permissions: membershipPermissions,
+            subscription: joinThreadSubscription,
+            unread: true,
+          }),
+          timestamps: {
+            ...currentThreadInfo.timestamps,
+            members: {
+              ...currentThreadInfo.timestamps.members,
+              ...memberTimestamps,
+            },
+          },
+        };
+
+        const updateInfos = [
+          {
+            type: updateTypes.UPDATE_THREAD,
+            id: uuid.v4(),
+            time,
+            threadInfo: resultThreadInfo,
+          },
+        ];
+
+        return {
+          rawMessageInfos,
+          updateInfos,
+          blobOps: [],
+        };
+      }
+
       const resultThreadInfo = createThickRawThreadInfo(
         {
           ...existingThreadDetails,
@@ -132,7 +186,7 @@
         },
       ];
       return {
-        rawMessageInfos,
+        rawMessageInfos: [],
         updateInfos,
         blobOps: [],
       };