diff --git a/web/modals/threads/members/add-members-modal.react.js b/web/modals/threads/members/add-members-modal.react.js index 60923a742..9b018ef06 100644 --- a/web/modals/threads/members/add-members-modal.react.js +++ b/web/modals/threads/members/add-members-modal.react.js @@ -1,202 +1,202 @@ // @flow import * as React from 'react'; import { changeThreadSettingsActionTypes, changeThreadSettings, } from 'lib/actions/thread-actions.js'; import { useENSNames } from 'lib/hooks/ens-cache.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { userSearchIndexForPotentialMembers, userInfoSelectorForPotentialMembers, } from 'lib/selectors/user-selectors.js'; import { getPotentialMemberItems } from 'lib/shared/search-utils.js'; import { threadActualMembers } from 'lib/shared/thread-utils.js'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-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 Label from '../../../components/label.react.js'; import { useSelector } from '../../../redux/redux-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 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 userSearchResultsWithENSNames = useENSNames(userSearchResults); 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 pendingUserInfos = React.useMemo( () => Array.from(pendingUsersToAdd) .map(userID => ({ id: userID, username: otherUserInfos[userID].username, })) .sort((a, b) => a.username.localeCompare(b.username)), [otherUserInfos, pendingUsersToAdd], ); const pendingUserInfosWithENSNames = useENSNames(pendingUserInfos); const labelItems = React.useMemo(() => { if (!pendingUserInfosWithENSNames.length) { return null; } return (
{pendingUserInfosWithENSNames.map(userInfo => ( ))}
); }, [onSwitchUser, pendingUserInfosWithENSNames]); 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; +export { AddMembersModal, AddMembersModalContent }; diff --git a/web/modals/threads/members/members-modal.css b/web/modals/threads/members/members-modal.css index fd82f3cae..50ee4d7de 100644 --- a/web/modals/threads/members/members-modal.css +++ b/web/modals/threads/members/members-modal.css @@ -1,93 +1,91 @@ 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; } diff --git a/web/modals/threads/members/members-modal.react.js b/web/modals/threads/members/members-modal.react.js index 4a36e52d5..16d9fb014 100644 --- a/web/modals/threads/members/members-modal.react.js +++ b/web/modals/threads/members/members-modal.react.js @@ -1,141 +1,141 @@ // @flow import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { userStoreSearchIndex } from 'lib/selectors/user-selectors.js'; import { memberHasAdminPowers, memberIsAdmin, threadHasPermission, } from 'lib/shared/thread-utils.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { type RelativeMemberInfo } from 'lib/types/thread-types.js'; -import AddMembersModal from './add-members-modal.react.js'; +import { AddMembersModal } 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 from '../../../components/tabs.react.js'; import { useSelector } from '../../../redux/redux-utils.js'; import SearchModal from '../../search-modal.react.js'; type ContentProps = { +searchText: string, +threadID: string, }; function ThreadMembersModalContent(props: ContentProps): React.Node { const { threadID, searchText } = props; const [tab, setTab] = React.useState<'All Members' | 'Admins'>('All Members'); const threadInfo = useSelector(state => threadInfoSelector(state)[threadID]); const { members: threadMembersNotFiltered } = threadInfo; const userSearchIndex = useSelector(userStoreSearchIndex); 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 adminMembers = React.useMemo( () => allMembers.filter( (member: RelativeMemberInfo) => memberIsAdmin(member, threadInfo) || memberHasAdminPowers(member), ), [allMembers, threadInfo], ); const allUsersTab = React.useMemo( () => ( ), [allMembers, threadInfo], ); const allAdminsTab = React.useMemo( () => ( ), [adminMembers, threadInfo], ); const { pushModal, popModal } = useModalContext(); const onClickAddMembers = React.useCallback(() => { pushModal(); }, [popModal, pushModal, threadID]); const canAddMembers = threadHasPermission( threadInfo, threadPermissions.ADD_MEMBERS, ); const addMembersButton = React.useMemo(() => { if (!canAddMembers) { return null; } return (
); }, [canAddMembers, onClickAddMembers]); return (
{allUsersTab} {allAdminsTab}
{addMembersButton}
); } 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/sidebar/community-creation/community-creation-members-modal.css b/web/sidebar/community-creation/community-creation-members-modal.css new file mode 100644 index 000000000..3bf7e5b38 --- /dev/null +++ b/web/sidebar/community-creation/community-creation-members-modal.css @@ -0,0 +1,36 @@ +.container { + display: flex; + flex-direction: column; + overflow: hidden; + margin: 16px; +} + +div.ancestryContainer { + display: flex; + justify-content: center; + align-items: center; + height: 40px; + background-color: var(--community-creation-ancestry-bg); + margin-top: 20px; +} + +div.ancestryContainer p { + color: var(--community-creation-ancestry-text); +} + +.keyserverContainer { + display: flex; + justify-content: center; + align-items: center; + padding: 5px 10px; + margin: 0 6px; + border-radius: 8px; + background-color: var(--community-creation-keyserver-container); +} + +.keyserverName { + padding-left: 6px; + color: var(--thread-ancestor-color); + font-size: var(--s-font-14); + font-weight: var(--semi-bold); +} diff --git a/web/sidebar/community-creation/community-creation-members-modal.react.js b/web/sidebar/community-creation/community-creation-members-modal.react.js new file mode 100644 index 000000000..30be4dbc1 --- /dev/null +++ b/web/sidebar/community-creation/community-creation-members-modal.react.js @@ -0,0 +1,49 @@ +// @flow + +import * as React from 'react'; + +import css from './community-creation-members-modal.css'; +import CommIcon from '../../CommIcon.react.js'; +import Search from '../../components/search.react.js'; +import Modal from '../../modals/modal.react.js'; +import { AddMembersModalContent } from '../../modals/threads/members/add-members-modal.react.js'; + +type Props = { + +threadID: string, + +onClose: () => void, +}; +function CommunityCreationMembersModal(props: Props): React.Node { + const { threadID, onClose } = props; + const [searchText, setSearchText] = React.useState(''); + + return ( + +
+

