Skip to content

Commit

Permalink
[lib] Get rid of effect in useUpdateRelationships
Browse files Browse the repository at this point in the history
Summary:
Kamil's work in D13486 lets us greatly simplify this code.

Depends on D13489

Test Plan:
I tested with two devices: one iOS simulator, one physical Android. I followed the steps below twice, with iOS / Android alternating roles:

1. User A and user B create accounts
2. User B backgrounds the app
3. User A searches for user B and sends them a friend request
4. Confirm notifs are received. Confirm chat shows up as DM in UI
5. User A backgrounds the app
6. User B accepts the friend request
7. Confirm notifs received

Reviewers: kamil

Reviewed By: kamil

Subscribers: tomek

Differential Revision: https://phab.comm.dev/D13496
  • Loading branch information
Ashoat committed Sep 30, 2024
1 parent 1fe84ce commit fc060e1
Showing 1 changed file with 33 additions and 123 deletions.
156 changes: 33 additions & 123 deletions lib/hooks/relationship-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@ import * as React from 'react';
import uuid from 'uuid';

import { useAllowOlmViaTunnelbrokerForDMs } from './flag-hooks.js';
import { useGetAndUpdateDeviceListsForUsers } from './peer-list-hooks.js';
import { useNewThickThread } from './thread-hooks.js';
import { useFindUserIdentities } from '../actions/find-user-identities-actions.js';
import { updateRelationships as serverUpdateRelationships } from '../actions/relationship-actions.js';
import { useLegacyAshoatKeyserverCall } from '../keyserver-conn/legacy-keyserver-call.js';
import { pendingToRealizedThreadIDsSelector } from '../selectors/thread-selectors.js';
import { dmOperationSpecificationTypes } from '../shared/dm-ops/dm-op-utils.js';
import { useProcessAndSendDMOperation } from '../shared/dm-ops/process-dm-ops.js';
import {
userHasDeviceList,
getPendingThreadID,
} from '../shared/thread-utils.js';
import { getPendingThreadID } from '../shared/thread-utils.js';
import type { RelationshipOperation } from '../types/messages/update-relationship.js';
import type { AppState } from '../types/redux-types.js';
import {
Expand All @@ -27,7 +24,6 @@ import {
} from '../types/relationship-types.js';
import { threadTypes } from '../types/thread-types-enum.js';
import { useSelector } from '../utils/redux-utils.js';
import sleep from '../utils/sleep.js';

type RobotextPlanForUser =
| { +plan: 'send_to_thin_thread' }
Expand All @@ -37,23 +33,6 @@ type RobotextPlanForUser =
}
| { +plan: 'send_to_new_thick_thread' };

// We can't call processAndSendDMOperation until device lists are in
// AuxUserStore, but this hook needs to support users who haven't been fetched
// yet. We implement an effect that watches AuxUserStore after a fetch, so we
// know when we're ready to call processAndSendDMOperation.
type Step =
| { +step: 'ongoing' }
| {
+step: 'waiting_for_updated_device_lists',
+action: RelationshipAction,
+userIDs: $ReadOnlyArray<string>,
+waitingForUserIDs: $ReadOnlyArray<string>,
+resolve: RelationshipErrors => void,
+reject: Error => mixed,
};

const deviceListTimeout = 10 * 1000; // ten seconds

