diff --git a/web/modals/threads/create/compose-subchannel-modal.react.js b/web/modals/threads/create/compose-subchannel-modal.react.js index e0dae6554..0bc2cf5f5 100644 --- a/web/modals/threads/create/compose-subchannel-modal.react.js +++ b/web/modals/threads/create/compose-subchannel-modal.react.js @@ -1,190 +1,284 @@ // @flow import * as React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { newThread, newThreadActionTypes } from 'lib/actions/thread-actions'; +import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import type { ThreadInfo } from 'lib/types/thread-types'; +import { threadTypes } from 'lib/types/thread-types'; +import { + useDispatchActionPromise, + useServerCall, +} from 'lib/utils/action-utils'; import { trimText } from 'lib/utils/text-utils'; import Stepper from '../../../components/stepper.react'; +import { updateNavInfoActionType } from '../../../redux/action-types'; +import { nonThreadCalendarQuery } from '../../../selectors/nav-selectors'; 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'; import type { VisibilityType } from './steps/subchannel-settings.react'; 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 } = parentThreadInfo; 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 = useServerCall(newThread); + + 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), + calendarQuery: query, + color: parentThreadInfo.color, + }); + + return result; + } catch (e) { + await setErrorMessage( + e.message === 'invalid_parameters' && announcement + ? 'announcement channels currently not available' + : 'unknown error', + ); + + return null; + } + }, [ + parentThreadInfo, + selectedUsers, + visibilityType, + announcement, + callNewThread, + calendarQuery, + channelName, + ]); + + 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, onClick: () => { - // TODO: make form logic + dispatchCreateSubchannel(); }, }, }, }), - [channelName], + [channelName, dispatchCreateSubchannel, loadingState, selectedUsers], ); const subchannelMembers = React.useMemo( () => ( ), [ selectedUsers, toggleUserSelection, parentThreadInfo, searchUserText, setSearchUserText, ], ); const modalName = activeStep === 'members' ? `Create channel - ${trimText(channelName, 11)}` : 'Create channel'; return (
); } export default ComposeSubchannelModal;