diff --git a/lib/handlers/user-infos-handler.react.js b/lib/handlers/user-infos-handler.react.js index 5046b1d2f..e5a8e9261 100644 --- a/lib/handlers/user-infos-handler.react.js +++ b/lib/handlers/user-infos-handler.react.js @@ -1,150 +1,167 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { updateRelationships, updateRelationshipsActionTypes, } from '../actions/relationship-actions.js'; import { useFindUserIdentities, findUserIdentitiesActionTypes, } from '../actions/user-actions.js'; import { useIsLoggedInToAuthoritativeKeyserver } from '../hooks/account-hooks.js'; import { useGetAndUpdateDeviceListsForUsers } from '../hooks/peer-list-hooks.js'; import { useLegacyAshoatKeyserverCall } from '../keyserver-conn/legacy-keyserver-call.js'; import { usersWithMissingDeviceListSelector } from '../selectors/user-selectors.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import { relationshipActions } from '../types/relationship-types.js'; import { getMessageForException } from '../utils/errors.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector } from '../utils/redux-utils.js'; import { relyingOnAuthoritativeKeyserver, usingCommServicesAccessToken, } from '../utils/services-utils.js'; function UserInfosHandler(): React.Node { const client = React.useContext(IdentityClientContext); invariant(client, 'Identity context should be set'); const { getAuthMetadata } = client; const userInfos = useSelector(state => state.userStore.userInfos); const userInfosWithMissingUsernames = React.useMemo(() => { const entriesWithoutUsernames = Object.entries(userInfos).filter( ([, value]) => !value.username, ); return Object.fromEntries(entriesWithoutUsernames); }, [userInfos]); const dispatchActionPromise = useDispatchActionPromise(); const findUserIdentities = useFindUserIdentities(); const requestedIDsRef = React.useRef(new Set()); const callUpdateRelationships = useLegacyAshoatKeyserverCall(updateRelationships); const currentUserInfo = useSelector(state => state.currentUserInfo); const loggedInToAuthKeyserver = useIsLoggedInToAuthoritativeKeyserver(); React.useEffect(() => { if (!loggedInToAuthKeyserver) { return; } const newUserIDs = Object.keys(userInfosWithMissingUsernames).filter( id => !requestedIDsRef.current.has(id), ); if (!usingCommServicesAccessToken || newUserIDs.length === 0) { return; } void (async () => { const authMetadata = await getAuthMetadata(); if (!authMetadata) { return; } // 1. Fetch usernames from identity const promise = (async () => { newUserIDs.forEach(id => requestedIDsRef.current.add(id)); const identities = await findUserIdentities(newUserIDs); newUserIDs.forEach(id => requestedIDsRef.current.delete(id)); const newUserInfos = []; for (const id in identities) { newUserInfos.push({ id, username: identities[id].username, }); } return { userInfos: newUserInfos }; })(); void dispatchActionPromise(findUserIdentitiesActionTypes, promise); // 2. Fetch avatars from auth keyserver if (relyingOnAuthoritativeKeyserver) { const userIDsWithoutOwnID = newUserIDs.filter( id => id !== currentUserInfo?.id, ); if (userIDsWithoutOwnID.length === 0) { return; } void dispatchActionPromise( updateRelationshipsActionTypes, callUpdateRelationships({ action: relationshipActions.ACKNOWLEDGE, userIDs: userIDsWithoutOwnID, }), ); } })(); }, [ getAuthMetadata, callUpdateRelationships, currentUserInfo?.id, dispatchActionPromise, findUserIdentities, userInfos, userInfosWithMissingUsernames, loggedInToAuthKeyserver, ]); - const usersWithMissingDeviceList = useSelector( + const usersWithMissingDeviceListSelected = useSelector( usersWithMissingDeviceListSelector, ); const getAndUpdateDeviceListsForUsers = useGetAndUpdateDeviceListsForUsers(); const { socketState } = useTunnelbroker(); + + const requestedDeviceListsIDsRef = React.useRef(new Set()); + React.useEffect(() => { + const usersWithMissingDeviceList = + usersWithMissingDeviceListSelected.filter( + id => !requestedDeviceListsIDsRef.current.has(id), + ); + if ( !usingCommServicesAccessToken || usersWithMissingDeviceList.length === 0 || !socketState.isAuthorized ) { return; } void (async () => { const authMetadata = await getAuthMetadata(); if (!authMetadata) { return; } try { - await getAndUpdateDeviceListsForUsers(usersWithMissingDeviceList, true); + usersWithMissingDeviceList.forEach(id => + requestedDeviceListsIDsRef.current.add(id), + ); + const foundDeviceListIDs = await getAndUpdateDeviceListsForUsers( + usersWithMissingDeviceList, + true, + ); + Object.keys(foundDeviceListIDs).forEach(id => + requestedDeviceListsIDsRef.current.delete(id), + ); } catch (e) { console.log( `Error getting and setting peer device list: ${ getMessageForException(e) ?? 'unknown' }`, ); } })(); }, [ + getAndUpdateDeviceListsForUsers, getAuthMetadata, socketState.isAuthorized, - getAndUpdateDeviceListsForUsers, - usersWithMissingDeviceList, + usersWithMissingDeviceListSelected, ]); } export { UserInfosHandler }; diff --git a/lib/hooks/peer-list-hooks.js b/lib/hooks/peer-list-hooks.js index 632e290c2..9cb0d9634 100644 --- a/lib/hooks/peer-list-hooks.js +++ b/lib/hooks/peer-list-hooks.js @@ -1,138 +1,140 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { setPeerDeviceListsActionType } from '../actions/aux-user-actions.js'; import { getAllPeerDevices } from '../selectors/user-selectors.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import type { UsersRawDeviceLists, UsersDevicesPlatformDetails, SignedDeviceList, RawDeviceList, } from '../types/identity-service-types.js'; import { type DeviceListUpdated, peerToPeerMessageTypes, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; import { getContentSigningKey } from '../utils/crypto-utils.js'; import { convertSignedDeviceListsToRawDeviceLists } from '../utils/device-list-utils.js'; import { values } from '../utils/objects.js'; import { useDispatch, useSelector } from '../utils/redux-utils.js'; function useGetDeviceListsForUsers(): ( userIDs: $ReadOnlyArray, ) => Promise<{ +deviceLists: UsersRawDeviceLists, +usersPlatformDetails: UsersDevicesPlatformDetails, }> { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); return React.useCallback( async (userIDs: $ReadOnlyArray) => { const peersDeviceLists = await identityClient.getDeviceListsForUsers(userIDs); return { deviceLists: convertSignedDeviceListsToRawDeviceLists( peersDeviceLists.usersSignedDeviceLists, ), usersPlatformDetails: peersDeviceLists.usersDevicesPlatformDetails, }; }, [identityClient], ); } function useGetAndUpdateDeviceListsForUsers(): ( userIDs: $ReadOnlyArray, broadcastUpdates: ?boolean, -) => Promise { +) => Promise { const getDeviceListsForUsers = useGetDeviceListsForUsers(); const dispatch = useDispatch(); const broadcastDeviceListUpdates = useBroadcastDeviceListUpdates(); const allPeerDevices = useSelector(getAllPeerDevices); return React.useCallback( async (userIDs: $ReadOnlyArray, broadcastUpdates: ?boolean) => { const { deviceLists, usersPlatformDetails } = await getDeviceListsForUsers(userIDs); if (Object.keys(deviceLists).length === 0) { - return; + return {}; } dispatch({ type: setPeerDeviceListsActionType, payload: { deviceLists, usersPlatformDetails }, }); if (!broadcastUpdates) { - return; + return deviceLists; } const thisDeviceID = await getContentSigningKey(); const newDevices = values(deviceLists) .map((deviceList: RawDeviceList) => deviceList.devices) .flat() .filter( deviceID => !allPeerDevices.includes(deviceID) && deviceID !== thisDeviceID, ); await broadcastDeviceListUpdates(newDevices); + + return deviceLists; }, [ allPeerDevices, broadcastDeviceListUpdates, dispatch, getDeviceListsForUsers, ], ); } function useBroadcastDeviceListUpdates(): ( deviceIDs: $ReadOnlyArray, signedDeviceList?: SignedDeviceList, ) => Promise { const { sendMessage } = useTunnelbroker(); const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'identity context not set'); return React.useCallback( async ( deviceIDs: $ReadOnlyArray, signedDeviceList?: SignedDeviceList, ) => { const { getAuthMetadata } = identityContext; const { userID } = await getAuthMetadata(); if (!userID) { throw new Error('missing auth metadata'); } const messageToPeer: DeviceListUpdated = { type: peerToPeerMessageTypes.DEVICE_LIST_UPDATED, userID, signedDeviceList, }; const payload = JSON.stringify(messageToPeer); const promises = deviceIDs.map((deviceID: string) => sendMessage({ deviceID, payload, }), ); await Promise.all(promises); }, [identityContext, sendMessage], ); } export { useGetDeviceListsForUsers, useBroadcastDeviceListUpdates, useGetAndUpdateDeviceListsForUsers, };