diff --git a/web/modals/threads/new-thread-modal.react.js b/web/modals/threads/new-thread-modal.react.js index 06fbeb432..ddd943cc1 100644 --- a/web/modals/threads/new-thread-modal.react.js +++ b/web/modals/threads/new-thread-modal.react.js @@ -1,303 +1,304 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { newThreadActionTypes, newThread } from 'lib/actions/thread-actions'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { threadInfoSelector } from 'lib/selectors/thread-selectors'; import { generateRandomColor, threadTypeDescriptions, } from 'lib/shared/thread-utils'; import { type ThreadInfo, threadTypes, assertThreadType, type ThreadType, type NewThreadRequest, type NewThreadResult, } from 'lib/types/thread-types'; import { type DispatchActionPromise, useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; +import { firstLine } from 'lib/utils/string-utils'; import { useSelector } from '../../redux/redux-utils'; import css from '../../style.css'; import Modal from '../modal.react'; import ColorPicker from './color-picker.react'; type BaseProps = {| +onClose: () => void, +parentThreadID?: ?string, |}; type Props = {| ...BaseProps, +inputDisabled: boolean, +parentThreadInfo: ?ThreadInfo, +dispatchActionPromise: DispatchActionPromise, +newThread: (request: NewThreadRequest) => Promise, |}; type State = {| +threadType: ?ThreadType, +name: string, +description: string, +color: string, +errorMessage: string, |}; class NewThreadModal extends React.PureComponent { nameInput: ?HTMLInputElement; openPrivacyInput: ?HTMLInputElement; threadPasswordInput: ?HTMLInputElement; constructor(props: Props) { super(props); this.state = { threadType: props.parentThreadID ? undefined : threadTypes.CHAT_SECRET, name: '', description: '', color: props.parentThreadInfo ? props.parentThreadInfo.color : generateRandomColor(), errorMessage: '', }; } componentDidMount() { invariant(this.nameInput, 'nameInput ref unset'); this.nameInput.focus(); } render() { let threadTypeSection = null; if (this.props.parentThreadID) { threadTypeSection = (
Thread type
); } return (
Thread name
Description
Color
); } else if (this.state.currentTabType === 'privacy') { mainContent = (
Thread type
); } else if (this.state.currentTabType === 'delete') { mainContent = ( <>

Your thread will be permanently deleted. There is no way to reverse this.

Please enter your account password to confirm your identity

Account password
); } let buttons = null; if (this.state.currentTabType === 'delete') { buttons = ( ); } else { buttons = ( ); } const tabs = [ , ]; if (this.possiblyChangedValue('parentThreadID')) { tabs.push( , ); } const canDeleteThread = threadHasPermission( this.props.threadInfo, threadPermissions.DELETE_THREAD, ); if (canDeleteThread) { tabs.push( , ); } return (
    {tabs}
{mainContent}
{buttons}
{this.state.errorMessage}
); } setTab = (tabType: TabType) => { this.setState({ currentTabType: tabType }); }; nameInputRef = (nameInput: ?HTMLInputElement) => { this.nameInput = nameInput; }; newThreadPasswordInputRef = (newThreadPasswordInput: ?HTMLInputElement) => { this.newThreadPasswordInput = newThreadPasswordInput; }; accountPasswordInputRef = (accountPasswordInput: ?HTMLInputElement) => { this.accountPasswordInput = accountPasswordInput; }; onChangeName = (event: SyntheticEvent) => { const target = event.currentTarget; const newValue = target.value !== this.props.threadInfo.name ? target.value : undefined; this.setState((prevState: State) => ({ ...prevState, queuedChanges: { ...prevState.queuedChanges, - name: newValue, + name: firstLine(newValue), }, })); }; onChangeDescription = (event: SyntheticEvent) => { const target = event.currentTarget; const newValue = target.value !== this.props.threadInfo.description ? target.value : undefined; this.setState((prevState: State) => ({ ...prevState, queuedChanges: { ...prevState.queuedChanges, description: newValue, }, })); }; onChangeColor = (color: string) => { const newValue = color !== this.props.threadInfo.color ? color : undefined; this.setState((prevState: State) => ({ ...prevState, queuedChanges: { ...prevState.queuedChanges, color: newValue, }, })); }; onChangeThreadType = (event: SyntheticEvent) => { const uiValue = assertThreadType(parseInt(event.currentTarget.value, 10)); const newValue = uiValue !== this.props.threadInfo.type ? uiValue : undefined; this.setState((prevState: State) => ({ ...prevState, queuedChanges: { ...prevState.queuedChanges, type: newValue, }, })); }; onChangeAccountPassword = (event: SyntheticEvent) => { const target = event.currentTarget; this.setState({ accountPassword: target.value }); }; onSubmit = (event: SyntheticEvent) => { event.preventDefault(); this.props.dispatchActionPromise( changeThreadSettingsActionTypes, this.changeThreadSettingsAction(), ); }; async changeThreadSettingsAction() { try { const response = await this.props.changeThreadSettings({ threadID: this.props.threadInfo.id, changes: this.state.queuedChanges, }); this.props.onClose(); return response; } catch (e) { this.setState( (prevState) => ({ ...prevState, queuedChanges: Object.freeze({}), accountPassword: '', errorMessage: 'unknown error', currentTabType: 'general', }), () => { invariant(this.nameInput, 'nameInput ref unset'); this.nameInput.focus(); }, ); throw e; } } onDelete = (event: SyntheticEvent) => { event.preventDefault(); this.props.dispatchActionPromise( deleteThreadActionTypes, this.deleteThreadAction(), ); }; async deleteThreadAction() { try { const response = await this.props.deleteThread( this.props.threadInfo.id, this.state.accountPassword, ); this.props.onClose(); return response; } catch (e) { const errorMessage = e.message === 'invalid_credentials' ? 'wrong password' : 'unknown error'; this.setState( { accountPassword: '', errorMessage: errorMessage, }, () => { invariant( this.accountPasswordInput, 'accountPasswordInput ref unset', ); this.accountPasswordInput.focus(); }, ); throw e; } } } ThreadSettingsModal.propTypes = { threadInfo: threadInfoPropType.isRequired, onClose: PropTypes.func.isRequired, inputDisabled: PropTypes.bool.isRequired, viewerID: PropTypes.string, userInfos: PropTypes.objectOf(userInfoPropType).isRequired, dispatchActionPromise: PropTypes.func.isRequired, deleteThread: PropTypes.func.isRequired, changeThreadSettings: PropTypes.func.isRequired, }; const deleteThreadLoadingStatusSelector = createLoadingStatusSelector( deleteThreadActionTypes, ); const changeThreadSettingsLoadingStatusSelector = createLoadingStatusSelector( changeThreadSettingsActionTypes, ); export default React.memo(function ConnectedThreadSettingsModal( props: BaseProps, ) { const inputDisabled = useSelector( (state) => deleteThreadLoadingStatusSelector(state) === 'loading' || changeThreadSettingsLoadingStatusSelector(state) === 'loading', ); const viewerID = useSelector( (state) => state.currentUserInfo && state.currentUserInfo.id, ); const userInfos = useSelector((state) => state.userStore.userInfos); const callDeleteThread = useServerCall(deleteThread); const callChangeThreadSettings = useServerCall(changeThreadSettings); const dispatchActionPromise = useDispatchActionPromise(); return ( ); });