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
@@ -1,14 +1,27 @@
 // @flow
 
+import invariant from 'invariant';
+import * as React from 'react';
 import uuid from 'uuid';
 
 import { dmOpSpecs } from './dm-op-specs.js';
-import type { DMOperation } from '../../types/dm-ops.js';
+import { useProcessAndSendDMOperation } from './process-dm-ops.js';
+import type {
+  CreateThickRawThreadInfoInput,
+  DMAddMembersOperation,
+  DMAddViewerToThreadMembersOperation,
+  DMOperation,
+} from '../../types/dm-ops.js';
+import type { ThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js';
 import type { InboundActionMetadata } from '../../types/redux-types.js';
 import {
   outboundP2PMessageStatuses,
   type OutboundP2PMessage,
 } from '../../types/sqlite-types.js';
+import {
+  assertThickThreadType,
+  thickThreadTypes,
+} from '../../types/thread-types-enum.js';
 import type { RawThreadInfos } from '../../types/thread-types.js';
 import {
   type DMOperationP2PMessage,
@@ -16,6 +29,7 @@
 } from '../../types/tunnelbroker/user-actions-peer-to-peer-message-types.js';
 import type { CurrentUserInfo } from '../../types/user-types.js';
 import { getContentSigningKey } from '../../utils/crypto-utils.js';
+import { useSelector } from '../../utils/redux-utils.js';
 
 function generateMessagesToPeers(
   message: DMOperation,
@@ -115,4 +129,103 @@
   return generateMessagesToPeers(operation.op, targetPeers);
 }
 
-export { createMessagesToPeersFromDMOp };
+function getCreateThickRawThreadInfoInputFromThreadInfo(
+  threadInfo: ThreadInfo,
+): CreateThickRawThreadInfoInput {
+  const roleID = Object.keys(threadInfo.roles).pop();
+  const thickThreadType = assertThickThreadType(threadInfo.type);
+  return {
+    threadID: threadInfo.id,
+    threadType: thickThreadType,
+    creationTime: threadInfo.creationTime,
+    parentThreadID: threadInfo.parentThreadID,
+    allMemberIDs: threadInfo.members.map(member => member.id),
+    roleID,
+    unread: !!threadInfo.currentUser.unread,
+    name: threadInfo.name,
+    avatar: threadInfo.avatar,
+    description: threadInfo.description,
+    color: threadInfo.color,
+    containingThreadID: threadInfo.containingThreadID,
+    sourceMessageID: threadInfo.sourceMessageID,
+    repliesCount: threadInfo.repliesCount,
+    pinnedCount: threadInfo.pinnedCount,
+  };
+}
+
+function useAddDMThreadMembers(): (
+  newMemberIDs: $ReadOnlyArray<string>,
+  threadInfo: ThreadInfo,
+) => Promise<void> {
+  const viewerID = useSelector(
+    state => state.currentUserInfo && state.currentUserInfo.id,
+  );
+  const processAndSendDMOperation = useProcessAndSendDMOperation();
+  const threadInfos = useSelector(state => state.threadStore.threadInfos);
+
+  return React.useCallback(
+    async (newMemberIDs: $ReadOnlyArray<string>, threadInfo: ThreadInfo) => {
+      const existingThreadDetails =
+        getCreateThickRawThreadInfoInputFromThreadInfo(threadInfo);
+
+      invariant(viewerID, 'viewerID should be set');
+      const addViewerToThreadMembersOperation: DMAddViewerToThreadMembersOperation =
+        {
+          type: 'add_viewer_to_thread_members',
+          existingThreadDetails,
+          editorID: viewerID,
+          time: Date.now(),
+          messageID: uuid.v4(),
+          addedUserIDs: newMemberIDs,
+        };
+      const viewerOperationSpecification: OutboundDMOperationSpecification = {
+        type: dmOperationSpecificationTypes.OUTBOUND,
+        op: addViewerToThreadMembersOperation,
+        recipients: {
+          type: 'some_users',
+          userIDs: newMemberIDs,
+        },
+        sendOnly: true,
+      };
+
+      invariant(viewerID, 'viewerID should be set');
+      const addMembersOperation: DMAddMembersOperation = {
+        type: 'add_members',
+        threadID: threadInfo.id,
+        editorID: viewerID,
+        time: Date.now(),
+        messageID: uuid.v4(),
+        addedUserIDs: newMemberIDs,
+      };
+      const newMemberIDsSet = new Set<string>(newMemberIDs);
+      const recipientsThreadID =
+        threadInfo.type === thickThreadTypes.THICK_SIDEBAR &&
+        threadInfo.parentThreadID
+          ? threadInfo.parentThreadID
+          : threadInfo.id;
+
+      const existingMembers =
+        threadInfos[recipientsThreadID]?.members
+          ?.map(member => member.id)
+          ?.filter(memberID => !newMemberIDsSet.has(memberID)) ?? [];
+
+      const addMembersOperationSpecification: OutboundDMOperationSpecification =
+        {
+          type: dmOperationSpecificationTypes.OUTBOUND,
+          op: addMembersOperation,
+          recipients: {
+            type: 'some_users',
+            userIDs: existingMembers,
+          },
+        };
+
+      await Promise.all([
+        processAndSendDMOperation(viewerOperationSpecification),
+        processAndSendDMOperation(addMembersOperationSpecification),
+      ]);
+    },
+    [processAndSendDMOperation, threadInfos, viewerID],
+  );
+}
+
+export { createMessagesToPeersFromDMOp, useAddDMThreadMembers };