diff --git a/web/modals/threads/create/compose-subchannel-modal.react.js b/web/modals/threads/create/compose-subchannel-modal.react.js
index 506d70043..b3e80b75e 100644
--- a/web/modals/threads/create/compose-subchannel-modal.react.js
+++ b/web/modals/threads/create/compose-subchannel-modal.react.js
@@ -1,289 +1,293 @@
// @flow
import * as React from 'react';
import {
newThreadActionTypes,
useNewThinThread,
} 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 Button from '../../../components/button.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 loadingState = useSelector(createSubchannelLoadingStatusSelector);
const [errorMessage, setErrorMessage] = React.useState('');
const calendarQuery = useSelector(nonThreadCalendarQuery);
const callNewThinThread = useNewThinThread();
const dispatchActionPromise = useDispatchActionPromise();
const dispatch = useDispatch();
+ const threadType = getThreadType(visibilityType, announcement);
+
const createSubchannel = React.useCallback(async () => {
try {
- const threadType = getThreadType(visibilityType, announcement);
-
const query = calendarQuery();
const result = await callNewThinThread({
name: channelName,
type: threadType,
parentThreadID: parentThreadInfo.id,
initialMemberIDs: Array.from(pendingUsersToAdd.keys()),
calendarQuery: query,
color: parentThreadInfo.color,
});
return result;
} catch (e) {
await setErrorMessage('unknown error');
return null;
}
}, [
- visibilityType,
- announcement,
calendarQuery,
callNewThinThread,
channelName,
+ threadType,
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 subchannelSettings = React.useMemo(
() => (
),
[
channelName,
visibilityType,
announcement,
onChangeChannelName,
onOpenVisibilityTypeSelected,
onSecretVisibilityTypeSelected,
onAnnouncementSelected,
],
);
const subchannelMembers = React.useMemo(
- () => ,
- [parentThreadInfo],
+ () => (
+
+ ),
+ [parentThreadInfo, threadType],
);
const modalName =
activeStep === 'members'
? `Create channel - ${trimText(channelName, 11)}`
: 'Create channel';
const subheader = React.useMemo(
() => ,
[parentThreadName],
);
const modalContent = React.useMemo(() => {
if (activeStep === 'settings') {
return subchannelSettings;
}
return subchannelMembers;
}, [activeStep, subchannelMembers, subchannelSettings]);
const onClickNext = React.useCallback(() => {
setErrorMessage('');
setChannelName(channelName.trim());
setActiveStep('members');
}, [channelName]);
const primaryButton = React.useMemo(() => {
if (activeStep === 'settings') {
return (
);
}
return (
);
}, [
activeStep,
channelName,
dispatchCreateSubchannel,
loadingState,
onClickNext,
pendingUsersToAdd.size,
]);
const onClickBack = React.useCallback(() => setActiveStep('settings'), []);
const secondaryButton = React.useMemo(() => {
if (activeStep !== 'members') {
return null;
}
return (
);
}, [activeStep, onClickBack]);
return (
{modalContent}
{errorMessage}
);
}
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.react.js b/web/modals/threads/create/steps/subchannel-members.react.js
index 87dde2ba3..7eef5efb0 100644
--- a/web/modals/threads/create/steps/subchannel-members.react.js
+++ b/web/modals/threads/create/steps/subchannel-members.react.js
@@ -1,45 +1,48 @@
// @flow
import * as React from 'react';
import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
+import type { ThreadType } from 'lib/types/thread-types-enum.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,
+ +threadType: ThreadType,
};
function SubchannelMembers(props: SubchannelMembersProps): React.Node {
- const { parentThreadInfo } = props;
+ const { parentThreadInfo, threadType } = props;
const [searchUserText, setSearchUserText] = React.useState('');
const { userInfos, sortedUsersWithENSNames } =
useSubchannelAddMembersListUserInfos({
- parentThreadID: parentThreadInfo.id,
+ parentThreadInfo,
searchText: searchUserText,
+ threadType,
});
return (
<>
0}
userInfos={userInfos}
sortedUsersWithENSNames={sortedUsersWithENSNames}
/>
>
);
}
export default SubchannelMembers;
diff --git a/web/settings/relationship/add-users-list.react.js b/web/settings/relationship/add-users-list.react.js
index a5adedc99..618cef610 100644
--- a/web/settings/relationship/add-users-list.react.js
+++ b/web/settings/relationship/add-users-list.react.js
@@ -1,191 +1,191 @@
// @flow
import * as React from 'react';
import { useSortedENSResolvedUsers } from 'lib/hooks/ens-cache.js';
import { stringForUser } from 'lib/shared/user-utils.js';
import AddUsersListItem, {
type BaseAddUserInfo,
} from './add-users-list-item.react.js';
import { useAddUsersListContext } from './add-users-list-provider.react.js';
import css from './add-users-list.css';
import Button from '../../components/button.react.js';
type Props = {
+searchModeActive: boolean,
+userInfos: {
- [string]: T,
+ +[string]: T,
},
+sortedUsersWithENSNames: $ReadOnlyArray,
};
function AddUsersList(props: Props): React.Node {
const { searchModeActive, userInfos, sortedUsersWithENSNames } = props;
const {
pendingUsersToAdd,
setPendingUsersToAdd,
previouslySelectedUsers,
setPreviouslySelectedUsers,
errorMessage,
} = useAddUsersListContext();
React.useEffect(() => {
setPreviouslySelectedUsers(pendingUsersToAdd);
// We want this effect to run ONLY when searchModeActive changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchModeActive]);
const previouslySelectedUsersList = React.useMemo(
() => Array.from(previouslySelectedUsers.values()),
[previouslySelectedUsers],
);
const toggleUser = React.useCallback(
(userID: string) => {
setPendingUsersToAdd(pendingUsers => {
const newPendingUsers = new Map(pendingUsers);
if (newPendingUsers.delete(userID)) {
return newPendingUsers;
}
if (!previouslySelectedUsers.has(userID)) {
const newPendingUser: BaseAddUserInfo = {
id: userID,
username: stringForUser(userInfos[userID]),
};
newPendingUsers.set(userID, newPendingUser);
}
const newPendingUser = previouslySelectedUsers.get(userID);
if (newPendingUser) {
newPendingUsers.set(userID, newPendingUser);
}
return newPendingUsers;
});
},
[userInfos, setPendingUsersToAdd, previouslySelectedUsers],
);
const userRows = React.useMemo(
() =>
sortedUsersWithENSNames.map(userInfo => (
)),
[sortedUsersWithENSNames, toggleUser, pendingUsersToAdd],
);
const sortedPreviouslySelectedUsersWithENSNames = useSortedENSResolvedUsers(
previouslySelectedUsersList,
);
const previouslySelectedUserRows = React.useMemo(() => {
if (searchModeActive || previouslySelectedUsers.size === 0) {
return null;
}
const sortedPreviouslySelectedUserRows =
sortedPreviouslySelectedUsersWithENSNames.map(userInfo => (
));
return (
{sortedPreviouslySelectedUserRows}
);
}, [
searchModeActive,
previouslySelectedUsers.size,
sortedPreviouslySelectedUsersWithENSNames,
toggleUser,
pendingUsersToAdd,
]);
const onClickClearAll = React.useCallback(() => {
setPendingUsersToAdd(new Map());
}, [setPendingUsersToAdd]);
const clearAllButtonColor = React.useMemo(() => {
if (pendingUsersToAdd.size === 0) {
return { color: 'var(--link-background-secondary-disabled)' };
}
return { color: 'var(--link-background-secondary-default)' };
}, [pendingUsersToAdd.size]);
const clearAllButton = React.useMemo(() => {
if (searchModeActive) {
return null;
}
return (
);
}, [
clearAllButtonColor,
onClickClearAll,
pendingUsersToAdd.size,
searchModeActive,
]);
const listHeader = React.useMemo(() => {
let selectionText = 'Select users';
if (searchModeActive) {
selectionText = 'Search results:';
} else if (pendingUsersToAdd.size > 0) {
selectionText = `${pendingUsersToAdd.size} selected`;
}
return (
{selectionText}
{clearAllButton}
);
}, [clearAllButton, pendingUsersToAdd.size, searchModeActive]);
let errors;
if (errorMessage) {
errors = {errorMessage}
;
}
const addUsersList = React.useMemo(
() => (
{listHeader}
{previouslySelectedUserRows}
{userRows}
{errors}
),
[errors, listHeader, userRows, previouslySelectedUserRows],
);
return addUsersList;
}
export default AddUsersList;
diff --git a/web/settings/relationship/add-users-utils.js b/web/settings/relationship/add-users-utils.js
index 903bb04a8..44effdbea 100644
--- a/web/settings/relationship/add-users-utils.js
+++ b/web/settings/relationship/add-users-utils.js
@@ -1,261 +1,242 @@
// @flow
+import _keyBy from 'lodash/fp/keyBy.js';
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 { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
import type { UserRelationshipStatus } from 'lib/types/relationship-types.js';
+import type { ThreadType } from 'lib/types/thread-types-enum.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;
}
type UseSubchannelAddMembersListUserInfosParams = {
- +parentThreadID: string,
+ +parentThreadInfo: ThreadInfo,
+searchText: string,
+ +threadType: ThreadType,
};
function useSubchannelAddMembersListUserInfos(
params: UseSubchannelAddMembersListUserInfosParams,
): {
+userInfos: {
- [string]: RelativeMemberInfo,
+ +[string]: UserListItem,
},
- +sortedUsersWithENSNames: $ReadOnlyArray,
+ +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 { searchText, parentThreadInfo, threadType } = params;
- const searchResult = React.useMemo(
- () => new Set(userSearchIndex.getSearchResults(searchText)),
- [userSearchIndex, searchText],
+ const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers);
+ const { community } = parentThreadInfo;
+ const communityThreadInfo = useSelector(state =>
+ community ? threadInfoSelector(state)[community] : null,
);
- 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 { previouslySelectedUsers } = useAddUsersListContext();
+ const previouslySelectedUserIDs = React.useMemo(
+ () => [...previouslySelectedUsers].map(([key]) => key),
+ [previouslySelectedUsers],
);
- const otherMemberListWithoutENSNames = React.useMemo(
- () => filterOutOtherMembersWithENSNames(communityThreadInfo.members),
- [communityThreadInfo.members, filterOutOtherMembersWithENSNames],
- );
+ const userSearchResults = usePotentialMemberItems({
+ text: searchText,
+ userInfos: otherUserInfos,
+ excludeUserIDs: previouslySelectedUserIDs,
+ inputParentThreadInfo: parentThreadInfo,
+ inputCommunityThreadInfo: communityThreadInfo,
+ threadType,
+ });
+ const userSearchResultWithENSNames =
+ useSortedENSResolvedUsers(userSearchResults);
- const sortedUsersWithENSNames = useSortedENSResolvedUsers(
- otherMemberListWithoutENSNames,
+ const userResults: { [id: string]: UserListItem } = React.useMemo(
+ () => _keyBy('id')(userSearchResults),
+ [userSearchResults],
);
return {
- userInfos,
- sortedUsersWithENSNames,
+ userInfos: userResults,
+ sortedUsersWithENSNames: userSearchResultWithENSNames,
};
}
export {
useUserRelationshipUserInfos,
useAddMembersListUserInfos,
useSubchannelAddMembersListUserInfos,
};