Changeset View
Changeset View
Standalone View
Standalone View
web/modals/threads/create/compose-subchannel-modal.react.js
// @flow | // @flow | ||||
import * as React from 'react'; | 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 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 { trimText } from 'lib/utils/text-utils'; | ||||
import Stepper from '../../../components/stepper.react'; | import Stepper from '../../../components/stepper.react'; | ||||
import { updateNavInfoActionType } from '../../../redux/action-types'; | |||||
import { nonThreadCalendarQuery } from '../../../selectors/nav-selectors'; | |||||
import Modal from '../../modal.react'; | import Modal from '../../modal.react'; | ||||
import css from './compose-subchannel-modal.css'; | import css from './compose-subchannel-modal.css'; | ||||
import SubchannelMembers from './steps/subchannel-members.react'; | import SubchannelMembers from './steps/subchannel-members.react'; | ||||
import SubchannelSettings from './steps/subchannel-settings.react'; | import SubchannelSettings from './steps/subchannel-settings.react'; | ||||
import type { VisibilityType } from './steps/subchannel-settings.react'; | import type { VisibilityType } from './steps/subchannel-settings.react'; | ||||
type Props = { | type Props = { | ||||
+onClose: () => void, | +onClose: () => void, | ||||
+parentThreadInfo: ThreadInfo, | +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 Steps = 'settings' | 'members'; | ||||
type HeaderProps = { | type HeaderProps = { | ||||
+parentThreadName: string, | +parentThreadName: string, | ||||
}; | }; | ||||
function ComposeSubchannelHeader(props: HeaderProps): React.Node { | function ComposeSubchannelHeader(props: HeaderProps): React.Node { | ||||
const { parentThreadName } = props; | const { parentThreadName } = props; | ||||
return ( | return ( | ||||
<div className={css.modalHeader}> | <div className={css.modalHeader}> | ||||
<div> | <div> | ||||
{'within '} | {'within '} | ||||
<div className={css.modalHeaderParentName}>{parentThreadName}</div> | <div className={css.modalHeaderParentName}>{parentThreadName}</div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
); | ); | ||||
} | } | ||||
const createSubchannelLoadingStatusSelector = createLoadingStatusSelector( | |||||
newThreadActionTypes, | |||||
); | |||||
function ComposeSubchannelModal(props: Props): React.Node { | function ComposeSubchannelModal(props: Props): React.Node { | ||||
const { parentThreadInfo, onClose } = props; | const { parentThreadInfo, onClose } = props; | ||||
const { uiName: parentThreadName } = parentThreadInfo; | const { uiName: parentThreadName } = parentThreadInfo; | ||||
const [activeStep, setActiveStep] = React.useState<Steps>('settings'); | const [activeStep, setActiveStep] = React.useState<Steps>('settings'); | ||||
const [channelName, setChannelName] = React.useState<string>(''); | const [channelName, setChannelName] = React.useState<string>(''); | ||||
const [visibilityType, setVisibilityType] = React.useState<VisibilityType>( | const [visibilityType, setVisibilityType] = React.useState<VisibilityType>( | ||||
'open', | 'open', | ||||
); | ); | ||||
const [announcement, setAnnouncement] = React.useState<boolean>(false); | const [announcement, setAnnouncement] = React.useState<boolean>(false); | ||||
const [selectedUsers, setSelectedUsers] = React.useState< | const [selectedUsers, setSelectedUsers] = React.useState< | ||||
$ReadOnlySet<string>, | $ReadOnlySet<string>, | ||||
>(new Set()); | >(new Set()); | ||||
const [searchUserText, setSearchUserText] = React.useState<string>(''); | const [searchUserText, setSearchUserText] = React.useState<string>(''); | ||||
const loadingState = useSelector(createSubchannelLoadingStatusSelector); | |||||
const [errorMessage, setErrorMessage] = React.useState<string>(''); | |||||
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( | const onChangeChannelName = React.useCallback( | ||||
(event: SyntheticEvent<HTMLInputElement>) => { | (event: SyntheticEvent<HTMLInputElement>) => { | ||||
const target = event.currentTarget; | const target = event.currentTarget; | ||||
setChannelName(target.value); | setChannelName(target.value); | ||||
}, | }, | ||||
[], | [], | ||||
); | ); | ||||
▲ Show 20 Lines • Show All 49 Lines • ▼ Show 20 Lines | function ComposeSubchannelModal(props: Props): React.Node { | ||||
const stepperButtons = React.useMemo( | const stepperButtons = React.useMemo( | ||||
() => ({ | () => ({ | ||||
settings: { | settings: { | ||||
nextProps: { | nextProps: { | ||||
content: 'Next', | content: 'Next', | ||||
disabled: !channelName.trim(), | disabled: !channelName.trim(), | ||||
onClick: () => { | onClick: () => { | ||||
setErrorMessage(''); | |||||
setChannelName(channelName.trim()); | setChannelName(channelName.trim()); | ||||
setActiveStep('members'); | setActiveStep('members'); | ||||
}, | }, | ||||
}, | }, | ||||
}, | }, | ||||
members: { | members: { | ||||
prevProps: { | prevProps: { | ||||
content: 'Back', | content: 'Back', | ||||
onClick: () => setActiveStep('settings'), | onClick: () => setActiveStep('settings'), | ||||
}, | }, | ||||
nextProps: { | nextProps: { | ||||
content: 'Create', | content: 'Create', | ||||
loading: loadingState === 'loading', | |||||
disabled: selectedUsers.size === 0, | |||||
onClick: () => { | onClick: () => { | ||||
// TODO: make form logic | dispatchCreateSubchannel(); | ||||
}, | }, | ||||
}, | }, | ||||
}, | }, | ||||
}), | }), | ||||
[channelName], | [channelName, dispatchCreateSubchannel, loadingState, selectedUsers], | ||||
); | ); | ||||
const subchannelMembers = React.useMemo( | const subchannelMembers = React.useMemo( | ||||
() => ( | () => ( | ||||
<SubchannelMembers | <SubchannelMembers | ||||
parentThreadInfo={parentThreadInfo} | parentThreadInfo={parentThreadInfo} | ||||
selectedUsers={selectedUsers} | selectedUsers={selectedUsers} | ||||
searchText={searchUserText} | searchText={searchUserText} | ||||
Show All 29 Lines | <Modal name={modalName} onClose={onClose} size="fit-content"> | ||||
name="settings" | name="settings" | ||||
nextProps={stepperButtons.settings.nextProps} | nextProps={stepperButtons.settings.nextProps} | ||||
/> | /> | ||||
<Stepper.Item | <Stepper.Item | ||||
content={subchannelMembers} | content={subchannelMembers} | ||||
name="members" | name="members" | ||||
prevProps={stepperButtons.members.prevProps} | prevProps={stepperButtons.members.prevProps} | ||||
nextProps={stepperButtons.members.nextProps} | nextProps={stepperButtons.members.nextProps} | ||||
errorMessage={errorMessage} | |||||
/> | /> | ||||
</Stepper.Container> | </Stepper.Container> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
</Modal> | </Modal> | ||||
); | ); | ||||
} | } | ||||
export default ComposeSubchannelModal; | export default ComposeSubchannelModal; |