diff --git a/lib/handlers/dm-activity-handler.js b/lib/handlers/dm-activity-handler.js
new file mode 100644
--- /dev/null
+++ b/lib/handlers/dm-activity-handler.js
@@ -0,0 +1,75 @@
+// @flow
+import * as React from 'react';
+
+import type { OutboundDMOperationSpecification } from '../shared/dm-ops/dm-op-utils';
+import { dmOperationSpecificationTypes } from '../shared/dm-ops/dm-op-utils.js';
+import { useProcessAndSendDMOperation } from '../shared/dm-ops/process-dm-ops.js';
+import { getMostRecentNonLocalMessageID } from '../shared/message-utils.js';
+import { threadIsPending } from '../shared/thread-utils.js';
+import type { DMChangeThreadReadStatusOperation } from '../types/dm-ops.js';
+import { threadTypeIsThick } from '../types/thread-types-enum.js';
+import { useSelector } from '../utils/redux-utils.js';
+
+function useDMActivityHandler(activeThread: ?string): void {
+ const activeThreadInfo = useSelector(state =>
+ activeThread ? state.threadStore.threadInfos[activeThread] : null,
+ );
+ const activeThreadLatestMessage = useSelector(state =>
+ activeThread
+ ? getMostRecentNonLocalMessageID(activeThread, state.messageStore)
+ : null,
+ );
+ const processAndSendDMOperation = useProcessAndSendDMOperation();
+
+ const prevActiveThreadRef = React.useRef();
+ const prevActiveThreadLatestMessageRef = React.useRef();
+
+ const viewerID = useSelector(
+ state => state.currentUserInfo && state.currentUserInfo.id,
+ );
+
+ React.useEffect(() => {
+ const prevActiveThread = prevActiveThreadRef.current;
+ const prevActiveThreadLatestMessage =
+ prevActiveThreadLatestMessageRef.current;
+
+ prevActiveThreadRef.current = activeThread;
+ prevActiveThreadLatestMessageRef.current = activeThreadLatestMessage;
+
+ if (
+ !viewerID ||
+ !activeThread ||
+ !activeThreadInfo ||
+ !threadTypeIsThick(activeThreadInfo.type) ||
+ threadIsPending(activeThread) ||
+ (activeThread === prevActiveThread &&
+ activeThreadLatestMessage === prevActiveThreadLatestMessage)
+ ) {
+ return;
+ }
+
+ const op: DMChangeThreadReadStatusOperation = {
+ type: 'change_thread_read_status',
+ time: Date.now(),
+ threadID: activeThread,
+ creatorID: viewerID,
+ unread: false,
+ };
+
+ const opSpecification: OutboundDMOperationSpecification = {
+ type: dmOperationSpecificationTypes.OUTBOUND,
+ op,
+ recipients: { type: 'self_devices' },
+ };
+
+ void processAndSendDMOperation(opSpecification);
+ }, [
+ activeThread,
+ viewerID,
+ processAndSendDMOperation,
+ activeThreadInfo,
+ activeThreadLatestMessage,
+ ]);
+}
+
+export default useDMActivityHandler;
diff --git a/native/components/dm-activity-handler.react.js b/native/components/dm-activity-handler.react.js
new file mode 100644
--- /dev/null
+++ b/native/components/dm-activity-handler.react.js
@@ -0,0 +1,27 @@
+// @flow
+import * as React from 'react';
+
+import useDMActivityHandler from 'lib/handlers/dm-activity-handler.js';
+import { isLoggedIn } from 'lib/selectors/user-selectors.js';
+
+import { activeMessageListSelector } from '../navigation/nav-selectors.js';
+import { NavContext } from '../navigation/navigation-context.js';
+import { useSelector } from '../redux/redux-utils.js';
+
+function DMActivityHandler(): React.Node {
+ const active = useSelector(
+ state => isLoggedIn(state) && state.lifecycleState !== 'background',
+ );
+ const navContext = React.useContext(NavContext);
+ const activeThread = React.useMemo(() => {
+ if (!active) {
+ return null;
+ }
+ return activeMessageListSelector(navContext);
+ }, [active, navContext]);
+
+ useDMActivityHandler(activeThread);
+ return null;
+}
+
+export default DMActivityHandler;
diff --git a/native/root.react.js b/native/root.react.js
--- a/native/root.react.js
+++ b/native/root.react.js
@@ -54,6 +54,7 @@
import { AutoJoinCommunityHandler } from './components/auto-join-community-handler.react.js';
import BackgroundIdentityLoginHandler from './components/background-identity-login-handler.react.js';
import ConnectFarcasterAlertHandler from './components/connect-farcaster-alert-handler.react.js';
+import DMActivityHandler from './components/dm-activity-handler.react.js';
import { FeatureFlagsProvider } from './components/feature-flags-provider.react.js';
import { NUXTipsContextProvider } from './components/nux-tips-context.react.js';
import PersistedStateGate from './components/persisted-state-gate.js';
@@ -373,6 +374,7 @@
detectUnsupervisedBackgroundRef
}
/>
+
diff --git a/web/app.react.js b/web/app.react.js
--- a/web/app.react.js
+++ b/web/app.react.js
@@ -55,6 +55,7 @@
import { MemberListSidebarProvider } from './chat/member-list-sidebar/member-list-sidebar-provider.react.js';
import { AutoJoinCommunityHandler } from './components/auto-join-community-handler.react.js';
import CommunitiesRefresher from './components/communities-refresher.react.js';
+import DMActivityHandler from './components/dm-activity-handler.react.js';
import LogOutIfMissingCSATHandler from './components/log-out-if-missing-csat-handler.react.js';
import NavigationArrows from './components/navigation-arrows.react.js';
import MinVersionHandler from './components/version-handler.react.js';
@@ -256,6 +257,7 @@
+
{content}
diff --git a/web/components/dm-activity-handler.react.js b/web/components/dm-activity-handler.react.js
new file mode 100644
--- /dev/null
+++ b/web/components/dm-activity-handler.react.js
@@ -0,0 +1,27 @@
+// @flow
+import * as React from 'react';
+
+import useDMActivityHandler from 'lib/handlers/dm-activity-handler.js';
+import { isLoggedIn } from 'lib/selectors/user-selectors.js';
+
+import { useSelector } from '../redux/redux-utils.js';
+import { activeThreadSelector } from '../selectors/nav-selectors.js';
+
+function DMActivityHandler(): React.Node {
+ const active = useSelector(
+ state => isLoggedIn(state) && state.lifecycleState !== 'background',
+ );
+ const reduxActiveThread = useSelector(activeThreadSelector);
+ const windowActive = useSelector(state => state.windowActive);
+ const activeThread = React.useMemo(() => {
+ if (!active || !windowActive) {
+ return null;
+ }
+ return reduxActiveThread;
+ }, [active, windowActive, reduxActiveThread]);
+
+ useDMActivityHandler(activeThread);
+ return null;
+}
+
+export default DMActivityHandler;