diff --git a/lib/selectors/user-selectors.js b/lib/selectors/user-selectors.js index 3ed806c61..b35908869 100644 --- a/lib/selectors/user-selectors.js +++ b/lib/selectors/user-selectors.js @@ -1,253 +1,275 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { getAvatarForUser, getRandomDefaultEmojiAvatar, } from '../shared/avatar-utils.js'; import SearchIndex from '../shared/search-index.js'; import SentencePrefixSearchIndex from '../shared/sentence-prefix-search-index.js'; import { getSingleOtherUser } from '../shared/thread-utils.js'; import type { ClientEmojiAvatar } from '../types/avatar-types'; +import type { MinimallyEncodedRawThreadInfo } 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 { LegacyRawThreadInfo, - LegacyRelativeMemberInfo, RelativeMemberInfo, 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; if (userID === viewerID) { relativeUserInfos.unshift({ id: userID, username, isViewer: true, }); } else { relativeUserInfos.push({ id: userID, username, isViewer: false, }); } } return relativeUserInfos; } -function getRelativeMemberInfos( - threadInfo: ?LegacyRawThreadInfo, +type ExtractArrayParam = (arr: $ReadOnlyArray) => T; + +function getRelativeMemberInfos< + TI: MinimallyEncodedRawThreadInfo | LegacyRawThreadInfo, +>( + threadInfo: ?TI, currentUserID: ?string, userInfos: UserInfos, -): $ReadOnlyArray { - const relativeMemberInfos = []; +): $ReadOnlyArray< + $ReadOnly<{ + ...$Call>, + +username: ?string, + +isViewer: boolean, + }>, +> { + const relativeMemberInfos: Array< + $ReadOnly<{ + ...$Call>, + +username: ?string, + +isViewer: boolean, + }>, + > = []; if (!threadInfo) { return relativeMemberInfos; } const memberInfos = threadInfo.members; - for (const memberInfo of memberInfos) { + for (const memberInfoInput of memberInfos) { + const memberInfo: $Call< + ExtractArrayParam, + $PropertyType, + > = memberInfoInput; if (!memberInfo.role) { continue; } - const username = userInfos[memberInfo.id] + const username: ?string = userInfos[memberInfo.id] ? userInfos[memberInfo.id].username : null; - if (memberInfo.id === currentUserID) { - relativeMemberInfos.unshift({ - ...memberInfo, - username, - isViewer: true, - }); + const isViewer: boolean = memberInfo.id === currentUserID; + const relativeMemberInfo: $ReadOnly<{ + ...$Call>, + +username: ?string, + +isViewer: boolean, + }> = { + ...memberInfo, + username, + isViewer, + }; + if (isViewer) { + relativeMemberInfos.unshift(relativeMemberInfo); } else { - relativeMemberInfos.push({ - ...memberInfo, - username, - isViewer: false, - }); + relativeMemberInfos.push(relativeMemberInfo); } } return relativeMemberInfos; } -const emptyArray: $ReadOnlyArray = []; +const emptyArray: $ReadOnlyArray = []; // Includes current user at the start const baseRelativeMemberInfoSelectorForMembersOfThread: ( threadID: ?string, -) => (state: BaseAppState<>) => $ReadOnlyArray = ( +) => (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; }, ); function searchIndexFromUserInfos(userInfos: { [id: string]: AccountUserInfo, }) { const searchIndex = new SearchIndex(); for (const id in userInfos) { searchIndex.addEntry(id, userInfos[id].username); } return searchIndex; } const userSearchIndexForPotentialMembers: ( state: BaseAppState<>, ) => SearchIndex = createSelector( userInfoSelectorForPotentialMembers, searchIndexFromUserInfos, ); const isLoggedIn = (state: BaseAppState<>): boolean => !!( state.currentUserInfo && !state.currentUserInfo.anonymous && state.dataLoaded ); const addUsersToSearchIndex = ( userInfos: UserInfos, searchIndex: SearchIndex | SentencePrefixSearchIndex, ): void => { for (const id in userInfos) { const { username } = userInfos[id]; if (!username) { continue; } searchIndex.addEntry(id, username); } }; const userStoreSearchIndex: (state: BaseAppState<>) => SearchIndex = createSelector( (state: BaseAppState<>) => state.userStore.userInfos, (userInfos: UserInfos) => { const searchIndex = new SearchIndex(); addUsersToSearchIndex(userInfos, searchIndex); return searchIndex; }, ); const userStoreMentionSearchIndex: ( state: BaseAppState<>, ) => SentencePrefixSearchIndex = createSelector( (state: BaseAppState<>) => state.userStore.userInfos, (userInfos: UserInfos) => { const searchIndex = new SentencePrefixSearchIndex(); addUsersToSearchIndex(userInfos, searchIndex); return searchIndex; }, ); 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; }; }, ); export { userIDsToRelativeUserInfos, getRelativeMemberInfos, relativeMemberInfoSelectorForMembersOfThread, userInfoSelectorForPotentialMembers, userSearchIndexForPotentialMembers, isLoggedIn, userStoreSearchIndex, userStoreMentionSearchIndex, usersWithPersonalThreadSelector, savedEmojiAvatarSelectorForCurrentUser, };