diff --git a/lib/handlers/user-infos-handler.react.js b/lib/handlers/user-infos-handler.react.js index ff83b2405..c42f9d009 100644 --- a/lib/handlers/user-infos-handler.react.js +++ b/lib/handlers/user-infos-handler.react.js @@ -1,68 +1,105 @@ // @flow import * as React from 'react'; +import { setPeerDeviceListsActionType } from '../actions/aux-user-actions.js'; import { useFindUserIdentities, findUserIdentitiesActionTypes, } from '../actions/user-actions.js'; +import { useGetDeviceListsForUsers } from '../hooks/peer-list-hooks.js'; +import { usersWithMissingDeviceListSelector } from '../selectors/user-selectors.js'; +import { getMessageForException } from '../utils/errors.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; -import { useSelector } from '../utils/redux-utils.js'; +import { useDispatch, useSelector } from '../utils/redux-utils.js'; import { relyingOnAuthoritativeKeyserver, usingCommServicesAccessToken, } from '../utils/services-utils.js'; function UserInfosHandler(): React.Node { 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()); React.useEffect(() => { const newUserIDs = Object.keys(userInfosWithMissingUsernames).filter( id => !requestedIDsRef.current.has(id), ); if (!usingCommServicesAccessToken || newUserIDs.length === 0) { return; } const promise = (async () => { newUserIDs.forEach(id => requestedIDsRef.current.add(id)); // 1. Fetch usernames from identity const identities = await findUserIdentities(newUserIDs); // 2. Fetch avatars and settings from auth keyserver if (relyingOnAuthoritativeKeyserver) { // TODO } 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); }, [ dispatchActionPromise, findUserIdentities, userInfos, userInfosWithMissingUsernames, ]); + + const usersWithMissingDeviceList = useSelector( + usersWithMissingDeviceListSelector, + ); + const getDeviceListsForUsers = useGetDeviceListsForUsers(); + const dispatch = useDispatch(); + React.useEffect(() => { + if ( + !usingCommServicesAccessToken || + usersWithMissingDeviceList.length === 0 + ) { + return; + } + void (async () => { + try { + const { deviceLists, usersPlatformDetails } = + await getDeviceListsForUsers(usersWithMissingDeviceList); + if (Object.keys(deviceLists).length === 0) { + return; + } + dispatch({ + type: setPeerDeviceListsActionType, + payload: { deviceLists, usersPlatformDetails }, + }); + } catch (e) { + console.log( + `Error setting peer device list: ${ + getMessageForException(e) ?? 'unknown' + }`, + ); + } + })(); + }, [dispatch, getDeviceListsForUsers, usersWithMissingDeviceList]); } export { UserInfosHandler }; diff --git a/lib/hooks/peer-list-hooks.js b/lib/hooks/peer-list-hooks.js index e8f968c90..f94fb52c7 100644 --- a/lib/hooks/peer-list-hooks.js +++ b/lib/hooks/peer-list-hooks.js @@ -1,43 +1,71 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { setPeerDeviceListsActionType } from '../actions/aux-user-actions.js'; import { getRelativeUserIDs } from '../selectors/user-selectors.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; +import type { + UsersRawDeviceLists, + UsersDevicesPlatformDetails, +} from '../types/identity-service-types.js'; import { convertSignedDeviceListsToRawDeviceLists } from '../utils/device-list-utils.js'; import { useDispatch, useSelector } from '../utils/redux-utils.js'; function useCreateInitialPeerList(): () => Promise { const dispatch = useDispatch(); const relativeUserIDs = useSelector(getRelativeUserIDs); const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'Identity context should be set'); return React.useCallback(async () => { if (!identityContext) { return; } try { const peersDeviceLists = await identityContext.identityClient.getDeviceListsForUsers( relativeUserIDs, ); const usersRawDeviceLists = convertSignedDeviceListsToRawDeviceLists( peersDeviceLists.usersSignedDeviceLists, ); const usersPlatformDetails = peersDeviceLists.usersDevicesPlatformDetails; dispatch({ type: setPeerDeviceListsActionType, payload: { deviceLists: usersRawDeviceLists, usersPlatformDetails }, }); } catch (e) { console.log(`Error creating initial peer list: ${e.message}`); } }, [dispatch, identityContext, relativeUserIDs]); } -export { useCreateInitialPeerList }; +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], + ); +} + +export { useCreateInitialPeerList, useGetDeviceListsForUsers }; diff --git a/lib/selectors/user-selectors.js b/lib/selectors/user-selectors.js index 1f98bb0d4..90d9dd4f9 100644 --- a/lib/selectors/user-selectors.js +++ b/lib/selectors/user-selectors.js @@ -1,210 +1,232 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; +import bots from '../facts/bots.js'; import { getAvatarForUser, getRandomDefaultEmojiAvatar, } from '../shared/avatar-utils.js'; import { getSingleOtherUser } from '../shared/thread-utils.js'; +import { type AuxUserInfos } from '../types/aux-user-types.js'; import type { ClientEmojiAvatar } from '../types/avatar-types'; import type { RelativeMemberInfo, RawThreadInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import type { BaseAppState } from '../types/redux-types.js'; import { userRelationshipStatus } from '../types/relationship-types.js'; import { threadTypes } from '../types/thread-types-enum.js'; import type { RawThreadInfos } from '../types/thread-types.js'; import type { UserInfos, RelativeUserInfo, AccountUserInfo, CurrentUserInfo, } from '../types/user-types.js'; // Used for specific message payloads that include an array of user IDs, ie. // array of initial users, array of added users function userIDsToRelativeUserInfos( userIDs: $ReadOnlyArray, viewerID: ?string, userInfos: UserInfos, ): RelativeUserInfo[] { const relativeUserInfos: RelativeUserInfo[] = []; for (const userID of userIDs) { const username = userInfos[userID] ? userInfos[userID].username : null; const relativeUserInfo = { id: userID, username, isViewer: userID === viewerID, }; if (userID === viewerID) { relativeUserInfos.unshift(relativeUserInfo); } else { relativeUserInfos.push(relativeUserInfo); } } return relativeUserInfos; } function getRelativeMemberInfos( threadInfo: ?RawThreadInfo, currentUserID: ?string, userInfos: UserInfos, ): $ReadOnlyArray { const relativeMemberInfos: RelativeMemberInfo[] = []; if (!threadInfo) { return relativeMemberInfos; } const memberInfos = threadInfo.members; for (const memberInfo of memberInfos) { if (!memberInfo.role) { continue; } const username = userInfos[memberInfo.id] ? userInfos[memberInfo.id].username : null; if (memberInfo.id === currentUserID) { relativeMemberInfos.unshift({ ...memberInfo, username, isViewer: true, }); } else { relativeMemberInfos.push({ ...memberInfo, username, isViewer: false, }); } } return relativeMemberInfos; } const emptyArray: $ReadOnlyArray = []; // Includes current user at the start const baseRelativeMemberInfoSelectorForMembersOfThread: ( threadID: ?string, ) => (state: BaseAppState<>) => $ReadOnlyArray = ( threadID: ?string, ) => { if (!threadID) { return () => emptyArray; } return createSelector( (state: BaseAppState<>) => state.threadStore.threadInfos[threadID], (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo.id, (state: BaseAppState<>) => state.userStore.userInfos, getRelativeMemberInfos, ); }; const relativeMemberInfoSelectorForMembersOfThread: ( threadID: ?string, ) => (state: BaseAppState<>) => $ReadOnlyArray = _memoize( baseRelativeMemberInfoSelectorForMembersOfThread, ); const userInfoSelectorForPotentialMembers: (state: BaseAppState<>) => { [id: string]: AccountUserInfo, } = createSelector( (state: BaseAppState<>) => state.userStore.userInfos, (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo.id, ( userInfos: UserInfos, currentUserID: ?string, ): { [id: string]: AccountUserInfo } => { const availableUsers: { [id: string]: AccountUserInfo } = {}; for (const id in userInfos) { const { username, relationshipStatus } = userInfos[id]; if (id === currentUserID || !username) { continue; } if ( relationshipStatus !== userRelationshipStatus.BLOCKED_VIEWER && relationshipStatus !== userRelationshipStatus.BOTH_BLOCKED ) { availableUsers[id] = { id, username, relationshipStatus }; } } return availableUsers; }, ); const isLoggedIn = (state: BaseAppState<>): boolean => !!( state.currentUserInfo && !state.currentUserInfo.anonymous && state.dataLoaded ); const isLoggedInToKeyserver: ( keyserverID: ?string, ) => (state: BaseAppState<>) => boolean = _memoize( (keyserverID: ?string) => (state: BaseAppState<>) => { if (!keyserverID) { return false; } const cookie = state.keyserverStore.keyserverInfos[keyserverID]?.cookie; return !!cookie && cookie.startsWith('user='); }, ); const usersWithPersonalThreadSelector: ( state: BaseAppState<>, ) => $ReadOnlySet = createSelector( (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo.id, (state: BaseAppState<>) => state.threadStore.threadInfos, (viewerID: ?string, threadInfos: RawThreadInfos) => { const personalThreadMembers = new Set(); for (const threadID in threadInfos) { const thread = threadInfos[threadID]; if ( thread.type !== threadTypes.PERSONAL || !thread.members.find(member => member.id === viewerID) ) { continue; } const otherMemberID = getSingleOtherUser(thread, viewerID); if (otherMemberID) { personalThreadMembers.add(otherMemberID); } } return personalThreadMembers; }, ); const savedEmojiAvatarSelectorForCurrentUser: ( state: BaseAppState<>, ) => () => ClientEmojiAvatar = createSelector( (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo, (currentUser: ?CurrentUserInfo) => { return () => { let userAvatar = getAvatarForUser(currentUser); if (userAvatar.type !== 'emoji') { userAvatar = getRandomDefaultEmojiAvatar(); } return userAvatar; }; }, ); -const getRelativeUserIDs = (state: BaseAppState<>): $ReadOnlyArray => - Object.keys(state.userStore.userInfos); +const getRelativeUserIDs: (state: BaseAppState<>) => $ReadOnlyArray = + createSelector( + (state: BaseAppState<>) => state.userStore.userInfos, + (userInfos: UserInfos): $ReadOnlyArray => Object.keys(userInfos), + ); + +const usersWithMissingDeviceListSelector: ( + state: BaseAppState<>, +) => $ReadOnlyArray = createSelector( + getRelativeUserIDs, + (state: BaseAppState<>) => state.auxUserStore.auxUserInfos, + ( + userIDs: $ReadOnlyArray, + auxUserInfos: AuxUserInfos, + ): $ReadOnlyArray => + userIDs.filter( + userID => + (!auxUserInfos[userID] || !auxUserInfos[userID].deviceList) && + userID !== bots.commbot.userID, + ), +); export { userIDsToRelativeUserInfos, getRelativeMemberInfos, relativeMemberInfoSelectorForMembersOfThread, userInfoSelectorForPotentialMembers, isLoggedIn, isLoggedInToKeyserver, usersWithPersonalThreadSelector, savedEmojiAvatarSelectorForCurrentUser, getRelativeUserIDs, + usersWithMissingDeviceListSelector, };