Skip to content

Commit

Permalink
Implement DMActivityHandler
Browse files Browse the repository at this point in the history
Summary: This differential implements DMActivityHandler component. Its role is the same as ActivityHandler but it works thick threads. I opted for new compoment instead of modifying ActivityHandler since we only need one component for thick threads while there are many ActivityHandlers (one for each keyserver). Therefore I couldn't see an easy way to reuse ActivityHandler.

Test Plan:
1. Create thick thread between users A and B.
2. Log in as user A on two devices (on web and on native) and log in as user B on third device.
3. Send message from B to A. Open the thread as A on web. Ensure rescinding happens on native.
4. Open the thread as A on native. Change opened thread as A on web. Send messages from B to A. Ensure that thread remains read on web.

Reviewers: tomek, kamil, inka

Reviewed By: tomek

Subscribers: ashoat

Differential Revision: https://phab.comm.dev/D13246
  • Loading branch information
marcinwasowicz committed Sep 9, 2024
1 parent af90fa4 commit 8c1cd8b
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 0 deletions.
107 changes: 107 additions & 0 deletions lib/handlers/dm-activity-handler.js
Original file line number Diff line number Diff line change
@@ -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<ActivityUpdateSuccessPayload> {
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<?string>();
const prevActiveThreadLatestMessageRef = React.useRef<?string>();

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;
27 changes: 27 additions & 0 deletions native/components/dm-activity-handler.react.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions native/root.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import AccessTokenHandler from './components/access-token-handler.react.js';
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';
Expand Down Expand Up @@ -373,6 +374,7 @@ function Root() {
detectUnsupervisedBackgroundRef
}
/>
<DMActivityHandler />
<VersionSupportedChecker />
<PlatformDetailsSynchronizer />
<BackgroundIdentityLoginHandler />
Expand Down
2 changes: 2 additions & 0 deletions web/app.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { EditModalProvider } from './chat/edit-message-provider.js';
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';
Expand Down Expand Up @@ -256,6 +257,7 @@ class App extends React.PureComponent<Props> {
<TunnelbrokerDeviceTokenHandler />
<FarcasterDataHandler />
<AutoJoinCommunityHandler />
<DMActivityHandler />
{content}
</ChatMentionContextProvider>
</MessageSearchStateProvider>
Expand Down
27 changes: 27 additions & 0 deletions web/components/dm-activity-handler.react.js
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 8c1cd8b

Please sign in to comment.