diff --git a/lib/shared/search-utils.js b/lib/shared/search-utils.js index 3f76312b1..2a644ab04 100644 --- a/lib/shared/search-utils.js +++ b/lib/shared/search-utils.js @@ -1,294 +1,302 @@ // @flow import * as React from 'react'; import { useSelector } from 'react-redux'; import SearchIndex from './search-index.js'; import { userIsMember, threadMemberHasPermission, getContainingThreadID, } from './thread-utils.js'; import { searchMessages, searchMessagesActionTypes, } from '../actions/message-actions.js'; import { searchUsers, searchUsersActionTypes, } from '../actions/user-actions.js'; import genesis from '../facts/genesis.js'; import type { RawMessageInfo } from '../types/message-types.js'; import { userRelationshipStatus } from '../types/relationship-types.js'; import { threadPermissions } from '../types/thread-permission-types.js'; import { type ThreadType, threadTypes } from '../types/thread-types-enum.js'; import { type ThreadInfo } from '../types/thread-types.js'; import type { AccountUserInfo, UserListItem, GlobalAccountUserInfo, } from '../types/user-types.js'; import { useServerCall, useDispatchActionPromise, } from '../utils/action-utils.js'; import { values } from '../utils/objects.js'; const notFriendNotice = 'not friend'; function getPotentialMemberItems({ text, userInfos, searchIndex, excludeUserIDs, includeServerSearchUsers, inputParentThreadInfo, inputCommunityThreadInfo, threadType, }: { +text: string, +userInfos: { +[id: string]: AccountUserInfo }, +searchIndex: SearchIndex, +excludeUserIDs: $ReadOnlyArray, +includeServerSearchUsers?: $ReadOnlyArray, +inputParentThreadInfo?: ?ThreadInfo, +inputCommunityThreadInfo?: ?ThreadInfo, +threadType?: ?ThreadType, }): UserListItem[] { const communityThreadInfo = inputCommunityThreadInfo && inputCommunityThreadInfo.id !== genesis.id ? inputCommunityThreadInfo : null; const parentThreadInfo = inputParentThreadInfo && inputParentThreadInfo.id !== genesis.id ? inputParentThreadInfo : null; const containgThreadID = threadType ? getContainingThreadID(parentThreadInfo, threadType) : null; let containingThreadInfo = null; if (containgThreadID === parentThreadInfo?.id) { containingThreadInfo = parentThreadInfo; } else if (containgThreadID === communityThreadInfo?.id) { containingThreadInfo = communityThreadInfo; } const results: { [id: string]: { ...AccountUserInfo | GlobalAccountUserInfo, isMemberOfParentThread: boolean, isMemberOfContainingThread: boolean, }, } = {}; const appendUserInfo = ( userInfo: AccountUserInfo | GlobalAccountUserInfo, ) => { const { id } = userInfo; if (excludeUserIDs.includes(id) || id in results) { return; } if ( communityThreadInfo && !threadMemberHasPermission( communityThreadInfo, id, threadPermissions.KNOW_OF, ) ) { return; } results[id] = { ...userInfo, isMemberOfParentThread: userIsMember(parentThreadInfo, id), isMemberOfContainingThread: userIsMember(containingThreadInfo, id), }; }; if (text === '') { for (const id in userInfos) { appendUserInfo(userInfos[id]); } } else { const ids = searchIndex.getSearchResults(text); for (const id of ids) { appendUserInfo(userInfos[id]); } } if (includeServerSearchUsers) { for (const userInfo of includeServerSearchUsers) { appendUserInfo(userInfo); } } const blockedRelationshipsStatuses = new Set([ userRelationshipStatus.BLOCKED_BY_VIEWER, userRelationshipStatus.BLOCKED_VIEWER, userRelationshipStatus.BOTH_BLOCKED, ]); let userResults = values(results); if (text === '') { userResults = userResults.filter(userInfo => containingThreadInfo ? userInfo.isMemberOfContainingThread && !blockedRelationshipsStatuses.has(userInfo.relationshipStatus) : userInfo?.relationshipStatus === userRelationshipStatus.FRIEND, ); } const nonFriends = []; const blockedUsers = []; const friends = []; const containingThreadMembers = []; const parentThreadMembers = []; for (const userResult of userResults) { const relationshipStatus = userResult.relationshipStatus; if (blockedRelationshipsStatuses.has(relationshipStatus)) { blockedUsers.push(userResult); } else if (userResult.isMemberOfParentThread) { parentThreadMembers.push(userResult); } else if (userResult.isMemberOfContainingThread) { containingThreadMembers.push(userResult); } else if (relationshipStatus === userRelationshipStatus.FRIEND) { friends.push(userResult); } else { nonFriends.push(userResult); } } const sortedResults = parentThreadMembers .concat(containingThreadMembers) .concat(friends) .concat(nonFriends) .concat(blockedUsers); return sortedResults.map( ({ isMemberOfContainingThread, isMemberOfParentThread, relationshipStatus, ...result }) => { - let notice, alertText, alertTitle; + let notice, alert; const username = result.username; if (blockedRelationshipsStatuses.has(relationshipStatus)) { notice = 'user is blocked'; - alertTitle = 'User is blocked'; - alertText = - `Before you add ${username} to this chat, ` + - 'you’ll need to unblock them. You can do this from the Block List ' + - 'in the Profile tab.'; + alert = { + title: 'User is blocked', + text: + `Before you add ${username} to this chat, ` + + 'you’ll need to unblock them. You can do this from the Block List ' + + 'in the Profile tab.', + }; } else if (!isMemberOfContainingThread && containingThreadInfo) { if (threadType !== threadTypes.SIDEBAR) { notice = 'not in community'; - alertTitle = 'Not in community'; - alertText = 'You can only add members of the community to this chat'; + alert = { + title: 'Not in community', + text: 'You can only add members of the community to this chat', + }; } else { notice = 'not in parent chat'; - alertTitle = 'Not in parent chat'; - alertText = 'You can only add members of the parent chat to a thread'; + alert = { + title: 'Not in parent chat', + text: 'You can only add members of the parent chat to a thread', + }; } } else if ( !containingThreadInfo && relationshipStatus !== userRelationshipStatus.FRIEND ) { notice = notFriendNotice; - alertTitle = 'Not a friend'; - alertText = - `Before you add ${username} to this chat, ` + - 'you’ll need to send them a friend request. ' + - 'You can do this from the Friend List in the Profile tab.'; + alert = { + title: 'Not a friend', + text: + `Before you add ${username} to this chat, ` + + 'you’ll need to send them a friend request. ' + + 'You can do this from the Friend List in the Profile tab.', + }; } else if (parentThreadInfo && !isMemberOfParentThread) { notice = 'not in parent chat'; } if (notice) { result = { ...result, notice }; } - if (alertTitle) { - result = { ...result, alertTitle, alertText }; + if (alert) { + result = { ...result, alert }; } return result; }, ); } function useSearchMessages(): ( query: string, threadID: string, onResultsReceived: ( messages: $ReadOnlyArray, endReached: boolean, ) => mixed, cursor?: string, ) => void { const callSearchMessages = useServerCall(searchMessages); const dispatchActionPromise = useDispatchActionPromise(); return React.useCallback( (query, threadID, onResultsReceived, cursor) => { const searchMessagesPromise = (async () => { if (query === '') { onResultsReceived([], true); return; } const { messages, endReached } = await callSearchMessages({ query, threadID, cursor, }); onResultsReceived(messages, endReached); })(); dispatchActionPromise(searchMessagesActionTypes, searchMessagesPromise); }, [callSearchMessages, dispatchActionPromise], ); } function useSearchUsers( usernameInputText: string, ): $ReadOnlyArray { const currentUserID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const [serverSearchResults, setServerSearchResults] = React.useState< $ReadOnlyArray, >([]); const callSearchUsers = useServerCall(searchUsers); const dispatchActionPromise = useDispatchActionPromise(); React.useEffect(() => { const searchUsersPromise = (async () => { if (usernameInputText.length === 0) { setServerSearchResults([]); } else { try { const { userInfos } = await callSearchUsers(usernameInputText); setServerSearchResults( userInfos.filter(({ id }) => id !== currentUserID), ); } catch (err) { setServerSearchResults([]); } } })(); dispatchActionPromise(searchUsersActionTypes, searchUsersPromise); }, [ callSearchUsers, currentUserID, dispatchActionPromise, usernameInputText, ]); return serverSearchResults; } export { getPotentialMemberItems, notFriendNotice, useSearchMessages, useSearchUsers, }; diff --git a/lib/types/user-types.js b/lib/types/user-types.js index 1865c56fc..8aa6e532a 100644 --- a/lib/types/user-types.js +++ b/lib/types/user-types.js @@ -1,139 +1,142 @@ // @flow import t, { type TInterface, type TDict, type TUnion } from 'tcomb'; import { type DefaultNotificationPayload, defaultNotificationPayloadValidator, } from './account-types.js'; import { type ClientAvatar, clientAvatarValidator } from './avatar-types.js'; import { type UserRelationshipStatus, userRelationshipStatusValidator, } from './relationship-types.js'; import type { UserInconsistencyReportCreationRequest } from './report-types.js'; import { tBool, tShape } from '../utils/validation-utils.js'; export type GlobalUserInfo = { +id: string, +username: ?string, +avatar?: ?ClientAvatar, }; export type GlobalAccountUserInfo = { +id: string, +username: string, +avatar?: ?ClientAvatar, }; export const globalAccountUserInfoValidator: TInterface = tShape({ id: t.String, username: t.String, avatar: t.maybe(clientAvatarValidator), }); export type UserInfo = { +id: string, +username: ?string, +relationshipStatus?: UserRelationshipStatus, +avatar?: ?ClientAvatar, }; export const userInfoValidator: TInterface = tShape({ id: t.String, username: t.maybe(t.String), relationshipStatus: t.maybe(userRelationshipStatusValidator), avatar: t.maybe(clientAvatarValidator), }); export type UserInfos = { +[id: string]: UserInfo }; export const userInfosValidator: TDict = t.dict( t.String, userInfoValidator, ); export type AccountUserInfo = { +id: string, +username: string, +relationshipStatus?: UserRelationshipStatus, +avatar?: ?ClientAvatar, }; export const accountUserInfoValidator: TInterface = tShape({ id: t.String, username: t.String, relationshipStatus: t.maybe(userRelationshipStatusValidator), avatar: t.maybe(clientAvatarValidator), }); export type UserStore = { +userInfos: UserInfos, +inconsistencyReports: $ReadOnlyArray, }; export type RelativeUserInfo = { +id: string, +username: ?string, +isViewer: boolean, +avatar?: ?ClientAvatar, }; export type OldLoggedInUserInfo = { +id: string, +username: string, +email: string, +emailVerified: boolean, }; export const oldLoggedInUserInfoValidator: TInterface = tShape({ id: t.String, username: t.String, email: t.String, emailVerified: t.Boolean, }); export type LoggedInUserInfo = { +id: string, +username: string, +settings?: DefaultNotificationPayload, +avatar?: ?ClientAvatar, }; export const loggedInUserInfoValidator: TInterface = tShape({ id: t.String, username: t.String, settings: t.maybe(defaultNotificationPayloadValidator), avatar: t.maybe(clientAvatarValidator), }); export type LoggedOutUserInfo = { +id: string, +anonymous: true, }; export const loggedOutUserInfoValidator: TInterface = tShape({ id: t.String, anonymous: tBool(true) }); export type OldCurrentUserInfo = OldLoggedInUserInfo | LoggedOutUserInfo; export const oldCurrentUserInfoValidator: TUnion = t.union([ oldLoggedInUserInfoValidator, loggedOutUserInfoValidator, ]); export type CurrentUserInfo = LoggedInUserInfo | LoggedOutUserInfo; export const currentUserInfoValidator: TUnion = t.union([ loggedInUserInfoValidator, loggedOutUserInfoValidator, ]); export type PasswordUpdate = { +updatedFields: { +password?: ?string, }, +currentPassword: string, }; export type UserListItem = { ...AccountUserInfo, +disabled?: boolean, +notice?: string, - +alertText?: string, - +alertTitle?: string, + +alert?: { + +text: string, + +title: string, + }, + +avatar?: ?ClientAvatar, }; diff --git a/native/chat/message-list-thread-search.react.js b/native/chat/message-list-thread-search.react.js index 9ee26e03d..81ac08a46 100644 --- a/native/chat/message-list-thread-search.react.js +++ b/native/chat/message-list-thread-search.react.js @@ -1,172 +1,172 @@ // @flow import * as React from 'react'; import { Text, View } from 'react-native'; import { useENSNames } from 'lib/hooks/ens-cache.js'; import { notFriendNotice } from 'lib/shared/search-utils.js'; import type { AccountUserInfo, UserListItem } from 'lib/types/user-types.js'; import { createTagInput } from '../components/tag-input.react.js'; import UserList from '../components/user-list.react.js'; import { useStyles } from '../themes/colors.js'; const TagInput = createTagInput(); type Props = { +usernameInputText: string, +updateUsernameInput: (text: string) => void, +userInfoInputArray: $ReadOnlyArray, +updateTagInput: (items: $ReadOnlyArray) => void, +resolveToUser: (user: AccountUserInfo) => void, +userSearchResults: $ReadOnlyArray, }; const inputProps = { placeholder: 'username', autoFocus: true, returnKeyType: 'go', }; const MessageListThreadSearch: React.ComponentType = React.memo( function MessageListThreadSearch({ usernameInputText, updateUsernameInput, userInfoInputArray, updateTagInput, resolveToUser, userSearchResults, }) { const styles = useStyles(unboundStyles); const [userListItems, nonFriends] = React.useMemo(() => { const nonFriendsSet = new Set(); if (userInfoInputArray.length > 0) { return [userSearchResults, nonFriendsSet]; } const userListItemsArr = []; for (const searchResult of userSearchResults) { if (searchResult.notice !== notFriendNotice) { userListItemsArr.push(searchResult); continue; } nonFriendsSet.add(searchResult.id); - const { alertText, alertTitle, ...rest } = searchResult; + const { alert, ...rest } = searchResult; userListItemsArr.push(rest); } return [userListItemsArr, nonFriendsSet]; }, [userSearchResults, userInfoInputArray]); const onUserSelect = React.useCallback( (userInfo: AccountUserInfo) => { for (const existingUserInfo of userInfoInputArray) { if (userInfo.id === existingUserInfo.id) { return; } } if (nonFriends.has(userInfo.id)) { resolveToUser(userInfo); return; } const newUserInfoInputArray = [...userInfoInputArray, userInfo]; updateUsernameInput(''); updateTagInput(newUserInfoInputArray); }, [ userInfoInputArray, nonFriends, updateTagInput, resolveToUser, updateUsernameInput, ], ); const tagDataLabelExtractor = React.useCallback( (userInfo: AccountUserInfo) => userInfo.username, [], ); const isSearchResultVisible = (userInfoInputArray.length === 0 || usernameInputText.length > 0) && userSearchResults.length > 0; let separator = null; let userList = null; let userSelectionAdditionalStyles = styles.userSelectionLimitedHeight; const userListItemsWithENSNames = useENSNames(userListItems); if (isSearchResultVisible) { userList = ( ); separator = ; userSelectionAdditionalStyles = null; } const userInfoInputArrayWithENSNames = useENSNames(userInfoInputArray); return ( <> To: {userList} {separator} ); }, ); const unboundStyles = { userSelection: { backgroundColor: 'panelBackground', flex: 1, }, userSelectionLimitedHeight: { flex: 0, }, tagInputLabel: { color: 'modalForegroundSecondaryLabel', fontSize: 16, paddingLeft: 12, }, tagInputContainer: { alignItems: 'center', backgroundColor: 'modalForeground', borderBottomWidth: 1, borderColor: 'modalForegroundBorder', flexDirection: 'row', paddingVertical: 6, }, tagInput: { flex: 1, }, userList: { backgroundColor: 'modalBackground', paddingLeft: 35, paddingRight: 12, flex: 1, }, separator: { height: 1, backgroundColor: 'modalForegroundBorder', }, }; export default MessageListThreadSearch; diff --git a/native/components/user-list-user.react.js b/native/components/user-list-user.react.js index 02155245a..428d21f0f 100644 --- a/native/components/user-list-user.react.js +++ b/native/components/user-list-user.react.js @@ -1,98 +1,97 @@ // @flow import * as React from 'react'; import { Text, Platform, Alert } from 'react-native'; import type { UserListItem, AccountUserInfo } from 'lib/types/user-types.js'; import Button from './button.react.js'; import { SingleLine } from './single-line.react.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import { type Colors, useColors, useStyles } from '../themes/colors.js'; import type { TextStyle } from '../types/styles.js'; // eslint-disable-next-line no-unused-vars const getUserListItemHeight = (item: UserListItem): number => { // TODO consider parent thread notice return Platform.OS === 'ios' ? 31.5 : 33.5; }; type BaseProps = { +userInfo: UserListItem, +onSelect: (user: AccountUserInfo) => void, +textStyle?: TextStyle, }; type Props = { ...BaseProps, // Redux state +colors: Colors, +styles: typeof unboundStyles, }; class UserListUser extends React.PureComponent { render() { const { userInfo } = this.props; let notice = null; if (userInfo.notice) { notice = {userInfo.notice}; } const { modalIosHighlightUnderlay: underlayColor } = this.props.colors; return ( ); } onSelect = () => { const { userInfo } = this.props; - if (!userInfo.alertText) { - const { alertText, alertTitle, notice, disabled, ...accountUserInfo } = - userInfo; + if (!userInfo.alert) { + const { alert, notice, disabled, ...accountUserInfo } = userInfo; this.props.onSelect(accountUserInfo); return; } - Alert.alert(userInfo.alertTitle, userInfo.alertText, [{ text: 'OK' }], { + Alert.alert(userInfo.alert.title, userInfo.alert.text, [{ text: 'OK' }], { cancelable: true, }); }; } const unboundStyles = { button: { alignItems: 'center', flexDirection: 'row', justifyContent: 'space-between', }, notice: { color: 'modalForegroundSecondaryLabel', fontStyle: 'italic', }, text: { color: 'modalForegroundLabel', flex: 1, fontSize: 16, paddingHorizontal: 12, paddingVertical: 6, }, }; const ConnectedUserListUser: React.ComponentType = React.memo(function ConnectedUserListUser(props: BaseProps) { const colors = useColors(); const styles = useStyles(unboundStyles); return ; }); export { ConnectedUserListUser as UserListUser, getUserListItemHeight }; diff --git a/web/chat/chat-thread-composer.react.js b/web/chat/chat-thread-composer.react.js index 6a7b15dcb..57e70bcca 100644 --- a/web/chat/chat-thread-composer.react.js +++ b/web/chat/chat-thread-composer.react.js @@ -1,211 +1,211 @@ // @flow import classNames from 'classnames'; import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { useENSNames } from 'lib/hooks/ens-cache.js'; import { userSearchIndexForPotentialMembers } from 'lib/selectors/user-selectors.js'; import { getPotentialMemberItems, useSearchUsers, } from 'lib/shared/search-utils.js'; import { threadIsPending } from 'lib/shared/thread-utils.js'; import type { AccountUserInfo, UserListItem } from 'lib/types/user-types.js'; import css from './chat-thread-composer.css'; import UserAvatar from '../avatars/user-avatar.react.js'; import Button from '../components/button.react.js'; import Label from '../components/label.react.js'; import Search from '../components/search.react.js'; import type { InputState } from '../input/input-state.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; type Props = { +userInfoInputArray: $ReadOnlyArray, +otherUserInfos: { [id: string]: AccountUserInfo }, +threadID: string, +inputState: InputState, }; type ActiveThreadBehavior = | 'reset-active-thread-if-pending' | 'keep-active-thread'; function ChatThreadComposer(props: Props): React.Node { const { userInfoInputArray, otherUserInfos, threadID, inputState } = props; const [usernameInputText, setUsernameInputText] = React.useState(''); const dispatch = useDispatch(); const userSearchIndex = useSelector(userSearchIndexForPotentialMembers); const userInfoInputIDs = React.useMemo( () => userInfoInputArray.map(userInfo => userInfo.id), [userInfoInputArray], ); const serverSearchResults = useSearchUsers(usernameInputText); const userListItems = React.useMemo( () => getPotentialMemberItems({ text: usernameInputText, userInfos: otherUserInfos, searchIndex: userSearchIndex, excludeUserIDs: userInfoInputIDs, includeServerSearchUsers: serverSearchResults, }), [ usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs, serverSearchResults, ], ); const userListItemsWithENSNames = useENSNames(userListItems); const onSelectUserFromSearch = React.useCallback( (user: AccountUserInfo) => { dispatch({ type: updateNavInfoActionType, payload: { selectedUserList: [...userInfoInputArray, user], }, }); setUsernameInputText(''); }, [dispatch, userInfoInputArray], ); const onRemoveUserFromSelected = React.useCallback( (userID: string) => { const newSelectedUserList = userInfoInputArray.filter( ({ id }) => userID !== id, ); if (_isEqual(userInfoInputArray)(newSelectedUserList)) { return; } dispatch({ type: updateNavInfoActionType, payload: { selectedUserList: newSelectedUserList, }, }); }, [dispatch, userInfoInputArray], ); const userSearchResultList = React.useMemo(() => { if ( !userListItemsWithENSNames.length || (!usernameInputText && userInfoInputArray.length) ) { return null; } const userItems = userListItemsWithENSNames.map( (userSearchResult: UserListItem) => { - const { alertTitle, alertText, notice, disabled, ...accountUserInfo } = + const { alert, notice, disabled, ...accountUserInfo } = userSearchResult; return (
  • ); }, ); return
      {userItems}
    ; }, [ onSelectUserFromSearch, userInfoInputArray.length, userListItemsWithENSNames, usernameInputText, ]); const hideSearch = React.useCallback( (threadBehavior: ActiveThreadBehavior = 'keep-active-thread') => { dispatch({ type: updateNavInfoActionType, payload: { chatMode: 'view', activeChatThreadID: threadBehavior === 'keep-active-thread' || !threadIsPending(threadID) ? threadID : null, }, }); }, [dispatch, threadID], ); const onCloseSearch = React.useCallback(() => { hideSearch('reset-active-thread-if-pending'); }, [hideSearch]); const userInfoInputArrayWithENSNames = useENSNames(userInfoInputArray); const tagsList = React.useMemo(() => { if (!userInfoInputArrayWithENSNames?.length) { return null; } const labels = userInfoInputArrayWithENSNames.map(user => { return ( ); }); return
    {labels}
    ; }, [userInfoInputArrayWithENSNames, onRemoveUserFromSelected]); React.useEffect(() => { if (!inputState) { return undefined; } inputState.registerSendCallback(hideSearch); return () => inputState.unregisterSendCallback(hideSearch); }, [hideSearch, inputState]); const threadSearchContainerStyles = classNames(css.threadSearchContainer, { [css.fullHeight]: !userInfoInputArray.length, }); return (
    {tagsList} {userSearchResultList}
    ); } export default ChatThreadComposer; diff --git a/web/modals/components/add-members-item.react.js b/web/modals/components/add-members-item.react.js index 699ce8afc..cc50de2e0 100644 --- a/web/modals/components/add-members-item.react.js +++ b/web/modals/components/add-members-item.react.js @@ -1,55 +1,55 @@ // @flow import * as React from 'react'; import type { UserListItem } from 'lib/types/user-types.js'; import css from './add-members.css'; import UserAvatar from '../../avatars/user-avatar.react.js'; import Button from '../../components/button.react.js'; type AddMembersItemProps = { +userInfo: UserListItem, +onClick: (userID: string) => void, +userAdded: boolean, }; function AddMemberItem(props: AddMembersItemProps): React.Node { const { userInfo, onClick, userAdded = false } = props; - const canBeAdded = !userInfo.alertText; + const canBeAdded = !userInfo.alert; const onClickCallback = React.useCallback(() => { if (!canBeAdded) { return; } onClick(userInfo.id); }, [canBeAdded, onClick, userInfo.id]); const action = React.useMemo(() => { if (!canBeAdded) { - return userInfo.alertTitle; + return userInfo.alert?.title; } if (userAdded) { return Remove; } else { return 'Add'; } - }, [canBeAdded, userAdded, userInfo.alertTitle]); + }, [canBeAdded, userAdded, userInfo.alert?.title]); return ( ); } export default AddMemberItem; diff --git a/web/modals/threads/members/add-members-list-content.react.js b/web/modals/threads/members/add-members-list-content.react.js index 060727268..ee6ef17fe 100644 --- a/web/modals/threads/members/add-members-list-content.react.js +++ b/web/modals/threads/members/add-members-list-content.react.js @@ -1,81 +1,81 @@ // @flow import _groupBy from 'lodash/fp/groupBy.js'; import _toPairs from 'lodash/fp/toPairs.js'; import * as React from 'react'; import type { UserListItem } from 'lib/types/user-types.js'; import AddMembersList from '../../components/add-members-list.react.js'; type Props = { +userListItems: $ReadOnlyArray, +pendingUsersToAdd: $ReadOnlySet, +switchUser: string => void, +hasParentThread: boolean, }; function AddMembersListContent(props: Props): React.Node { const { userListItems, pendingUsersToAdd, switchUser, hasParentThread } = props; const usersAvailableToAdd = React.useMemo( - () => userListItems.filter(user => !user.alertText), + () => userListItems.filter(user => !user.alert), [userListItems], ); const groupedAvailableUsersList = React.useMemo( () => _groupBy(userInfo => userInfo.notice)(usersAvailableToAdd), [usersAvailableToAdd], ); const membersInParentThread = React.useMemo(() => { if (!groupedAvailableUsersList['undefined']) { return null; } const label = hasParentThread ? 'Users in parent channel' : null; return [label, groupedAvailableUsersList['undefined']]; }, [groupedAvailableUsersList, hasParentThread]); const membersNotInParentThread = React.useMemo( () => _toPairs(groupedAvailableUsersList) .filter(group => group[0] !== 'undefined') .sort((a, b) => a[0].localeCompare(b[0])) .map(([header, users]) => [ header.charAt(0).toUpperCase() + header.substring(1), users, ]), [groupedAvailableUsersList], ); const usersUnavailableToAdd = React.useMemo(() => { - const usersUnavailable = userListItems.filter(user => user.alertText); + const usersUnavailable = userListItems.filter(user => user.alert); if (!usersUnavailable.length) { return null; } return ['Unavailable users', usersUnavailable]; }, [userListItems]); const sortedGroupedUsersList = React.useMemo( () => [ membersInParentThread, ...membersNotInParentThread, usersUnavailableToAdd, ] .filter(Boolean) .map(([header, userInfos]) => ({ header, userInfos })), [membersInParentThread, membersNotInParentThread, usersUnavailableToAdd], ); return ( ); } export default AddMembersListContent;