diff --git a/web/modals/threads/thread-settings-modal.react.js b/web/modals/threads/thread-settings-modal.react.js index 04420fcc3..d3f2fa868 100644 --- a/web/modals/threads/thread-settings-modal.react.js +++ b/web/modals/threads/thread-settings-modal.react.js @@ -1,510 +1,518 @@ // @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 SetState } from 'lib/types/hook-types.js'; import { type ThreadInfo, threadTypes, assertThreadType, type ChangeThreadSettingsPayload, type UpdateThreadRequest, type LeaveThreadPayload, threadPermissions, type ThreadChanges, } from 'lib/types/thread-types'; import type { UserInfos } from 'lib/types/user-types'; import { useDispatchActionPromise, useServerCall, type DispatchActionPromise, } from 'lib/utils/action-utils'; import { firstLine } from 'lib/utils/string-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, }; type Props = { ...BaseProps, +threadInfo: ThreadInfo, +changeInProgress: boolean, +viewerID: ?string, +userInfos: UserInfos, +dispatchActionPromise: DispatchActionPromise, +deleteThread: ( threadID: string, currentAccountPassword: string, ) => Promise, +changeThreadSettings: ( update: UpdateThreadRequest, ) => Promise, +onClose: () => void, +errorMessage: string, +setErrorMessage: SetState, +accountPassword: string, +setAccountPassword: SetState, +currentTabType: TabType, +setCurrentTabType: SetState, +queuedChanges: ThreadChanges, +setQueuedChanges: SetState, +namePlaceholder: string, +changeQueued: boolean, +onChangeName: (event: SyntheticEvent) => void, +onChangeDescription: (event: SyntheticEvent) => void, +onChangeColor: (color: string) => void, +onChangeThreadType: (event: SyntheticEvent) => void, +onChangeAccountPassword: (event: SyntheticEvent) => void, + +hasPermissionForTab: (thread: ThreadInfo, tab: TabType) => boolean, }; class ThreadSettingsModal extends React.PureComponent { nameInput: ?HTMLInputElement; accountPasswordInput: ?HTMLInputElement; constructor(props: Props) { super(props); } componentDidMount() { invariant(this.nameInput, 'nameInput ref unset'); this.nameInput.focus(); } componentDidUpdate(prevProps: Props) { if (this.props.currentTabType !== 'delete') { return; } - const permissionForDeleteTab = this.hasPermissionForTab( + const permissionForDeleteTab = this.props.hasPermissionForTab( this.props.threadInfo, 'delete', ); - const prevPermissionForDeleteTab = this.hasPermissionForTab( + const prevPermissionForDeleteTab = this.props.hasPermissionForTab( prevProps.threadInfo, 'delete', ); if (!permissionForDeleteTab && prevPermissionForDeleteTab) { this.setTab('general'); } } - hasPermissionForTab(threadInfo: ThreadInfo, tab: TabType) { - if (tab === 'general') { - return threadHasPermission( - threadInfo, - threadPermissions.EDIT_THREAD_NAME, - ); - } else if (tab === 'privacy') { - return threadHasPermission( - threadInfo, - threadPermissions.EDIT_PERMISSIONS, - ); - } else if (tab === 'delete') { - return threadHasPermission(threadInfo, threadPermissions.DELETE_THREAD); - } - invariant(false, `invalid tab ${tab}`); - } - possiblyChangedValue(key: string) { const valueChanged = this.props.queuedChanges[key] !== null && this.props.queuedChanges[key] !== undefined; return valueChanged ? this.props.queuedChanges[key] : this.props.threadInfo[key]; } render() { const { threadInfo } = this.props; const inputDisabled = this.props.changeInProgress || - !this.hasPermissionForTab(threadInfo, this.props.currentTabType); + !this.props.hasPermissionForTab(threadInfo, this.props.currentTabType); let mainContent = null; if (this.props.currentTabType === 'general') { mainContent = ( ); } else if (this.props.currentTabType === 'privacy') { mainContent = ( ); } else if (this.props.currentTabType === 'delete') { mainContent = ( ); } let buttons = null; if (this.props.currentTabType === 'delete') { buttons = ( ); } else { 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 = this.possiblyChangedValue('parentThreadID') && threadInfo.sourceMessageID && (threadInfo.type === threadTypes.COMMUNITY_OPEN_SUBTHREAD || threadInfo.type === threadTypes.COMMUNITY_SECRET_SUBTHREAD); if (canSeePrivacyTab) { tabs.push( , ); } - const canDeleteThread = this.hasPermissionForTab(threadInfo, 'delete'); + const canDeleteThread = this.props.hasPermissionForTab( + threadInfo, + 'delete', + ); if (canDeleteThread) { tabs.push( , ); } return (
      {tabs}
    {mainContent}
    {buttons}
    {this.props.errorMessage}
    ); } setTab = (tabType: TabType) => { this.props.setCurrentTabType(tabType); }; nameInputRef = (nameInput: ?HTMLInputElement) => { this.nameInput = nameInput; }; accountPasswordInputRef = (accountPasswordInput: ?HTMLInputElement) => { this.accountPasswordInput = accountPasswordInput; }; 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.props.queuedChanges, }); this.props.onClose(); return response; } catch (e) { this.props.setErrorMessage('unknown error'); this.props.setAccountPassword(''); this.props.setCurrentTabType('general'); this.props.setQueuedChanges(Object.freeze({})); 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.props.accountPassword, ); this.props.onClose(); return response; } catch (e) { const errorMessage = e.message === 'invalid_credentials' ? 'wrong password' : 'unknown error'; this.props.setErrorMessage(errorMessage); this.props.setAccountPassword(''); invariant(this.accountPasswordInput, 'accountPasswordInput ref unset'); this.accountPasswordInput.focus(); throw e; } } } 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 onChangeName = React.useCallback( (event: SyntheticEvent) => { const target = event.currentTarget; setQueuedChanges( Object.freeze({ ...queuedChanges, name: firstLine( target.value !== threadInfo?.name ? target.value : undefined, ), }), ); }, [queuedChanges, threadInfo?.name], ); const onChangeDescription = React.useCallback( (event: SyntheticEvent) => { const target = event.currentTarget; setQueuedChanges( Object.freeze({ ...queuedChanges, description: target.value !== threadInfo?.description ? target.value : undefined, }), ); }, [queuedChanges, threadInfo?.description], ); const onChangeColor = React.useCallback( (color: string) => { setQueuedChanges( Object.freeze({ ...queuedChanges, color: color !== threadInfo?.color ? color : undefined, }), ); }, [queuedChanges, threadInfo?.color], ); const onChangeThreadType = React.useCallback( (event: SyntheticEvent) => { const uiValue = assertThreadType( parseInt(event.currentTarget.value, 10), ); setQueuedChanges( Object.freeze({ ...queuedChanges, type: uiValue !== threadInfo?.type ? uiValue : undefined, }), ); }, [queuedChanges, threadInfo?.type], ); 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}`); + }, + [], + ); + if (!threadInfo) { return (

    You no longer have permission to view this thread

    ); } return ( ); }, ); export default ConnectedThreadSettingsModal;