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(
() => (
<>
>
),
[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 };