diff --git a/web/modals/threads/thread-settings-delete-tab.react.js b/web/modals/threads/thread-settings-delete-tab.react.js index 041e56171..98f219386 100644 --- a/web/modals/threads/thread-settings-delete-tab.react.js +++ b/web/modals/threads/thread-settings-delete-tab.react.js @@ -1,44 +1,108 @@ // @flow import * as React from 'react'; +import { + deleteThreadActionTypes, + deleteThread, +} from 'lib/actions/thread-actions'; +import { type SetState } from 'lib/types/hook-types.js'; +import { type ThreadInfo } from 'lib/types/thread-types'; +import { + useDispatchActionPromise, + useServerCall, +} from 'lib/utils/action-utils'; + +import Button from '../../components/button.react.js'; +import { useModalContext } from '../modal-provider.react.js'; import css from './thread-settings-modal.css'; type ThreadSettingsDeleteTabProps = { +accountPassword: string, + +setAccountPassword: SetState, +onChangeAccountPassword: (event: SyntheticEvent) => void, +inputDisabled: boolean, + +threadInfo: ThreadInfo, + +setErrorMessage: SetState, }; function ThreadSettingsDeleteTab( props: ThreadSettingsDeleteTabProps, ): React.Node { - const { accountPassword, onChangeAccountPassword, inputDisabled } = props; + const { + accountPassword, + setAccountPassword, + onChangeAccountPassword, + inputDisabled, + threadInfo, + setErrorMessage, + } = props; + + const modalContext = useModalContext(); + const dispatchActionPromise = useDispatchActionPromise(); + const callDeleteThread = useServerCall(deleteThread); + + const deleteThreadAction = React.useCallback(async () => { + try { + const response = await callDeleteThread(threadInfo.id, accountPassword); + modalContext.popModal(); + return response; + } catch (e) { + setErrorMessage( + e.message === 'invalid_credentials' + ? 'wrong password' + : 'unknown error', + ); + setAccountPassword(''); + // TODO: accountPasswordInput.focus() + // (once ref is moved up to functional component) + throw e; + } + }, [ + accountPassword, + callDeleteThread, + modalContext, + setAccountPassword, + setErrorMessage, + threadInfo.id, + ]); + + const onDelete = React.useCallback( + (event: SyntheticEvent) => { + event.preventDefault(); + dispatchActionPromise(deleteThreadActionTypes, deleteThreadAction()); + }, + [deleteThreadAction, dispatchActionPromise], + ); + return ( <>

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

Please enter your account password to confirm your identity

