diff --git a/lib/hooks/relationship-hooks.js b/lib/hooks/relationship-hooks.js index 2cea2cf8d..58b84d0e4 100644 --- a/lib/hooks/relationship-hooks.js +++ b/lib/hooks/relationship-hooks.js @@ -1,383 +1,293 @@ // @flow 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 { type RelationshipAction, type RelationshipErrors, type RelationshipRequestUserInfo, type RelationshipRequest, relationshipActions, userRelationshipStatus, } 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' } | { +plan: 'send_to_existing_thick_thread', +thickThreadID: string, } | { +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, - +waitingForUserIDs: $ReadOnlyArray, - +resolve: RelationshipErrors => void, - +reject: Error => mixed, - }; - -const deviceListTimeout = 10 * 1000; // ten seconds - function useUpdateRelationships(): ( action: RelationshipAction, userIDs: $ReadOnlyArray, ) => Promise { const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const processAndSendDMOperation = useProcessAndSendDMOperation(); const sendRobotextToThickThread = React.useCallback( async ( userID: string, thickThreadID: string, relationshipOperation: RelationshipOperation, ): Promise => { if (!viewerID) { console.log('skipping sendRobotextToThickThread since logged out'); return; } const op = { type: 'update_relationship', threadID: thickThreadID, creatorID: viewerID, time: Date.now(), operation: relationshipOperation, targetUserID: userID, messageID: uuid.v4(), }; const opSpecification = { type: dmOperationSpecificationTypes.OUTBOUND, op, // We need to use a different mechanism than `all_thread_members` // because when creating a thread, the thread might not yet // be in the store. recipients: { type: 'some_users', userIDs: [viewerID, userID], }, }; await processAndSendDMOperation(opSpecification); }, [viewerID, processAndSendDMOperation], ); const updateRelationships = useLegacyAshoatKeyserverCall( serverUpdateRelationships, ); - const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos); const pendingToRealizedThreadIDs = useSelector((state: AppState) => pendingToRealizedThreadIDsSelector(state.threadStore.threadInfos), ); const userInfos = useSelector(state => state.userStore.userInfos); const rawThreadInfos = useSelector( (state: AppState) => state.threadStore.threadInfos, ); 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) => { if (!viewerID) { console.log( 'skipping updateRelationshipsAndSendRobotext since logged out', ); return {}; } + const { identities } = await findUserIdentities(userIDs); const planForUsers = new Map(); for (const userID of userIDs) { - const supportsThickThreads = userHasDeviceList(userID, auxUserInfos); - if (!supportsThickThreads) { + if (!identities[userID]) { planForUsers.set(userID, { plan: 'send_to_thin_thread' }); continue; } const pendingThreadID = getPendingThreadID( threadTypes.PERSONAL, [userID, viewerID], null, ); const realizedThreadID = pendingToRealizedThreadIDs.get(pendingThreadID); if (!realizedThreadID) { planForUsers.set(userID, { plan: 'send_to_new_thick_thread' }); continue; } const rawThreadInfo = rawThreadInfos[realizedThreadID]; if (!rawThreadInfo) { console.log( `could not find rawThreadInfo for realizedThreadID ` + `${realizedThreadID} found for pendingThreadID ` + pendingThreadID, ); planForUsers.set(userID, { plan: 'send_to_new_thick_thread' }); continue; } if (rawThreadInfo.type === threadTypes.PERSONAL) { planForUsers.set(userID, { plan: 'send_to_existing_thick_thread', thickThreadID: realizedThreadID, }); } else { planForUsers.set(userID, { plan: 'send_to_thin_thread' }); } } const usersForKeyserverCall: { [userID: string]: RelationshipRequestUserInfo, } = {}; for (const [userID, planForUser] of planForUsers) { usersForKeyserverCall[userID] = { createRobotextInThinThread: planForUser.plan === 'send_to_thin_thread', }; } let request: RelationshipRequest; if (action === 'farcaster' || action === 'friend') { request = { action, users: usersForKeyserverCall }; } else { request = { action, users: usersForKeyserverCall }; } const keyserverResultPromise = updateRelationships(request); const thickThreadPromises: Array> = []; for (const [userID, planForUser] of planForUsers) { if (planForUser.plan === 'send_to_thin_thread') { // Keyserver calls handles creating robotext for thin threads continue; } if ( action !== relationshipActions.FRIEND && action !== relationshipActions.FARCASTER_MUTUAL ) { // We only create robotext for FRIEND and FARCASTER_MUTUAL continue; } const relationshipStatus = userInfos[userID]?.relationshipStatus; let relationshipOperation; if (action === relationshipActions.FARCASTER_MUTUAL) { relationshipOperation = 'farcaster_mutual'; } else if ( relationshipStatus === userRelationshipStatus.FRIEND || relationshipStatus === userRelationshipStatus.REQUEST_RECEIVED ) { relationshipOperation = 'request_accepted'; } else { relationshipOperation = 'request_sent'; } if (planForUser.plan === 'send_to_existing_thick_thread') { const { thickThreadID } = planForUser; thickThreadPromises.push( sendRobotextToThickThread( userID, thickThreadID, relationshipOperation, ), ); continue; } const createThickThreadAndSendRobotextPromise = (async () => { const thickThreadID = await createNewThickThread({ type: threadTypes.PERSONAL, initialMemberIDs: [userID], }); return await sendRobotextToThickThread( userID, thickThreadID, relationshipOperation, ); })(); thickThreadPromises.push(createThickThreadAndSendRobotextPromise); } const [keyserverResult] = await Promise.all([ keyserverResultPromise, Promise.all(thickThreadPromises), ]); return keyserverResult; }, [ viewerID, updateRelationships, - auxUserInfos, + findUserIdentities, pendingToRealizedThreadIDs, sendRobotextToThickThread, userInfos, rawThreadInfos, createNewThickThread, ], ); - const [step, setStep] = React.useState(); - - // 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(false); const usingOlmViaTunnelbrokerForDMs = useAllowOlmViaTunnelbrokerForDMs(); - const getAndUpdateDeviceListsForUsers = useGetAndUpdateDeviceListsForUsers(); const coreFunctionality = React.useCallback( async (action: RelationshipAction, userIDs: $ReadOnlyArray) => { - // 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 = []; - 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 = []; - for (const userID of missingDeviceListsUserIDs) { - if (deviceLists[userID] && deviceLists[userID].devices.length > 0) { - waitingForUserIDs.push(userID); - } - } - - if (waitingForUserIDs.length > 0) { - const nextStepPromise = new Promise( - (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) => { - 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], ); } export { useUpdateRelationships };