diff --git a/lib/shared/dm-ops/process-dm-ops.js b/lib/shared/dm-ops/process-dm-ops.js
--- a/lib/shared/dm-ops/process-dm-ops.js
+++ b/lib/shared/dm-ops/process-dm-ops.js
@@ -16,7 +16,14 @@
 import { useProcessBlobHolders } from '../../actions/holder-actions.js';
 import { processNewUserIDsActionType } from '../../actions/user-actions.js';
 import { useDebugLogs } from '../../components/debug-logs-context.js';
+import { useGetLatestMessageEdit } from '../../hooks/latest-message-edit.js';
 import { useDispatchWithMetadata } from '../../hooks/ops-hooks.js';
+import { mergeUpdatesWithMessageInfos } from '../../reducers/message-reducer.js';
+import {
+  type MessageNotifyType,
+  messageNotifyTypes,
+} from '../../shared/messages/message-spec.js';
+import { messageSpecs } from '../../shared/messages/message-specs.js';
 import {
   usePeerToPeerCommunication,
   type ProcessOutboundP2PMessagesResult,
@@ -26,9 +33,12 @@
   processDMOpsActionType,
   queueDMOpsActionType,
   dmOperationValidator,
+  type ProcessDMOpsPayload,
 } from '../../types/dm-ops.js';
+import type { RawMessageInfo } from '../../types/message-types.js';
 import type { DispatchMetadata } from '../../types/redux-types.js';
 import type { OutboundP2PMessage } from '../../types/sqlite-types.js';
+import type { ClientUpdateInfo } from '../../types/update-types.js';
 import { extractUserIDsFromPayload } from '../../utils/conversion-utils.js';
 import { useSelector, useDispatch } from '../../utils/redux-utils.js';
 
@@ -41,6 +51,7 @@
   const processBlobHolders = useProcessBlobHolders();
   const createMessagesToPeersFromDMOp = useCreateMessagesToPeersFromDMOp();
   const confirmPeerToPeerMessage = useConfirmPeerToPeerMessage();
+  const getMessageNotifyTypes = useGetMessageNotifyTypes();
 
   const dispatch = useDispatch();
 
@@ -109,17 +120,16 @@
           dmOp.type
         ].processDMOperation(dmOp, utilities);
 
+        const payload: ProcessDMOpsPayload = {
+          rawMessageInfos: [],
+          updateInfos: [],
+          outboundP2PMessages,
+          composableMessageID,
+          notificationsCreationData,
+          messagesNotifyTypes: {},
+        };
         dispatchWithMetadata(
-          {
-            type: processDMOpsActionType,
-            payload: {
-              rawMessageInfos: [],
-              updateInfos: [],
-              outboundP2PMessages,
-              composableMessageID,
-              notificationsCreationData,
-            },
-          },
+          { type: processDMOpsActionType, payload },
           dispatchMetadata,
         );
 
@@ -214,6 +224,12 @@
         notificationsCreationData,
       } = await dmOpSpecs[dmOp.type].processDMOperation(dmOp, utilities);
 
+      const messagesNotifyTypes = await getMessageNotifyTypes(
+        viewerID,
+        rawMessageInfos,
+        updateInfos,
+      );
+
       const outboundNotificationsCreationData =
         dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND
           ? notificationsCreationData
@@ -236,17 +252,16 @@
         .filter(Boolean);
       void processBlobHolders(holderOps);
 
+      const payload: ProcessDMOpsPayload = {
+        rawMessageInfos,
+        updateInfos,
+        outboundP2PMessages,
+        composableMessageID,
+        notificationsCreationData: outboundNotificationsCreationData,
+        messagesNotifyTypes,
+      };
       dispatchWithMetadata(
-        {
-          type: processDMOpsActionType,
-          payload: {
-            rawMessageInfos,
-            updateInfos,
-            outboundP2PMessages,
-            composableMessageID,
-            notificationsCreationData: outboundNotificationsCreationData,
-          },
-        },
+        { type: processDMOpsActionType, payload },
         dispatchMetadata,
       );
     },
