diff --git a/web/chat/member-list-sidebar/member-list-sidebar-header.react.js b/web/chat/member-list-sidebar/member-list-sidebar-header.react.js index bcf37b119..106087a94 100644 --- a/web/chat/member-list-sidebar/member-list-sidebar-header.react.js +++ b/web/chat/member-list-sidebar/member-list-sidebar-header.react.js @@ -1,52 +1,54 @@ // @flow import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import css from './member-list-sidebar-header.css'; import { useMemberListSidebarContext } from './member-list-sidebar-provider.react.js'; import AddButton from '../../components/add-button.react.js'; -import { AddMembersModal } from '../../modals/threads/members/add-members-modal.react.js'; +import { AddMembersModalWrapper } from '../../modals/threads/members/add-members-modal.react.js'; type Props = { +threadID: string, }; function MemberListSidebarHeader(props: Props): React.Node { const { threadID } = props; const { pushModal, popModal } = useModalContext(); const onClickAddButton = React.useCallback(() => { - pushModal(); + pushModal( + , + ); }, [popModal, pushModal, threadID]); const { setShowMemberListSidebar } = useMemberListSidebarContext(); const onClickCloseButton = React.useCallback(() => { setShowMemberListSidebar(false); }, [setShowMemberListSidebar]); const memberListSidebarHeader = React.useMemo( () => ( <>
Member list
), [onClickAddButton, onClickCloseButton], ); return memberListSidebarHeader; } export default MemberListSidebarHeader; 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 ee6ef17fe..1b42f4cc9 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,76 @@ // @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.alert), - [userListItems], - ); - const groupedAvailableUsersList = React.useMemo( - () => _groupBy(userInfo => userInfo.notice)(usersAvailableToAdd), - [usersAvailableToAdd], + () => _groupBy(userInfo => userInfo.notice)(userListItems), + [userListItems], ); 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.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; diff --git a/web/modals/threads/members/add-members-modal.react.js b/web/modals/threads/members/add-members-modal.react.js index 1f157fba6..f07ff304e 100644 --- a/web/modals/threads/members/add-members-modal.react.js +++ b/web/modals/threads/members/add-members-modal.react.js @@ -1,153 +1,141 @@ // @flow import * as React from 'react'; import { changeThreadSettingsActionTypes, useChangeThreadSettings, } from 'lib/actions/thread-actions.js'; -import { useENSNames } from 'lib/hooks/ens-cache.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; -import { userInfoSelectorForPotentialMembers } from 'lib/selectors/user-selectors.js'; -import { usePotentialMemberItems } from 'lib/shared/search-utils.js'; -import { threadActualMembers } from 'lib/shared/thread-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import AddMembersListContent from './add-members-list-content.react.js'; import css from './members-modal.css'; import Button from '../../../components/button.react.js'; import { useSelector } from '../../../redux/redux-utils.js'; +import { AddUsersListProvider } from '../../../settings/relationship/add-users-list-provider.react.js'; +import { useAddMembersListUserInfos } from '../../../settings/relationship/add-users-utils.js'; import SearchModal from '../../search-modal.react.js'; type ContentProps = { +searchText: string, +threadID: string, +onClose: () => void, }; function AddMembersModalContent(props: ContentProps): React.Node { const { searchText, threadID, onClose } = props; const [pendingUsersToAdd, setPendingUsersToAdd] = React.useState< $ReadOnlySet, >(new Set()); const threadInfo = useSelector(state => threadInfoSelector(state)[threadID]); - const { parentThreadID, community } = threadInfo; - const parentThreadInfo = useSelector(state => - parentThreadID ? threadInfoSelector(state)[parentThreadID] : null, - ); - const communityThreadInfo = useSelector(state => - community ? threadInfoSelector(state)[community] : null, - ); - const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); - const excludeUserIDs = React.useMemo( - () => - threadActualMembers(threadInfo.members).concat( - Array.from(pendingUsersToAdd), - ), - [pendingUsersToAdd, threadInfo.members], - ); - const userSearchResults = usePotentialMemberItems({ - text: searchText, - userInfos: otherUserInfos, - excludeUserIDs, - inputParentThreadInfo: parentThreadInfo, - inputCommunityThreadInfo: communityThreadInfo, - threadType: threadInfo.type, + const { sortedUsersWithENSNames } = useAddMembersListUserInfos({ + threadID, + searchText, }); - const userSearchResultsWithENSNames = useENSNames(userSearchResults); const onSwitchUser = React.useCallback( (userID: string) => setPendingUsersToAdd(users => { const newUsers = new Set(users); if (newUsers.has(userID)) { newUsers.delete(userID); } else { newUsers.add(userID); } return newUsers; }), [], ); const dispatchActionPromise = useDispatchActionPromise(); const callChangeThreadSettings = useChangeThreadSettings(); const addUsers = React.useCallback(() => { void dispatchActionPromise( changeThreadSettingsActionTypes, callChangeThreadSettings({ threadID, changes: { newMemberIDs: Array.from(pendingUsersToAdd) }, }), ); onClose(); }, [ callChangeThreadSettings, dispatchActionPromise, onClose, pendingUsersToAdd, threadID, ]); return (
); } type Props = { +threadID: string, +onClose: () => void, }; function AddMembersModal(props: Props): React.Node { const { threadID, onClose } = props; const addMembersModalContent = React.useCallback( (searchText: string) => ( ), [onClose, threadID], ); return ( {addMembersModalContent} ); } -export { AddMembersModal, AddMembersModalContent }; +function AddMembersModalWrapper(props: Props): React.Node { + const { threadID, onClose } = props; + + return ( + + + + ); +} + +export { AddMembersModalWrapper, AddMembersModalContent }; diff --git a/web/modals/threads/members/members-modal.react.js b/web/modals/threads/members/members-modal.react.js index 18096916b..b801ec683 100644 --- a/web/modals/threads/members/members-modal.react.js +++ b/web/modals/threads/members/members-modal.react.js @@ -1,152 +1,154 @@ // @flow import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { useUserSearchIndex } from 'lib/selectors/nav-selectors.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { roleIsAdminRole, threadHasPermission, } from 'lib/shared/thread-utils.js'; import type { RelativeMemberInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { useRolesFromCommunityThreadInfo } from 'lib/utils/role-utils.js'; -import { AddMembersModal } from './add-members-modal.react.js'; +import { AddMembersModalWrapper } from './add-members-modal.react.js'; import ThreadMembersList from './members-list.react.js'; import css from './members-modal.css'; import Button from '../../../components/button.react.js'; import Tabs, { type TabData } from '../../../components/tabs.react.js'; import { useSelector } from '../../../redux/redux-utils.js'; import SearchModal from '../../search-modal.react.js'; type TabType = 'All Members' | 'Admins'; const tabsData: $ReadOnlyArray> = [ { id: 'All Members', header: 'All Members', }, { id: 'Admins', header: 'Admins', }, ]; type ContentProps = { +searchText: string, +threadID: string, }; function ThreadMembersModalContent(props: ContentProps): React.Node { const { threadID, searchText } = props; const [tab, setTab] = React.useState('All Members'); const threadInfo = useSelector(state => threadInfoSelector(state)[threadID]); const { members: threadMembersNotFiltered } = threadInfo; const userSearchIndex = useUserSearchIndex(threadMembersNotFiltered); const userIDs = React.useMemo( () => userSearchIndex.getSearchResults(searchText), [searchText, userSearchIndex], ); const allMembers = React.useMemo( () => threadMembersNotFiltered.filter( (member: RelativeMemberInfo) => searchText.length === 0 || userIDs.includes(member.id), ), [searchText.length, threadMembersNotFiltered, userIDs], ); const roles = useRolesFromCommunityThreadInfo(threadInfo, allMembers); const adminMembers = React.useMemo( () => allMembers.filter((member: RelativeMemberInfo) => roleIsAdminRole(roles.get(member.id)), ), [allMembers, roles], ); const tabs = React.useMemo( () => , [tab], ); const tabContent = React.useMemo(() => { if (tab === 'All Members') { return ( ); } return ( ); }, [adminMembers, allMembers, tab, threadInfo]); const { pushModal, popModal } = useModalContext(); const onClickAddMembers = React.useCallback(() => { - pushModal(); + pushModal( + , + ); }, [popModal, pushModal, threadID]); const canAddMembers = threadHasPermission( threadInfo, threadPermissions.ADD_MEMBERS, ); const addMembersButton = React.useMemo(() => { if (!canAddMembers) { return null; } return (
); }, [canAddMembers, onClickAddMembers]); const threadMembersModalContent = React.useMemo( () => (
{tabs}
{tabContent}
{addMembersButton}
), [addMembersButton, tabContent, tabs], ); return threadMembersModalContent; } type Props = { +threadID: string, +onClose: () => void, }; function ThreadMembersModal(props: Props): React.Node { const { onClose, threadID } = props; const renderModalContent = React.useCallback( (searchText: string) => ( ), [threadID], ); return ( {renderModalContent} ); } export default ThreadMembersModal; diff --git a/web/settings/relationship/add-users-utils.js b/web/settings/relationship/add-users-utils.js index 29015996b..274eb6057 100644 --- a/web/settings/relationship/add-users-utils.js +++ b/web/settings/relationship/add-users-utils.js @@ -1,108 +1,185 @@ // @flow import * as React from 'react'; import { useSortedENSResolvedUsers } from 'lib/hooks/ens-cache.js'; import { useUserSearchIndex } from 'lib/selectors/nav-selectors.js'; -import { useSearchUsers } from 'lib/shared/search-utils.js'; +import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; +import { userInfoSelectorForPotentialMembers } from 'lib/selectors/user-selectors.js'; +import { + useSearchUsers, + usePotentialMemberItems, +} from 'lib/shared/search-utils.js'; +import { threadActualMembers } from 'lib/shared/thread-utils.js'; import type { UserRelationshipStatus } from 'lib/types/relationship-types.js'; import type { GlobalAccountUserInfo, AccountUserInfo, + UserListItem, } from 'lib/types/user-types.js'; import { values } from 'lib/utils/objects.js'; import { useAddUsersListContext } from './add-users-list-provider.react.js'; import { useSelector } from '../../redux/redux-utils.js'; type UseUserRelationshipUserInfosParams = { +searchText: string, +excludedStatuses: $ReadOnlySet, }; function useUserRelationshipUserInfos( params: UseUserRelationshipUserInfosParams, ): { +mergedUserInfos: { [string]: GlobalAccountUserInfo | AccountUserInfo, }, +sortedUsersWithENSNames: $ReadOnlyArray< GlobalAccountUserInfo | AccountUserInfo, >, } { const { searchText, excludedStatuses } = params; const { previouslySelectedUsers } = useAddUsersListContext(); const viewerID = useSelector(state => state.currentUserInfo?.id); const userInfos = useSelector(state => state.userStore.userInfos); const userInfosArray = React.useMemo(() => values(userInfos), [userInfos]); const userStoreSearchIndex = useUserSearchIndex(userInfosArray); const [userStoreSearchResults, setUserStoreSearchResults] = React.useState< $ReadOnlySet, >(new Set(userStoreSearchIndex.getSearchResults(searchText))); React.useEffect(() => { setUserStoreSearchResults( new Set(userStoreSearchIndex.getSearchResults(searchText)), ); }, [searchText, userStoreSearchIndex]); const serverSearchResults = useSearchUsers(searchText); const searchModeActive = searchText.length > 0; const mergedUserInfos = React.useMemo(() => { const mergedInfos: { [string]: GlobalAccountUserInfo | AccountUserInfo } = {}; for (const userInfo of serverSearchResults) { mergedInfos[userInfo.id] = userInfo; } const userStoreUserIDs = searchModeActive ? userStoreSearchResults : Object.keys(userInfos); for (const id of userStoreUserIDs) { const { username, relationshipStatus } = userInfos[id]; if (username) { mergedInfos[id] = { id, username, relationshipStatus }; } } return mergedInfos; }, [ searchModeActive, serverSearchResults, userInfos, userStoreSearchResults, ]); const filteredUsers = React.useMemo( () => Object.keys(mergedUserInfos) .map(userID => mergedUserInfos[userID]) .filter( user => user.id !== viewerID && (!user.relationshipStatus || !excludedStatuses.has(user.relationshipStatus)) && !previouslySelectedUsers.has(user.id), ), [excludedStatuses, mergedUserInfos, viewerID, previouslySelectedUsers], ); const sortedUsersWithENSNames = useSortedENSResolvedUsers(filteredUsers); const result = React.useMemo( () => ({ mergedUserInfos, sortedUsersWithENSNames, }), [mergedUserInfos, sortedUsersWithENSNames], ); return result; } -export { useUserRelationshipUserInfos }; +type UseAddMembersListUserInfosParams = { + +threadID: string, + +searchText: string, +}; + +function useAddMembersListUserInfos(params: UseAddMembersListUserInfosParams): { + +userInfos: { + [string]: UserListItem, + }, + +sortedUsersWithENSNames: $ReadOnlyArray, +} { + const { threadID, searchText } = params; + + const { previouslySelectedUsers } = useAddUsersListContext(); + + const threadInfo = useSelector(state => threadInfoSelector(state)[threadID]); + const { parentThreadID, community } = threadInfo; + const parentThreadInfo = useSelector(state => + parentThreadID ? threadInfoSelector(state)[parentThreadID] : null, + ); + const communityThreadInfo = useSelector(state => + community ? threadInfoSelector(state)[community] : null, + ); + const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); + const excludeUserIDs = React.useMemo( + () => + threadActualMembers(threadInfo.members).concat( + Array.from(previouslySelectedUsers.keys()), + ), + [previouslySelectedUsers, threadInfo.members], + ); + + const userSearchResults = usePotentialMemberItems({ + text: searchText, + userInfos: otherUserInfos, + excludeUserIDs, + inputParentThreadInfo: parentThreadInfo, + inputCommunityThreadInfo: communityThreadInfo, + threadType: threadInfo.type, + }); + + const userInfos = React.useMemo(() => { + const mergedInfos: { [string]: UserListItem } = {}; + + for (const userInfo of userSearchResults) { + mergedInfos[userInfo.id] = userInfo; + } + + return mergedInfos; + }, [userSearchResults]); + + const usersAvailableToAdd = React.useMemo( + () => userSearchResults.filter(user => !user.alert), + [userSearchResults], + ); + + const sortedUsersWithENSNames = + useSortedENSResolvedUsers(usersAvailableToAdd); + + const result = React.useMemo( + () => ({ + userInfos, + sortedUsersWithENSNames, + }), + [userInfos, sortedUsersWithENSNames], + ); + + return result; +} + +export { useUserRelationshipUserInfos, useAddMembersListUserInfos };