function useUpdateRelationships(): (
action: RelationshipAction,
userIDs: $ReadOnlyArray<string>,
Expand Down Expand Up @@ -100,7 +79,6 @@ function useUpdateRelationships(): (
const updateRelationships = useLegacyAshoatKeyserverCall(
serverUpdateRelationships,
);
const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos);
const pendingToRealizedThreadIDs = useSelector((state: AppState) =>
pendingToRealizedThreadIDsSelector(state.threadStore.threadInfos),
);
Expand All @@ -110,9 +88,8 @@ function useUpdateRelationships(): (
);
const createNewThickThread = useNewThickThread();

// This callback contains the core of the logic. We extract it here because
// before we run it, we need to make sure auxUserInfos is correctly populated,
// and that might require waiting on a Redux action to be reduced
const findUserIdentities = useFindUserIdentities();

const updateRelationshipsAndSendRobotext = React.useCallback(
async (action: RelationshipAction, userIDs: $ReadOnlyArray<string>) => {
if (!viewerID) {
Expand All @@ -121,10 +98,10 @@ function useUpdateRelationships(): (
);
return {};
}
const { identities } = await findUserIdentities(userIDs);
const planForUsers = new Map<string, RobotextPlanForUser>();
for (const userID of userIDs) {
const supportsThickThreads = userHasDeviceList(userID, auxUserInfos);
if (!supportsThickThreads) {
if (!identities[userID]) {
planForUsers.set(userID, { plan: 'send_to_thin_thread' });
continue;
}
Expand Down Expand Up @@ -243,7 +220,7 @@ function useUpdateRelationships(): (
[
viewerID,
updateRelationships,
auxUserInfos,
findUserIdentities,
pendingToRealizedThreadIDs,
sendRobotextToThickThread,
userInfos,
Expand All @@ -252,131 +229,64 @@ function useUpdateRelationships(): (
],
);

const [step, setStep] = React.useState<?Step>();

// This hook watches AuxUserStore after a fetch to make sure we're ready to
// call processAndSendDMOperation. We can't do that from the returned
// callback, as it will have an old version of auxUserInfos bound into it.
React.useEffect(() => {
if (step?.step !== 'waiting_for_updated_device_lists') {
return;
}
const { action, userIDs, waitingForUserIDs, resolve, reject } = step;
for (const userID of waitingForUserIDs) {
const supportsThickThreads = userHasDeviceList(userID, auxUserInfos);
if (!supportsThickThreads) {
// It's safe to wait until every single user ID in waitingForUserIDs
// passes this check because we make the same check when populating
// waitingForUserIDs in the callback below
return;
}
}
setStep({ step: 'ongoing' });
updateRelationshipsAndSendRobotext(action, userIDs).then(resolve, reject);
}, [step, auxUserInfos, updateRelationshipsAndSendRobotext]);
const [inProgress, setInProgress] = React.useState<boolean>(false);

const usingOlmViaTunnelbrokerForDMs = useAllowOlmViaTunnelbrokerForDMs();
const getAndUpdateDeviceListsForUsers = useGetAndUpdateDeviceListsForUsers();

const coreFunctionality = React.useCallback(
async (action: RelationshipAction, userIDs: $ReadOnlyArray<string>) => {
// We only need to create robotext for FRIEND and FARCASTER_MUTUAL,
// so we skip the complexity below for other RelationshipActions
if (
!usingOlmViaTunnelbrokerForDMs ||
(action !== relationshipActions.FRIEND &&
action !== relationshipActions.FARCASTER_MUTUAL)
usingOlmViaTunnelbrokerForDMs &&
(action === relationshipActions.FRIEND ||
action === relationshipActions.FARCASTER_MUTUAL)
) {
let request: RelationshipRequest;
if (action === 'farcaster' || action === 'friend') {
const users = Object.fromEntries(
userIDs.map(userID => [
userID,
{
createRobotextInThinThread: true,
},
]),
);
request = { action, users };
} else {
const users = Object.fromEntries(userIDs.map(userID => [userID, {}]));
request = { action, users };
}
return await updateRelationships(request);
}

const missingDeviceListsUserIDs: Array<string> = [];
for (const userID of userIDs) {
const supportsThickThreads = userHasDeviceList(userID, auxUserInfos);
if (!supportsThickThreads) {
missingDeviceListsUserIDs.push(userID);
}
// We only need to create robotext for FRIEND and FARCASTER_MUTUAL, so
// we skip the complexity of updateRelationshipsAndSendRobotext for
// other RelationshipActions
return await updateRelationshipsAndSendRobotext(action, userIDs);
}

if (missingDeviceListsUserIDs.length > 0) {
const deviceLists = await getAndUpdateDeviceListsForUsers(
missingDeviceListsUserIDs,
true,
);

const waitingForUserIDs: Array<string> = [];
for (const userID of missingDeviceListsUserIDs) {
if (deviceLists[userID] && deviceLists[userID].devices.length > 0) {
waitingForUserIDs.push(userID);
}
}

if (waitingForUserIDs.length > 0) {
const nextStepPromise = new Promise<RelationshipErrors>(
(resolve, reject) => {
setStep({
step: 'waiting_for_updated_device_lists',
action,
userIDs,
waitingForUserIDs,
resolve,
reject,
});
let request: RelationshipRequest;
if (action === 'farcaster' || action === 'friend') {
const users = Object.fromEntries(
userIDs.map(userID => [
userID,
{
createRobotextInThinThread: true,
},
);
return await Promise.race([
nextStepPromise,
(async () => {
await sleep(deviceListTimeout);
throw new Error(`Fetch device lists timed out`);
})(),
]);
}
]),
);
request = { action, users };
} else {
const users = Object.fromEntries(userIDs.map(userID => [userID, {}]));
request = { action, users };
}

return await updateRelationshipsAndSendRobotext(action, userIDs);
return await updateRelationships(request);
},
[
getAndUpdateDeviceListsForUsers,
updateRelationshipsAndSendRobotext,
updateRelationships,
auxUserInfos,
usingOlmViaTunnelbrokerForDMs,
],
);

return React.useCallback(
async (action: RelationshipAction, userIDs: $ReadOnlyArray<string>) => {
if (step) {
if (inProgress) {
console.log(
'updateRelationships called from same component before last call ' +
'finished. ignoring',
);
return {};
}
setStep({ step: 'ongoing' });
setInProgress(true);
try {
return await coreFunctionality(action, userIDs);
} finally {
setStep(null);
setInProgress(false);
}
},
[step, coreFunctionality],
[inProgress, coreFunctionality],
);
}

Expand Down

0 comments on commit fc060e1

Please sign in to comment.