diff --git a/web/modals/threads/members/add-members-group.react.js b/web/modals/components/add-members-group.react.js similarity index 96% rename from web/modals/threads/members/add-members-group.react.js rename to web/modals/components/add-members-group.react.js index 108cc44cd..5cf8e799d 100644 --- a/web/modals/threads/members/add-members-group.react.js +++ b/web/modals/components/add-members-group.react.js @@ -1,47 +1,47 @@ // @flow import * as React from 'react'; import type { UserListItem } from 'lib/types/user-types'; import AddMembersItem from './add-members-item.react'; -import css from './members-modal.css'; +import css from './add-members.css'; type AddMemberItemGroupProps = { +header: ?string, +userInfos: $ReadOnlyArray, +onUserClick: (userID: string) => void, +usersAdded: $ReadOnlySet, }; function AddMembersItemGroup(props: AddMemberItemGroupProps): React.Node { const { userInfos, onUserClick, usersAdded, header } = props; const sortedUserInfos = React.useMemo(() => { return [...userInfos].sort((a, b) => a.username.localeCompare(b.username)); }, [userInfos]); const userInfosComponents = React.useMemo( () => sortedUserInfos.map(userInfo => ( )), [onUserClick, sortedUserInfos, usersAdded], ); const headerComponent = header ? (
{header}:
) : null; return ( <> {headerComponent} {userInfosComponents} ); } export default AddMembersItemGroup; diff --git a/web/modals/threads/members/add-members-item.react.js b/web/modals/components/add-members-item.react.js similarity index 96% rename from web/modals/threads/members/add-members-item.react.js rename to web/modals/components/add-members-item.react.js index 9254d4913..365d10dde 100644 --- a/web/modals/threads/members/add-members-item.react.js +++ b/web/modals/components/add-members-item.react.js @@ -1,50 +1,50 @@ // @flow import * as React from 'react'; import type { UserListItem } from 'lib/types/user-types'; -import css from './members-modal.css'; +import css from './add-members.css'; 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 onClickCallback = React.useCallback(() => { if (!canBeAdded) { return; } onClick(userInfo.id); }, [canBeAdded, onClick, userInfo.id]); const action = React.useMemo(() => { if (!canBeAdded) { return userInfo.alertTitle; } if (userAdded) { return Remove; } else { return 'Add'; } }, [canBeAdded, userAdded, userInfo.alertTitle]); return ( ); } export default AddMemberItem; diff --git a/web/modals/components/add-members-list.react.js b/web/modals/components/add-members-list.react.js new file mode 100644 index 000000000..b023e55e0 --- /dev/null +++ b/web/modals/components/add-members-list.react.js @@ -0,0 +1,40 @@ +// @flow + +import * as React from 'react'; + +import type { UserListItem } from 'lib/types/user-types'; + +import AddMembersItemGroup from './add-members-group.react'; + +type MemberGroupItem = { + +header: ?string, + +userInfos: $ReadOnlyArray, +}; + +type Props = { + +switchUser: string => void, + +pendingUsersToAdd: $ReadOnlySet, + +sortedGroupedUsersList: $ReadOnlyArray, +}; + +function AddMembersList(props: Props): React.Node { + const { switchUser, pendingUsersToAdd, sortedGroupedUsersList } = props; + + const addMembersList = React.useMemo( + () => + sortedGroupedUsersList.map(({ header, userInfos }) => ( + + )), + [sortedGroupedUsersList, switchUser, pendingUsersToAdd], + ); + + return addMembersList; +} + +export default AddMembersList; diff --git a/web/modals/components/add-members.css b/web/modals/components/add-members.css new file mode 100644 index 000000000..c5c2d6634 --- /dev/null +++ b/web/modals/components/add-members.css @@ -0,0 +1,41 @@ +div.addMemberItemsGroupHeader { + font-size: var(--s-font-14); + color: var(--add-members-group-header-color); + margin: 16px; +} + +button.addMemberItem { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + color: var(--add-members-item-color); + font-size: var(--l-font-18); + background-color: transparent; + border: none; + width: 100%; +} + +button.addMemberItem:hover { + color: var(--add-members-item-color-hover); +} + +button.addMemberItem:disabled { + color: var(--add-members-item-disabled-color); + cursor: not-allowed; +} + +button.addMemberItem:hover:disabled { + color: var(--add-members-item-disabled-color-hover); +} + +button.addMemberItem .label { + padding: 8px 16px; +} + +button.addMemberItem .danger { + color: var(--add-members-remove-pending-color); +} +button.addMemberItem:hover .danger { + color: var(--add-members-remove-pending-color-hover); +} diff --git a/web/modals/threads/members/add-members-list.react.js b/web/modals/threads/members/add-members-list-content.react.js similarity index 81% rename from web/modals/threads/members/add-members-list.react.js rename to web/modals/threads/members/add-members-list-content.react.js index ba1a90bc9..0cf67af5d 100644 --- a/web/modals/threads/members/add-members-list.react.js +++ b/web/modals/threads/members/add-members-list-content.react.js @@ -1,85 +1,85 @@ // @flow import _groupBy from 'lodash/fp/groupBy'; import _toPairs from 'lodash/fp/toPairs'; import * as React from 'react'; import type { UserListItem } from 'lib/types/user-types'; -import AddMembersItemGroup from './add-members-group.react'; +import AddMembersList from '../../components/add-members-list.react'; type Props = { +userListItems: $ReadOnlyArray, +pendingUsersToAdd: $ReadOnlySet, +switchUser: string => void, +hasParentThread: boolean, }; -function AddMembersList(props: Props): React.Node { +function AddMembersListContent(props: Props): React.Node { const { userListItems, pendingUsersToAdd, switchUser, hasParentThread, } = props; const usersAvailableToAdd = React.useMemo( () => userListItems.filter(user => !user.alertText), [userListItems], ); const groupedAvailableUsersList = React.useMemo( () => _groupBy(userInfo => userInfo.notice)(usersAvailableToAdd), [usersAvailableToAdd], ); const membersInParentThread = React.useMemo(() => { if (!groupedAvailableUsersList['undefined']) { return; } 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); if (!usersUnavailable.length) { return null; } return ['Unavailable users', usersUnavailable]; }, [userListItems]); const sortedGroupedUsersList = React.useMemo( () => [ membersInParentThread, ...membersNotInParentThread, usersUnavailableToAdd, - ].filter(Boolean), + ] + .filter(Boolean) + .map(([header, userInfos]) => ({ header, userInfos })), [membersInParentThread, membersNotInParentThread, usersUnavailableToAdd], ); - return sortedGroupedUsersList.map(([header, userInfos]) => ( - - )); + ); } -export default AddMembersList; +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 771a0a6be..1844f1326 100644 --- a/web/modals/threads/members/add-members-modal.react.js +++ b/web/modals/threads/members/add-members-modal.react.js @@ -1,196 +1,196 @@ // @flow import * as React from 'react'; import { changeThreadSettingsActionTypes, changeThreadSettings, } from 'lib/actions/thread-actions'; import { threadInfoSelector } from 'lib/selectors/thread-selectors'; import { userSearchIndexForPotentialMembers, userInfoSelectorForPotentialMembers, } from 'lib/selectors/user-selectors'; import { getPotentialMemberItems } from 'lib/shared/search-utils'; import { threadActualMembers } from 'lib/shared/thread-utils'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; import Button from '../../../components/button.react'; import Label from '../../../components/label.react'; import { useSelector } from '../../../redux/redux-utils'; import SearchModal from '../../search-modal.react'; -import AddMembersList from './add-members-list.react'; +import AddMembersListContent from './add-members-list-content.react'; import css from './members-modal.css'; 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 userSearchIndex = useSelector(userSearchIndexForPotentialMembers); const excludeUserIDs = React.useMemo( () => threadActualMembers(threadInfo.members).concat( Array.from(pendingUsersToAdd), ), [pendingUsersToAdd, threadInfo.members], ); const userSearchResults = React.useMemo( () => getPotentialMemberItems( searchText, otherUserInfos, userSearchIndex, excludeUserIDs, parentThreadInfo, communityThreadInfo, threadInfo.type, ), [ communityThreadInfo, excludeUserIDs, otherUserInfos, parentThreadInfo, searchText, threadInfo.type, userSearchIndex, ], ); const onSwitchUser = React.useCallback( userID => 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 = useServerCall(changeThreadSettings); const addUsers = React.useCallback(() => { dispatchActionPromise( changeThreadSettingsActionTypes, callChangeThreadSettings({ threadID, changes: { newMemberIDs: Array.from(pendingUsersToAdd) }, }), ); onClose(); }, [ callChangeThreadSettings, dispatchActionPromise, onClose, pendingUsersToAdd, threadID, ]); const pendingUsersWithNames = React.useMemo( () => Array.from(pendingUsersToAdd) .map(userID => [userID, otherUserInfos[userID].username]) .sort((a, b) => a[1].localeCompare(b[1])), [otherUserInfos, pendingUsersToAdd], ); const labelItems = React.useMemo(() => { if (!pendingUsersWithNames.length) { return null; } return (
{pendingUsersWithNames.map(([userID, username]) => ( ))}
); }, [onSwitchUser, pendingUsersWithNames]); return (
{labelItems}
-
); } 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 default AddMembersModal; diff --git a/web/modals/threads/members/members-modal.css b/web/modals/threads/members/members-modal.css index 19714be4a..fd82f3cae 100644 --- a/web/modals/threads/members/members-modal.css +++ b/web/modals/threads/members/members-modal.css @@ -1,135 +1,93 @@ div.modalContentContainer { width: 383px; height: 617px; overflow: hidden; display: flex; flex-direction: column; row-gap: 16px; } div.membersListTabs { flex: 1; overflow: hidden; } div.addNewMembers button { width: 100%; } div.membersList { overflow: auto; padding: 8px 0; color: var(--members-modal-member-text); } div.noScroll { overflow: hidden; } div.memberContainer { display: flex; flex-direction: row; justify-content: space-between; padding: 8px 16px; } div.memberContainer:hover { color: var(--members-modal-member-text-hover); } div.memberContainerWithMenuOpen { color: var(--members-modal-member-text-hover); } div.memberInfo { font-size: var(--l-font-18); display: flex; align-items: center; gap: 10px; } div.memberAction { position: relative; } h5.memberletterHeader { margin: 16px; color: var(--members-modal-member-text); font-size: var(--s-font-14); } div.noUsers { padding-top: 16px; text-align: center; } div.addMembersContent { display: flex; flex-direction: column; overflow: auto; color: var(--fg); width: 383px; height: 617px; } div.addMembersPendingList { display: flex; flex-direction: row; flex-wrap: wrap; gap: 6px; padding: 8px; } div.addMembersListContainer { overflow: auto; flex: 1; } div.addMembersFooter { display: flex; justify-content: end; column-gap: 16px; margin-top: 16px; } - -div.addMemberItemsGroupHeader { - font-size: var(--s-font-14); - color: var(--add-members-group-header-color); - margin: 16px; -} - -button.addMemberItem { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - color: var(--add-members-item-color); - font-size: var(--l-font-18); - background-color: transparent; - border: none; - width: 100%; -} - -button.addMemberItem:hover { - color: var(--add-members-item-color-hover); -} - -button.addMemberItem:disabled { - color: var(--add-members-item-disabled-color); - cursor: not-allowed; -} - -button.addMemberItem:hover:disabled { - color: var(--add-members-item-disabled-color-hover); -} - -button.addMemberItem .label { - padding: 8px 16px; -} - -button.addMemberItem .danger { - color: var(--add-members-remove-pending-color); -} -button.addMemberItem:hover .danger { - color: var(--add-members-remove-pending-color-hover); -}