diff --git a/web/modals/threads/create/compose-subchannel-modal.react.js b/web/modals/threads/create/compose-subchannel-modal.react.js
index e8a335564..af035e9b9 100644
--- a/web/modals/threads/create/compose-subchannel-modal.react.js
+++ b/web/modals/threads/create/compose-subchannel-modal.react.js
@@ -1,276 +1,271 @@
// @flow
import * as React from 'react';
import {
newThreadActionTypes,
useNewThread,
} from 'lib/actions/thread-actions.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
import { threadTypes } from 'lib/types/thread-types-enum.js';
import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js';
import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js';
import { useDispatch } from 'lib/utils/redux-utils.js';
import { trimText } from 'lib/utils/text-utils.js';
import css from './compose-subchannel-modal.css';
import SubchannelMembers from './steps/subchannel-members.react.js';
import type { VisibilityType } from './steps/subchannel-settings.react.js';
import SubchannelSettings from './steps/subchannel-settings.react.js';
import Stepper from '../../../components/stepper.react.js';
import { updateNavInfoActionType } from '../../../redux/action-types.js';
import { useSelector } from '../../../redux/redux-utils.js';
import { nonThreadCalendarQuery } from '../../../selectors/nav-selectors.js';
+import {
+ useAddUsersListContext,
+ AddUsersListProvider,
+} from '../../../settings/relationship/add-users-list-provider.react.js';
import Modal from '../../modal.react.js';
type Props = {
+onClose: () => void,
+parentThreadInfo: ThreadInfo,
};
const getThreadType = (visibility: VisibilityType, announcement: boolean) => {
if (visibility === 'open') {
return announcement
? threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD
: threadTypes.COMMUNITY_OPEN_SUBTHREAD;
} else {
return announcement
? threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD
: threadTypes.COMMUNITY_SECRET_SUBTHREAD;
}
};
type Steps = 'settings' | 'members';
type HeaderProps = {
+parentThreadName: string,
};
function ComposeSubchannelHeader(props: HeaderProps): React.Node {
const { parentThreadName } = props;
return (
{'within '}
{parentThreadName}
);
}
const createSubchannelLoadingStatusSelector =
createLoadingStatusSelector(newThreadActionTypes);
function ComposeSubchannelModal(props: Props): React.Node {
const { parentThreadInfo, onClose } = props;
const { uiName: parentThreadName } = useResolvedThreadInfo(parentThreadInfo);
+ const { pendingUsersToAdd } = useAddUsersListContext();
+
const [activeStep, setActiveStep] = React.useState('settings');
const [channelName, setChannelName] = React.useState('');
const [visibilityType, setVisibilityType] =
React.useState('open');
const [announcement, setAnnouncement] = React.useState(false);
- const [selectedUsers, setSelectedUsers] = React.useState<
- $ReadOnlySet,
- >(new Set());
- const [searchUserText, setSearchUserText] = React.useState('');
const loadingState = useSelector(createSubchannelLoadingStatusSelector);
const [errorMessage, setErrorMessage] = React.useState('');
const calendarQuery = useSelector(nonThreadCalendarQuery);
const callNewThread = useNewThread();
const dispatchActionPromise = useDispatchActionPromise();
const dispatch = useDispatch();
const createSubchannel = React.useCallback(async () => {
try {
const threadType = getThreadType(visibilityType, announcement);
const query = calendarQuery();
const result = await callNewThread({
name: channelName,
type: threadType,
parentThreadID: parentThreadInfo.id,
- initialMemberIDs: Array.from(selectedUsers),
+ initialMemberIDs: Array.from(pendingUsersToAdd.keys()),
calendarQuery: query,
color: parentThreadInfo.color,
});
return result;
} catch (e) {
await setErrorMessage('unknown error');
return null;
}
}, [
- parentThreadInfo,
- selectedUsers,
visibilityType,
announcement,
- callNewThread,
calendarQuery,
+ callNewThread,
channelName,
+ parentThreadInfo.id,
+ parentThreadInfo.color,
+ pendingUsersToAdd,
]);
const dispatchCreateSubchannel = React.useCallback(async () => {
await setErrorMessage('');
const response = createSubchannel();
await dispatchActionPromise(newThreadActionTypes, response);
const result = await response;
if (result) {
const { newThreadID } = result;
await dispatch({
type: updateNavInfoActionType,
payload: {
activeChatThreadID: newThreadID,
},
});
props.onClose();
}
}, [dispatchActionPromise, createSubchannel, props, dispatch]);
const onChangeChannelName = React.useCallback(
(event: SyntheticEvent) => {
const target = event.currentTarget;
setChannelName(target.value);
},
[],
);
const onOpenVisibilityTypeSelected = React.useCallback(
() => setVisibilityType('open'),
[],
);
const onSecretVisibilityTypeSelected = React.useCallback(
() => setVisibilityType('secret'),
[],
);
const onAnnouncementSelected = React.useCallback(
() => setAnnouncement(!announcement),
[announcement],
);
- const toggleUserSelection = React.useCallback((userID: string) => {
- setSelectedUsers((users: $ReadOnlySet) => {
- const newUsers = new Set(users);
- if (newUsers.has(userID)) {
- newUsers.delete(userID);
- } else {
- newUsers.add(userID);
- }
- return newUsers;
- });
- }, []);
-
const subchannelSettings = React.useMemo(
() => (
),
[
channelName,
visibilityType,
announcement,
onChangeChannelName,
onOpenVisibilityTypeSelected,
onSecretVisibilityTypeSelected,
onAnnouncementSelected,
],
);
const stepperButtons = React.useMemo(
() => ({
settings: {
nextProps: {
content: 'Next',
disabled: !channelName.trim(),
onClick: () => {
setErrorMessage('');
setChannelName(channelName.trim());
setActiveStep('members');
},
},
},
members: {
prevProps: {
content: 'Back',
onClick: () => setActiveStep('settings'),
},
nextProps: {
content: 'Create',
loading: loadingState === 'loading',
- disabled: selectedUsers.size === 0,
+ disabled: pendingUsersToAdd.size === 0,
onClick: dispatchCreateSubchannel,
},
},
}),
- [channelName, dispatchCreateSubchannel, loadingState, selectedUsers],
+ [
+ channelName,
+ dispatchCreateSubchannel,
+ loadingState,
+ pendingUsersToAdd.size,
+ ],
);
const subchannelMembers = React.useMemo(
- () => (
-
- ),
- [
- selectedUsers,
- toggleUserSelection,
- parentThreadInfo,
- searchUserText,
- setSearchUserText,
- ],
+ () => ,
+ [parentThreadInfo],
);
const modalName =
activeStep === 'members'
? `Create channel - ${trimText(channelName, 11)}`
: 'Create channel';
return (
);
}
-export default ComposeSubchannelModal;
+function ComposeSubchannelModalWrapper(props: Props): React.Node {
+ const composeSubchannelModalWrapper = React.useMemo(
+ () => (
+
+
+
+ ),
+ [props],
+ );
+
+ return composeSubchannelModalWrapper;
+}
+
+export default ComposeSubchannelModalWrapper;
diff --git a/web/modals/threads/create/steps/subchannel-members-list.react.js b/web/modals/threads/create/steps/subchannel-members-list.react.js
deleted file mode 100644
index c08294c8f..000000000
--- a/web/modals/threads/create/steps/subchannel-members-list.react.js
+++ /dev/null
@@ -1,108 +0,0 @@
-// @flow
-
-import * as React from 'react';
-
-import { useENSNames } from 'lib/hooks/ens-cache.js';
-import { stringForUser } from 'lib/shared/user-utils.js';
-import type {
- RelativeMemberInfo,
- ThreadInfo,
-} from 'lib/types/minimally-encoded-thread-permissions-types.js';
-import type { UserListItem } from 'lib/types/user-types.js';
-
-import { useSelector } from '../../../../redux/redux-utils.js';
-import AddMembersList from '../../../components/add-members-list.react.js';
-
-type Props = {
- +searchText: string,
- +searchResult: $ReadOnlySet,
- +communityThreadInfo: ThreadInfo,
- +parentThreadInfo: ThreadInfo,
- +selectedUsers: $ReadOnlySet,
- +toggleUserSelection: (userID: string) => void,
-};
-
-function SubchannelMembersList(props: Props): React.Node {
- const {
- searchText,
- searchResult,
- communityThreadInfo,
- parentThreadInfo,
- selectedUsers,
- toggleUserSelection,
- } = props;
-
- const { name: communityName } = communityThreadInfo;
-
- const currentUserId = useSelector(state => state.currentUserInfo?.id);
-
- const parentMembersSet = React.useMemo(
- () => new Set(parentThreadInfo.members.map(user => user.id)),
- [parentThreadInfo],
- );
-
- const filterOutParentMembersWithENSNames = React.useCallback(
- (members: $ReadOnlyArray) =>
- members
- .filter(
- user =>
- user.id !== currentUserId &&
- (searchResult.has(user.id) || searchText.length === 0),
- )
- .map(user => ({ id: user.id, username: stringForUser(user) })),
- [currentUserId, searchResult, searchText.length],
- );
-
- const parentMemberListWithoutENSNames = React.useMemo(
- () => filterOutParentMembersWithENSNames(parentThreadInfo.members),
- [filterOutParentMembersWithENSNames, parentThreadInfo.members],
- );
-
- const parentMemberList = useENSNames(
- parentMemberListWithoutENSNames,
- );
-
- const filterOutOtherMembersWithENSNames = React.useCallback(
- (members: $ReadOnlyArray) =>
- members
- .filter(
- user =>
- !parentMembersSet.has(user.id) &&
- user.id !== currentUserId &&
- (searchResult.has(user.id) || searchText.length === 0),
- )
- .map(user => ({ id: user.id, username: stringForUser(user) })),
- [currentUserId, parentMembersSet, searchResult, searchText.length],
- );
-
- const otherMemberListWithoutENSNames = React.useMemo(
- () => filterOutOtherMembersWithENSNames(communityThreadInfo.members),
- [communityThreadInfo.members, filterOutOtherMembersWithENSNames],
- );
-
- const otherMemberList = useENSNames(
- otherMemberListWithoutENSNames,
- );
-
- const sortedGroupedUserList = React.useMemo(
- () =>
- [
- { header: 'Users in parent channel', userInfos: parentMemberList },
- {
- header: `All users in ${communityName ?? 'community'}`,
- userInfos: otherMemberList,
- },
- ].filter(item => item.userInfos.length),
- [parentMemberList, otherMemberList, communityName],
- );
-
- return (
-
- );
-}
-
-export default SubchannelMembersList;
diff --git a/web/modals/threads/create/steps/subchannel-members.css b/web/modals/threads/create/steps/subchannel-members.css
index 3dcd4c365..46f36c541 100644
--- a/web/modals/threads/create/steps/subchannel-members.css
+++ b/web/modals/threads/create/steps/subchannel-members.css
@@ -1,10 +1,5 @@
-.members {
- overflow-y: auto;
-}
-
.searchBar {
- background-color: var(--modal-bg);
position: sticky;
- padding: 2.5px 0;
top: 0;
+ z-index: 1;
}
diff --git a/web/modals/threads/create/steps/subchannel-members.react.js b/web/modals/threads/create/steps/subchannel-members.react.js
index a65940884..87dde2ba3 100644
--- a/web/modals/threads/create/steps/subchannel-members.react.js
+++ b/web/modals/threads/create/steps/subchannel-members.react.js
@@ -1,63 +1,45 @@
// @flow
import * as React from 'react';
-import { useUserSearchIndex } from 'lib/selectors/nav-selectors.js';
-import { useAncestorThreads } from 'lib/shared/ancestor-threads.js';
import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
-import MembersList from './subchannel-members-list.react.js';
import css from './subchannel-members.css';
import Search from '../../../../components/search.react.js';
+import AddUsersList from '../../../../settings/relationship/add-users-list.react.js';
+import { useSubchannelAddMembersListUserInfos } from '../../../../settings/relationship/add-users-utils.js';
type SubchannelMembersProps = {
+parentThreadInfo: ThreadInfo,
- +selectedUsers: $ReadOnlySet,
- +searchText: string,
- +setSearchText: string => void,
- +toggleUserSelection: (userID: string) => void,
};
function SubchannelMembers(props: SubchannelMembersProps): React.Node {
- const {
- toggleUserSelection,
- searchText,
- setSearchText,
- parentThreadInfo,
- selectedUsers,
- } = props;
-
- const ancestorThreads = useAncestorThreads(parentThreadInfo);
-
- const communityThread = ancestorThreads[0] ?? parentThreadInfo;
-
- const userSearchIndex = useUserSearchIndex(communityThread.members);
- const searchResult = React.useMemo(
- () => new Set(userSearchIndex.getSearchResults(searchText)),
- [userSearchIndex, searchText],
- );
+ const { parentThreadInfo } = props;
+
+ const [searchUserText, setSearchUserText] = React.useState('');
+
+ const { userInfos, sortedUsersWithENSNames } =
+ useSubchannelAddMembersListUserInfos({
+ parentThreadID: parentThreadInfo.id,
+ searchText: searchUserText,
+ });
return (
<>
-
-
-
+ 0}
+ userInfos={userInfos}
+ sortedUsersWithENSNames={sortedUsersWithENSNames}
+ />
>
);
}
export default SubchannelMembers;
diff --git a/web/settings/relationship/add-users-utils.js b/web/settings/relationship/add-users-utils.js
index 274eb6057..903bb04a8 100644
--- a/web/settings/relationship/add-users-utils.js
+++ b/web/settings/relationship/add-users-utils.js
@@ -1,185 +1,261 @@
// @flow
import * as React from 'react';
import { useSortedENSResolvedUsers } from 'lib/hooks/ens-cache.js';
import { useUserSearchIndex } from 'lib/selectors/nav-selectors.js';
import { threadInfoSelector } from 'lib/selectors/thread-selectors.js';
import { userInfoSelectorForPotentialMembers } from 'lib/selectors/user-selectors.js';
+import { useAncestorThreads } from 'lib/shared/ancestor-threads.js';
import {
useSearchUsers,
usePotentialMemberItems,
} from 'lib/shared/search-utils.js';
import { threadActualMembers } from 'lib/shared/thread-utils.js';
+import type { RelativeMemberInfo } from 'lib/types/minimally-encoded-thread-permissions-types.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;
}
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 };
+type UseSubchannelAddMembersListUserInfosParams = {
+ +parentThreadID: string,
+ +searchText: string,
+};
+
+function useSubchannelAddMembersListUserInfos(
+ params: UseSubchannelAddMembersListUserInfosParams,
+): {
+ +userInfos: {
+ [string]: RelativeMemberInfo,
+ },
+ +sortedUsersWithENSNames: $ReadOnlyArray,
+} {
+ const { parentThreadID, searchText } = params;
+
+ const { previouslySelectedUsers } = useAddUsersListContext();
+
+ const parentThreadInfo = useSelector(
+ state => threadInfoSelector(state)[parentThreadID],
+ );
+
+ const currentUserID = useSelector(state => state.currentUserInfo?.id);
+
+ const ancestorThreads = useAncestorThreads(parentThreadInfo);
+
+ const communityThreadInfo = ancestorThreads[0] ?? parentThreadInfo;
+
+ const userInfos = React.useMemo(() => {
+ const infos: { [string]: RelativeMemberInfo } = {};
+
+ for (const member of communityThreadInfo.members) {
+ infos[member.id] = member;
+ }
+
+ return infos;
+ }, [communityThreadInfo.members]);
+
+ const userSearchIndex = useUserSearchIndex(communityThreadInfo.members);
+
+ const searchResult = React.useMemo(
+ () => new Set(userSearchIndex.getSearchResults(searchText)),
+ [userSearchIndex, searchText],
+ );
+
+ const filterOutOtherMembersWithENSNames = React.useCallback(
+ (members: $ReadOnlyArray) =>
+ members.filter(
+ user =>
+ !previouslySelectedUsers.has(user.id) &&
+ user.id !== currentUserID &&
+ (searchResult.has(user.id) || searchText.length === 0),
+ ),
+ [currentUserID, previouslySelectedUsers, searchResult, searchText.length],
+ );
+
+ const otherMemberListWithoutENSNames = React.useMemo(
+ () => filterOutOtherMembersWithENSNames(communityThreadInfo.members),
+ [communityThreadInfo.members, filterOutOtherMembersWithENSNames],
+ );
+
+ const sortedUsersWithENSNames = useSortedENSResolvedUsers(
+ otherMemberListWithoutENSNames,
+ );
+
+ return {
+ userInfos,
+ sortedUsersWithENSNames,
+ };
+}
+
+export {
+ useUserRelationshipUserInfos,
+ useAddMembersListUserInfos,
+ useSubchannelAddMembersListUserInfos,
+};