within

+
+ +
ashoat
+
+
+
+ + +
+
+ ); +} + +export default CommunityCreationMembersModal; diff --git a/web/sidebar/community-creation/community-creation-modal.react.js b/web/sidebar/community-creation/community-creation-modal.react.js index 9e8440775..6c234d113 100644 --- a/web/sidebar/community-creation/community-creation-modal.react.js +++ b/web/sidebar/community-creation/community-creation-modal.react.js @@ -1,206 +1,213 @@ // @flow import * as React from 'react'; import { useDispatch } from 'react-redux'; import { newThread, newThreadActionTypes } from 'lib/actions/thread-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import type { NewThreadResult } from 'lib/types/thread-types.js'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils.js'; +import CommunityCreationMembersModal from './community-creation-members-modal.react.js'; import css from './community-creation-modal.css'; import CommIcon from '../../CommIcon.react.js'; import Button, { buttonThemes } from '../../components/button.react.js'; import EnumSettingsOption from '../../components/enum-settings-option.react.js'; import UserAvatar from '../../components/user-avatar.react.js'; import LoadingIndicator from '../../loading-indicator.react.js'; import Input from '../../modals/input.react.js'; import Modal from '../../modals/modal.react.js'; import { updateNavInfoActionType } from '../../redux/action-types.js'; import { useSelector } from '../../redux/redux-utils.js'; import { nonThreadCalendarQuery } from '../../selectors/nav-selectors.js'; const announcementStatements = [ { statement: `This option sets the community's root channel to an ` + `announcement channel. Only admins and other admin-appointed ` + `roles can send messages in an announcement channel.`, isStatementValid: true, styleStatementBasedOnValidity: false, }, ]; const createNewCommunityLoadingStatusSelector = createLoadingStatusSelector(newThreadActionTypes); function CommunityCreationModal(): React.Node { const modalContext = useModalContext(); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const callNewThread = useServerCall(newThread); const calendarQueryFunc = useSelector(nonThreadCalendarQuery); const [errorMessage, setErrorMessage] = React.useState(); const [pendingCommunityName, setPendingCommunityName] = React.useState(''); const onChangePendingCommunityName = React.useCallback( (event: SyntheticEvent) => { setErrorMessage(); setPendingCommunityName(event.currentTarget.value); }, [], ); const [announcementSetting, setAnnouncementSetting] = React.useState(false); const onAnnouncementSelected = React.useCallback(() => { setErrorMessage(); setAnnouncementSetting(!announcementSetting); }, [announcementSetting]); const callCreateNewCommunity = React.useCallback(async () => { const calendarQuery = calendarQueryFunc(); try { const newThreadResult: NewThreadResult = await callNewThread({ name: pendingCommunityName, type: announcementSetting ? threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT : threadTypes.COMMUNITY_ROOT, calendarQuery, }); return newThreadResult; } catch (e) { setErrorMessage('Community creation failed. Please try again.'); throw e; } }, [ announcementSetting, calendarQueryFunc, callNewThread, pendingCommunityName, ]); const createNewCommunity = React.useCallback(async () => { setErrorMessage(); const newThreadResultPromise = callCreateNewCommunity(); dispatchActionPromise(newThreadActionTypes, newThreadResultPromise); const newThreadResult: NewThreadResult = await newThreadResultPromise; const { newThreadID } = newThreadResult; await dispatch({ type: updateNavInfoActionType, payload: { activeChatThreadID: newThreadID, }, }); modalContext.popModal(); + modalContext.pushModal( + , + ); }, [callCreateNewCommunity, dispatch, dispatchActionPromise, modalContext]); const megaphoneIcon = React.useMemo( () => , [], ); const avatarNodeEnabled = false; let avatarNode; if (avatarNodeEnabled) { avatarNode = (
); } const createNewCommunityLoadingStatus: LoadingStatus = useSelector( createNewCommunityLoadingStatusSelector, ); let buttonContent; if (createNewCommunityLoadingStatus === 'loading') { buttonContent = ( ); } else if (errorMessage) { buttonContent = errorMessage; } else { buttonContent = 'Create community'; } return (

within

ashoat
{avatarNode}
Community Name
You may edit your community's image and name later.

Optional settings
); } export default CommunityCreationModal;