diff --git a/web/chat/thread-menu.react.js b/web/chat/thread-menu.react.js
--- a/web/chat/thread-menu.react.js
+++ b/web/chat/thread-menu.react.js
@@ -28,6 +28,7 @@
import SidebarPromoteModal from '../modals/chat/sidebar-promote-modal.react';
import { useModalContext } from '../modals/modal-provider.react';
import ConfirmLeaveThreadModal from '../modals/threads/confirm-leave-thread-modal.react';
+import ComposeSubchannelModal from '../modals/threads/create/compose-subchannel-modal.react';
import ThreadMembersModal from '../modals/threads/members/members-modal.react';
import ThreadNotificationsModal from '../modals/threads/notifications/notifications-modal.react';
import ThreadSettingsModal from '../modals/threads/settings/thread-settings-modal.react';
@@ -144,6 +145,17 @@
);
}, [hasSubchannels, onClickViewSubchannels]);
+ const onClickCreateSubchannel = React.useCallback(
+ () =>
+ pushModal(
+ ,
+ ),
+ [popModal, pushModal, threadInfo],
+ );
+
const createSubchannelsItem = React.useMemo(() => {
if (!canCreateSubchannels) {
return null;
@@ -153,9 +165,10 @@
key="newSubchannel"
text="Create new subchannel"
icon="plus-circle"
+ onClick={onClickCreateSubchannel}
/>
);
- }, [canCreateSubchannels]);
+ }, [canCreateSubchannels, onClickCreateSubchannel]);
const dispatchActionPromise = useDispatchActionPromise();
const callLeaveThread = useServerCall(leaveThread);
@@ -245,16 +258,13 @@
const menuItems = React.useMemo(() => {
const separator =
;
- // TODO: Enable menu items when the modals are implemented
- const SHOW_CREATE_SUBCHANNELS = false;
-
const items = [
settingsItem,
notificationsItem,
membersItem,
sidebarItem,
viewSubchannelsItem,
- SHOW_CREATE_SUBCHANNELS && createSubchannelsItem,
+ createSubchannelsItem,
leaveThreadItem && separator,
canPromoteSidebar && promoteSidebar,
leaveThreadItem,
diff --git a/web/modals/threads/create/compose-subchannel-modal.css b/web/modals/threads/create/compose-subchannel-modal.css
new file mode 100644
--- /dev/null
+++ b/web/modals/threads/create/compose-subchannel-modal.css
@@ -0,0 +1,35 @@
+div.modalHeader {
+ padding: 15px;
+
+ font-weight: 500;
+
+ color: var(--shades-black-60);
+ background-color: var(--shades-black-80);
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ margin-top: 30px;
+}
+
+div.modalHeaderParentName {
+ color: var(--violet-light-100);
+ display: inline;
+}
+
+div.container {
+ width: 383px;
+ max-height: 100vh;
+ overflow-y: auto;
+}
+
+div.stepContainer {
+ position: relative;
+ height: 100vh;
+ max-height: min(533px, 100vh);
+}
+
+div.stepItem {
+ padding: 20px;
+}
diff --git a/web/modals/threads/create/compose-subchannel-modal.react.js b/web/modals/threads/create/compose-subchannel-modal.react.js
new file mode 100644
--- /dev/null
+++ b/web/modals/threads/create/compose-subchannel-modal.react.js
@@ -0,0 +1,208 @@
+// @flow
+import * as React from 'react';
+import { useState } from 'react';
+
+import type { ThreadInfo } from 'lib/types/thread-types';
+import { trimText } from 'lib/utils/text-utils';
+
+import Stepper from '../../../components/stepper.react';
+import Modal from '../../modal.react';
+import css from './compose-subchannel-modal.css';
+import SubchannelMembers from './steps/subchannel-members.react';
+import SubchannelSettings from './steps/subchannel-settings.react';
+
+type Props = {
+ +onClose: () => any,
+ +parentThreadInfo: ThreadInfo,
+};
+
+type Pages = 'settings' | 'members';
+type VisibilityType = 'open' | 'closed';
+
+type HeaderProps = {
+ +parentThreadName: string,
+};
+
+function ComposeSubchannelHeader(props: HeaderProps): React.Node {
+ const { parentThreadName } = props;
+ return (
+
+
+ {'within '}
+
{parentThreadName}
+
+
+ );
+}
+
+function ComposeSubchannelModal(props: Props): React.Node {
+ const { parentThreadInfo } = props;
+ const { uiName: parentThreadName } = parentThreadInfo;
+
+ const [activeStep, setActiveStep] = useState('settings');
+
+ const [channelName, setChannelName] = useState('');
+ const [visibilityType, setVisibilityType] = useState('open');
+ const [announcement, setAnnouncement] = useState(false);
+ const [selectedUsers, setSelectedUsers] = useState>(new Set());
+ const [searchUserText, setSearchUserText] = useState('');
+
+ const handleChanges = React.useCallback(
+ (
+ event: SyntheticEvent,
+ setValue: (newValue: any) => any,
+ ) => {
+ const target = event.currentTarget;
+ setValue(target.value);
+ },
+ [],
+ );
+
+ const onChangeChannelName = React.useCallback(
+ (event: SyntheticEvent) => {
+ handleChanges(event, setChannelName);
+ },
+ [handleChanges],
+ );
+
+ const onOpenVisibilityTypeSelected = React.useCallback(
+ () => setVisibilityType('open'),
+ [],
+ );
+
+ const onClosedVisibilityTypeSelected = React.useCallback(
+ () => setVisibilityType('closed'),
+ [],
+ );
+
+ const onAnnouncementSelected = React.useCallback(
+ () => setAnnouncement(!announcement),
+ [announcement],
+ );
+
+ const switchUser = React.useCallback((userID: string) => {
+ setSelectedUsers((users: Set) => {
+ const newUsers = new Set(users);
+ if (newUsers.has(userID)) {
+ newUsers.delete(userID);
+ } else {
+ newUsers.add(userID);
+ }
+ return newUsers;
+ });
+ }, []);
+
+ const steps = [];
+
+ const subchannelSettings = React.useMemo(
+ () => (
+
+ ),
+ [
+ channelName,
+ visibilityType,
+ announcement,
+ onChangeChannelName,
+ onOpenVisibilityTypeSelected,
+ onClosedVisibilityTypeSelected,
+ onAnnouncementSelected,
+ ],
+ );
+
+ const stepperButtons = React.useMemo(
+ () => ({
+ settings: {
+ nextProps: {
+ content: 'Next',
+ disabled: !channelName.trim(),
+ onClick: () => {
+ setChannelName(channelName.trim());
+ setActiveStep('members');
+ },
+ },
+ },
+ members: {
+ prevProps: {
+ content: 'Back',
+ onClick: () => setActiveStep('settings'),
+ },
+ nextProps: {
+ content: 'Create',
+ onClick: () => {
+ /// TODO: make form logic
+ },
+ },
+ },
+ }),
+ [channelName],
+ );
+
+ const subchannelMembers = React.useMemo(
+ () => (
+
+ ),
+ [
+ selectedUsers,
+ switchUser,
+ parentThreadInfo,
+ searchUserText,
+ setSearchUserText,
+ ],
+ );
+
+ steps.push(
+ ,
+ );
+ steps.push(
+ ,
+ );
+
+ const modalName = React.useMemo(() => {
+ if (activeStep !== 'settings') {
+ return `Create channel - ${trimText(channelName || '', 11)}`;
+ } else {
+ return 'Create channel';
+ }
+ }, [activeStep, channelName]);
+
+ return (
+
+
+
+
+ );
+}
+
+export default ComposeSubchannelModal;
diff --git a/web/modals/threads/create/steps/subchannel-members-list.react.js b/web/modals/threads/create/steps/subchannel-members-list.react.js
new file mode 100644
--- /dev/null
+++ b/web/modals/threads/create/steps/subchannel-members-list.react.js
@@ -0,0 +1,86 @@
+// @flow
+
+import * as React from 'React';
+import { useSelector } from 'react-redux';
+
+import type { ThreadInfo } from 'lib/types/thread-types';
+import { trimText } from 'lib/utils/text-utils';
+
+import AddMembersList from '../../../components/add-members-list.react';
+
+type Props = {
+ +searchText: string,
+ +searchResult: $ReadOnlySet,
+ +communityThreadInfo: ThreadInfo,
+ +parentThreadInfo: ThreadInfo,
+ +selectedUsers: $ReadOnlySet,
+ +switchUser: (userID: string) => void,
+};
+
+function Memberlist(props: Props): React.Node {
+ const {
+ searchText,
+ searchResult,
+ communityThreadInfo,
+ parentThreadInfo,
+ selectedUsers,
+ switchUser,
+ } = props;
+
+ const {
+ members: communityMembers,
+ name: communityName,
+ } = communityThreadInfo;
+
+ const currentUserId = useSelector(state => state.currentUserInfo.id);
+
+ const parentMembers = React.useMemo(
+ () => new Set(parentThreadInfo.members.map(user => user.id)),
+ [parentThreadInfo],
+ );
+
+ const parentMemberList = React.useMemo(
+ () =>
+ communityMembers.filter(
+ user =>
+ parentMembers.has(user.id) &&
+ user.id !== currentUserId &&
+ (searchResult.has(user.id) || searchText.length === 0),
+ ),
+ [communityMembers, parentMembers, currentUserId, searchResult, searchText],
+ );
+
+ const otherMemberList = React.useMemo(
+ () =>
+ communityMembers.filter(
+ user =>
+ !parentMembers.has(user.id) &&
+ (searchResult.has(user.id) || searchText.length === 0),
+ ),
+ [communityMembers, parentMembers, searchResult, searchText],
+ );
+
+ const sortedGroupedUserList = React.useMemo(
+ () =>
+ [
+ { header: 'Users in parent channel', userInfos: parentMemberList },
+ {
+ header: `All users in ${
+ trimText(communityName || '', 22) ?? ''
+ }`,
+ userInfos: otherMemberList,
+ },
+ ].filter(item => item.userInfos.length),
+ [parentMemberList, otherMemberList, communityName],
+ );
+
+ return (
+
+ );
+}
+
+export default Memberlist;
diff --git a/web/modals/threads/create/steps/subchannel-members.css b/web/modals/threads/create/steps/subchannel-members.css
new file mode 100644
--- /dev/null
+++ b/web/modals/threads/create/steps/subchannel-members.css
@@ -0,0 +1,10 @@
+.members {
+ overflow-y: auto;
+}
+
+.searchBar {
+ background-color: var(--modal-bg);
+ position: sticky;
+ padding: 2.5px 0;
+ top: 0;
+}
diff --git a/web/modals/threads/create/steps/subchannel-members.react.js b/web/modals/threads/create/steps/subchannel-members.react.js
new file mode 100644
--- /dev/null
+++ b/web/modals/threads/create/steps/subchannel-members.react.js
@@ -0,0 +1,68 @@
+// @flow
+
+import * as React from 'react';
+import { useSelector } from 'react-redux';
+
+import { threadInfoSelector } from 'lib/selectors/thread-selectors';
+import { userStoreSearchIndex } from 'lib/selectors/user-selectors';
+import { useAncestorThreads } from 'lib/shared/ancestor-threads';
+import type { ThreadInfo } from 'lib/types/thread-types';
+
+import Search from '../../../../components/search.react';
+import MembersList from './subchannel-members-list.react';
+import css from './subchannel-members.css';
+
+type SubchannelMembersProps = {
+ +parentThreadInfo: ThreadInfo,
+ +selectedUsers: $ReadOnlySet,
+ +searchText: string,
+ +setSearchText: string => void,
+ +switchUser: (userID: string) => void,
+};
+
+function SubchannelMembers(props: SubchannelMembersProps): React.Node {
+ const {
+ switchUser,
+ searchText,
+ setSearchText,
+ parentThreadInfo,
+ selectedUsers,
+ } = props;
+
+ const ancestorThreads = useAncestorThreads(parentThreadInfo);
+ const { id: threadID } = ancestorThreads[0] ?? parentThreadInfo;
+
+ const communityThread = useSelector(
+ state => threadInfoSelector(state)[threadID],
+ );
+
+ const userSearchIndex = useSelector(userStoreSearchIndex);
+ const searchResult = React.useMemo(
+ () => new Set(userSearchIndex.getSearchResults(searchText)),
+ [userSearchIndex, searchText],
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+}
+
+export default SubchannelMembers;
diff --git a/web/modals/threads/create/steps/subchannel-settings.css b/web/modals/threads/create/steps/subchannel-settings.css
new file mode 100644
--- /dev/null
+++ b/web/modals/threads/create/steps/subchannel-settings.css
@@ -0,0 +1,3 @@
+.wrapper {
+ color: var(--violet-dark-100);
+}
diff --git a/web/modals/threads/create/steps/subchannel-settings.react.js b/web/modals/threads/create/steps/subchannel-settings.react.js
new file mode 100644
--- /dev/null
+++ b/web/modals/threads/create/steps/subchannel-settings.react.js
@@ -0,0 +1,122 @@
+// @flow
+import * as React from 'react';
+
+import { threadTypeDescriptions } from 'lib/shared/thread-utils';
+import { threadTypes } from 'lib/types/thread-types';
+
+import EnumSettingsOption from '../../../../components/enum-settings-option.react';
+import ModalLabel from '../../../../components/modal-label.react';
+import SWMansionIcon from '../../../../SWMansionIcon.react';
+import Input from '../../../input.react';
+import css from './subchannel-settings.css';
+
+const { COMMUNITY_OPEN_SUBTHREAD, COMMUNITY_SECRET_SUBTHREAD } = threadTypes;
+
+type Visibilities = 'open' | 'closed';
+
+type Props = {
+ +channelName: string,
+ +onChangeChannelName: (SyntheticEvent) => void,
+ +visibilityType: Visibilities,
+ +onOpenTypeSelect: () => void,
+ +onClosedTypeSelect: () => void,
+ +announcement: boolean,
+ +onAnnouncementSelected: () => void,
+};
+
+const openStatements = [
+ {
+ statement: threadTypeDescriptions[COMMUNITY_OPEN_SUBTHREAD],
+ isStatementValid: true,
+ styleStatementBasedOnValidity: false,
+ },
+];
+
+const secretStatements = [
+ {
+ statement: threadTypeDescriptions[COMMUNITY_SECRET_SUBTHREAD],
+ isStatementValid: true,
+ styleStatementBasedOnValidity: false,
+ },
+];
+
+const announcementStatements = [
+ {
+ statement: 'Admins can create Announcement channels.',
+ isStatementValid: true,
+ styleStatementBasedOnValidity: false,
+ },
+];
+
+function SubchannelSettings(props: Props): React.Node {
+ const {
+ channelName,
+ onChangeChannelName,
+ visibilityType,
+ onOpenTypeSelect,
+ onClosedTypeSelect,
+ announcement,
+ onAnnouncementSelected,
+ } = props;
+
+ const globeIcon = React.useMemo(
+ () => ,
+ [],
+ );
+
+ const lockIcon = React.useMemo(
+ () => ,
+ [],
+ );
+
+ const flagIcon = React.useMemo(
+ () => ,
+ [],
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default SubchannelSettings;