@@ -258,6 +273,7 @@
       createMessagesToPeersFromDMOp,
       confirmPeerToPeerMessage,
       dispatch,
+      getMessageNotifyTypes,
     ],
   );
 }
@@ -287,6 +303,7 @@
   const { processOutboundMessages } = usePeerToPeerCommunication();
   const localMessageInfos = useSelector(state => state.messageStore.local);
   const createMessagesToPeersFromDMOp = useCreateMessagesToPeersFromDMOp();
+  const getMessageNotifyTypes = useGetMessageNotifyTypes();
 
   return React.useCallback(
     async (
@@ -338,20 +355,23 @@
       const { rawMessageInfos, updateInfos, notificationsCreationData } =
         await dmOpSpecs[op.type].processDMOperation(op, utilities);
 
+      const messagesNotifyTypes = await getMessageNotifyTypes(
+        viewerID,
+        rawMessageInfos,
+        updateInfos,
+      );
+
+      const payload: ProcessDMOpsPayload = {
+        rawMessageInfos,
+        updateInfos,
+        outboundP2PMessages,
+        composableMessageID,
+        notificationsCreationData,
+        messagesNotifyTypes,
+      };
       dispatchWithMetadata(
-        {
-          type: processDMOpsActionType,
-          payload: {
-            rawMessageInfos,
-            updateInfos,
-            outboundP2PMessages,
-            composableMessageID,
-            notificationsCreationData,
-          },
-        },
-        {
-          dmOpID,
-        },
+        { type: processDMOpsActionType, payload },
+        { dmOpID },
       );
 
       try {
@@ -373,10 +393,50 @@
       createMessagesToPeersFromDMOp,
       dispatchWithMetadata,
       processOutboundMessages,
+      getMessageNotifyTypes,
     ],
   );
 }
 
+function useGetMessageNotifyTypes(): (
+  viewerID: string,
+  rawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
+  updateInfos: $ReadOnlyArray<ClientUpdateInfo>,
+) => Promise<{ [messageID: string]: MessageNotifyType }> {
+  const getLatestMessageEdit = useGetLatestMessageEdit();
+  return React.useCallback(
+    async (viewerID, rawMessageInfos, updateInfos) => {
+      const { rawMessageInfos: allNewMessageInfos } =
+        mergeUpdatesWithMessageInfos(rawMessageInfos, updateInfos);
+
+      const messageNotifyTypePairPromises = allNewMessageInfos.map(
+        async (rawMessageInfo: RawMessageInfo) => {
+          const { id, type } = rawMessageInfo;
+          invariant(id, 'Thick thread RawMessageInfos should always have ID');
+          const { getMessageNotifyType } = messageSpecs[type];
+
+          let messageNotifyType = messageNotifyTypes.SET_UNREAD;
+          if (getMessageNotifyType) {
+            messageNotifyType = await getMessageNotifyType(rawMessageInfo, {
+              notifTargetUserID: viewerID,
+              userNotMemberOfSubthreads: new Set(),
+              fetchMessageInfoByID: getLatestMessageEdit,
+            });
+          }
+
+          return [id, messageNotifyType];
+        },
+      );
+
+      const messageNotifyTypePairs = await Promise.all(
+        messageNotifyTypePairPromises,
+      );
+      return Object.fromEntries(messageNotifyTypePairs);
+    },
+    [getLatestMessageEdit],
+  );
+}
+
 export {
   useProcessDMOperation,
   useProcessAndSendDMOperation,
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
@@ -24,6 +24,7 @@
   type ThreadTimestamps,
 } from './thread-types.js';
 import type { ClientUpdateInfo } from './update-types.js';
+import type { MessageNotifyType } from '../shared/messages/message-spec.js';
 import { values } from '../utils/objects.js';
 import {
   tColor,
@@ -530,6 +531,7 @@
   // were queued on Tunnelbroker.
   +composableMessageID: ?string,
   +notificationsCreationData: ?NotificationsCreationData,
+  +messagesNotifyTypes: { +[messageID: string]: MessageNotifyType },
 };
 
 export const queueDMOpsActionType = 'QUEUE_DM_OPS';