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,107 @@ +// @flow +import invariant from 'invariant'; +import * as React from 'react'; + +import { updateActivityActionTypes } from '../actions/activity-actions.js'; +import { + type OutboundDMOperationSpecification, + 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 { ActivityUpdateSuccessPayload } from '../types/activity-types.js'; +import type { DMChangeThreadReadStatusOperation } from '../types/dm-ops.js'; +import type { RawThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; +import { threadTypeIsThick } from '../types/thread-types-enum.js'; +import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; +import { useSelector } from '../utils/redux-utils.js'; + +function useUpdateDMActivity(): ( + viewerID: string, + activeThreadInfo: RawThreadInfo, +) => Promise { + const processAndSendDMOperation = useProcessAndSendDMOperation(); + return React.useCallback( + async (viewerID: string, activeThreadInfo: RawThreadInfo) => { + invariant( + threadTypeIsThick(activeThreadInfo.type), + 'thread must be thick', + ); + const op: DMChangeThreadReadStatusOperation = { + type: 'change_thread_read_status', + time: Date.now(), + threadID: activeThreadInfo.id, + creatorID: viewerID, + unread: false, + }; + + const opSpecification: OutboundDMOperationSpecification = { + type: dmOperationSpecificationTypes.OUTBOUND, + op, + recipients: { type: 'self_devices' }, + }; + + await processAndSendDMOperation(opSpecification); + return { activityUpdates: {}, result: { unfocusedToUnread: [] } }; + }, + [processAndSendDMOperation], + ); +} + +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, + ); + const updateDMActivity = useUpdateDMActivity(); + const dispatchActionPromise = useDispatchActionPromise(); + + 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; + } + + void dispatchActionPromise( + updateActivityActionTypes, + updateDMActivity(viewerID, activeThreadInfo), + ); + }, [ + updateDMActivity, + dispatchActionPromise, + 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;