Account password
+ ); } export default ThreadSettingsDeleteTab; diff --git a/web/modals/threads/thread-settings-modal.react.js b/web/modals/threads/thread-settings-modal.react.js index 6a712857d..a7f070382 100644 --- a/web/modals/threads/thread-settings-modal.react.js +++ b/web/modals/threads/thread-settings-modal.react.js @@ -1,333 +1,301 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import * as React from 'react'; import { deleteThreadActionTypes, - deleteThread, changeThreadSettingsActionTypes, changeThreadSettings, } from 'lib/actions/thread-actions'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { threadInfoSelector } from 'lib/selectors/thread-selectors'; import { threadHasPermission, robotextName } from 'lib/shared/thread-utils'; import { type ThreadInfo, threadTypes, threadPermissions, type ThreadChanges, } from 'lib/types/thread-types'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; import Button from '../../components/button.react'; import { useModalContext } from '../../modals/modal-provider.react'; import { useSelector } from '../../redux/redux-utils'; import Modal from '../modal.react'; import ThreadSettingsDeleteTab from './thread-settings-delete-tab.react'; import ThreadSettingsGeneralTab from './thread-settings-general-tab.react'; import css from './thread-settings-modal.css'; import ThreadSettingsPrivacyTab from './thread-settings-privacy-tab.react'; type TabType = 'general' | 'privacy' | 'delete'; type TabProps = { +name: string, +tabType: TabType, +selected: boolean, +onClick: (tabType: TabType) => void, }; class Tab extends React.PureComponent { render() { const classNamesForTab = classNames({ [css['current-tab']]: this.props.selected, [css['delete-tab']]: this.props.selected && this.props.tabType === 'delete', }); return (
  • {this.props.name}
  • ); } onClick = () => { return this.props.onClick(this.props.tabType); }; } type BaseProps = { +threadID: string, }; const deleteThreadLoadingStatusSelector = createLoadingStatusSelector( deleteThreadActionTypes, ); const changeThreadSettingsLoadingStatusSelector = createLoadingStatusSelector( changeThreadSettingsActionTypes, ); const ConnectedThreadSettingsModal: React.ComponentType = React.memo( function ConnectedThreadSettingsModal(props) { const changeInProgress = 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(); const threadInfo: ?ThreadInfo = useSelector( state => threadInfoSelector(state)[props.threadID], ); const modalContext = useModalContext(); const [errorMessage, setErrorMessage] = React.useState(''); const [accountPassword, setAccountPassword] = React.useState(''); const [currentTabType, setCurrentTabType] = React.useState( 'general', ); const [queuedChanges, setQueuedChanges] = React.useState( Object.freeze({}), ); const namePlaceholder: string = React.useMemo(() => { invariant(threadInfo, 'threadInfo should exist in namePlaceholder'); return robotextName(threadInfo, viewerID, userInfos); }, [threadInfo, userInfos, viewerID]); const changeQueued: boolean = React.useMemo( () => Object.values(queuedChanges).some(v => v !== null && v !== undefined), [queuedChanges], ); const onChangeAccountPassword = React.useCallback( (event: SyntheticEvent) => { const target = event.currentTarget; setAccountPassword(target.value); }, [], ); const hasPermissionForTab = React.useCallback( (thread: ThreadInfo, tab: TabType) => { if (tab === 'general') { return threadHasPermission( thread, threadPermissions.EDIT_THREAD_NAME, ); } else if (tab === 'privacy') { return threadHasPermission( thread, threadPermissions.EDIT_PERMISSIONS, ); } else if (tab === 'delete') { return threadHasPermission(thread, threadPermissions.DELETE_THREAD); } invariant(false, `invalid tab: ${tab}`); }, [], ); - const deleteThreadAction = React.useCallback(async () => { - invariant(threadInfo, 'threadInfo should exist in deleteThreadAction'); - try { - const response = await callDeleteThread(threadInfo.id, accountPassword); - modalContext.popModal(); - return response; - } catch (e) { - setErrorMessage( - e.message === 'invalid_credentials' - ? 'wrong password' - : 'unknown error', - ); - setAccountPassword(''); - // TODO: accountPasswordInput.focus() - // (once ref is moved up to functional component) - throw e; - } - }, [accountPassword, callDeleteThread, modalContext, threadInfo]); - - const onDelete = React.useCallback( - (event: SyntheticEvent) => { - event.preventDefault(); - dispatchActionPromise(deleteThreadActionTypes, deleteThreadAction()); - }, - [deleteThreadAction, dispatchActionPromise], - ); - const changeThreadSettingsAction = React.useCallback(async () => { invariant( threadInfo, 'threadInfo should exist in changeThreadSettingsAction', ); try { const response = await callChangeThreadSettings({ threadID: threadInfo.id, changes: queuedChanges, }); modalContext.popModal(); return response; } catch (e) { setErrorMessage('unknown_error'); setAccountPassword(''); setCurrentTabType('general'); setQueuedChanges(Object.freeze({})); throw e; } }, [callChangeThreadSettings, modalContext, queuedChanges, threadInfo]); const onSubmit = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); dispatchActionPromise( changeThreadSettingsActionTypes, changeThreadSettingsAction(), ); }, [changeThreadSettingsAction, dispatchActionPromise], ); React.useEffect(() => { if ( threadInfo && currentTabType !== 'general' && !hasPermissionForTab(threadInfo, currentTabType) ) { setCurrentTabType('general'); } }, [currentTabType, hasPermissionForTab, threadInfo]); if (!threadInfo) { return (

    You no longer have permission to view this thread

    ); } const inputDisabled = changeInProgress || !hasPermissionForTab(threadInfo, currentTabType); let mainContent; if (currentTabType === 'general') { mainContent = ( ); } else if (currentTabType === 'privacy') { mainContent = ( ); } else if (currentTabType === 'delete') { mainContent = ( ); } let buttons; - if (currentTabType === 'delete') { - buttons = ( - - ); - } else { + if (currentTabType !== 'delete') { buttons = ( ); } const tabs = [ , ]; // This UI needs to be updated to handle sidebars but we haven't gotten // there yet. We'll probably end up ripping it out anyways, so for now we // are just hiding the privacy tab for any thread that was created as a // sidebar const canSeePrivacyTab = (queuedChanges['parentThreadID'] ?? threadInfo['parentThreadID']) && threadInfo.sourceMessageID && (threadInfo.type === threadTypes.COMMUNITY_OPEN_SUBTHREAD || threadInfo.type === threadTypes.COMMUNITY_SECRET_SUBTHREAD); if (canSeePrivacyTab) { tabs.push( , ); } const canDeleteThread = hasPermissionForTab(threadInfo, 'delete'); if (canDeleteThread) { tabs.push( , ); } return (
      {tabs}
    {mainContent}
    {buttons}
    {errorMessage}
    ); }, ); export default ConnectedThreadSettingsModal;