diff --git a/web/components/button.react.js b/web/components/button.react.js index e6f0ad240..20d6e89d3 100644 --- a/web/components/button.react.js +++ b/web/components/button.react.js @@ -1,47 +1,47 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import css from './button.css'; export type ButtonVariant = | 'primary' | 'secondary' | 'success' | 'danger' | 'round'; -type Props = { +export type ButtonProps = { +onClick: (event: SyntheticEvent) => mixed, +children: React.Node, +variant?: ButtonVariant, +type?: string, +disabled?: boolean, +className?: string, }; -function Button(props: Props): React.Node { +function Button(props: ButtonProps): React.Node { const { onClick, children, variant = 'primary', type, disabled = false, className = '', } = props; const btnCls = classnames(css.btn, css[variant]); return ( ); } export default Button; diff --git a/web/modals/threads/settings/submit-section.css b/web/modals/threads/settings/submit-section.css new file mode 100644 index 000000000..216c11ee0 --- /dev/null +++ b/web/modals/threads/settings/submit-section.css @@ -0,0 +1,22 @@ +.container { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; +} + +.error { + text-align: center; + font-size: 16px; + color: var(--error); + font-style: italic; + + margin-top: auto; + padding: 5px; +} + +.button { + width: 100%; + align-self: flex-end; + min-height: 45px; +} diff --git a/web/modals/threads/settings/submit-section.react.js b/web/modals/threads/settings/submit-section.react.js new file mode 100644 index 000000000..838ba1176 --- /dev/null +++ b/web/modals/threads/settings/submit-section.react.js @@ -0,0 +1,46 @@ +// @flow + +import classnames from 'classnames'; +import * as React from 'react'; + +import type { ButtonProps } from '../../../components/button.react'; +import Button from '../../../components/button.react'; +import css from './submit-section.css'; + +type Props = { + ...ButtonProps, + +errorMessage?: ?string, + +containerClassName?: string, +}; + +function SubmitSection(props: Props): React.Node { + const { + children, + containerClassName = '', + errorMessage, + onClick, + variant, + disabled = false, + className = '', + } = props; + + const containerStyle = classnames(css.container, containerClassName); + const buttonStyle = classnames(css.button, className); + + return ( +
+
{errorMessage}
+ +
+ ); +} + +export default SubmitSection; diff --git a/web/modals/threads/settings/thread-settings-delete-tab.css b/web/modals/threads/settings/thread-settings-delete-tab.css index a5fc533ee..42c1a1ea0 100644 --- a/web/modals/threads/settings/thread-settings-delete-tab.css +++ b/web/modals/threads/settings/thread-settings-delete-tab.css @@ -1,31 +1,33 @@ p.confirm_account_password { margin-bottom: 4px; color: var(--fg); font-size: var(--s-font-14); } div.form_title { padding-top: 6px; font-size: var(--s-font-14); font-weight: 600; vertical-align: top; color: var(--fg); } div.form_content { display: flex; font-family: var(--font-stack); color: var(--fg); margin-top: 8px; margin-bottom: 12px; } +form.container { + display: flex; + flex-direction: column; + flex: 1; +} + .deletion_warning { font-weight: var(--bold); color: var(--fg); font-size: var(--s-font-14); margin-bottom: 16px; } - -.delete_button { - width: 100%; -} diff --git a/web/modals/threads/settings/thread-settings-delete-tab.react.js b/web/modals/threads/settings/thread-settings-delete-tab.react.js index 9deb0e7c5..c04798389 100644 --- a/web/modals/threads/settings/thread-settings-delete-tab.react.js +++ b/web/modals/threads/settings/thread-settings-delete-tab.react.js @@ -1,124 +1,126 @@ // @flow import * as React from 'react'; import { deleteThreadActionTypes, deleteThread, } from 'lib/actions/thread-actions'; import { type SetState } from 'lib/types/hook-types'; import { type ThreadInfo } from 'lib/types/thread-types'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; -import Button from '../../../components/button.react'; import SWMansionIcon from '../../../SWMansionIcon.react'; import Input from '../../input.react'; import { useModalContext } from '../../modal-provider.react'; +import SubmitSection from './submit-section.react'; import css from './thread-settings-delete-tab.css'; type ThreadSettingsDeleteTabProps = { +threadSettingsOperationInProgress: boolean, +threadInfo: ThreadInfo, - +setErrorMessage: SetState, + +setErrorMessage: SetState, + +errorMessage?: ?string, }; function ThreadSettingsDeleteTab( props: ThreadSettingsDeleteTabProps, ): React.Node { const { threadSettingsOperationInProgress, threadInfo, setErrorMessage, + errorMessage, } = props; const modalContext = useModalContext(); const dispatchActionPromise = useDispatchActionPromise(); const callDeleteThread = useServerCall(deleteThread); const accountPasswordInputRef = React.useRef(); const [accountPassword, setAccountPassword] = React.useState(''); const onChangeAccountPassword = React.useCallback( (event: SyntheticEvent) => { const target = event.currentTarget; setAccountPassword(target.value); }, [], ); const deleteThreadAction = React.useCallback(async () => { try { setErrorMessage(''); const response = await callDeleteThread(threadInfo.id, accountPassword); modalContext.popModal(); return response; } catch (e) { setErrorMessage( e.message === 'invalid_credentials' ? 'wrong password' : 'unknown error', ); setAccountPassword(''); accountPasswordInputRef.current?.focus(); throw e; } }, [ accountPassword, callDeleteThread, modalContext, setAccountPassword, setErrorMessage, threadInfo.id, ]); const onDelete = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); dispatchActionPromise(deleteThreadActionTypes, deleteThreadAction()); }, [deleteThreadAction, dispatchActionPromise], ); return ( -
+

Your chat 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/settings/thread-settings-general-tab.css b/web/modals/threads/settings/thread-settings-general-tab.css index 689b3a88a..7fe08877c 100644 --- a/web/modals/threads/settings/thread-settings-general-tab.css +++ b/web/modals/threads/settings/thread-settings-general-tab.css @@ -1,33 +1,34 @@ div.form_title { padding-top: 6px; font-size: 14px; font-weight: 600; vertical-align: top; color: var(--fg); } div.form_content { display: flex; font-family: var(--font-stack); color: var(--fg); margin-top: 8px; margin-bottom: 12px; } div.form_content textarea { padding: 12px; background: var(--modal-bg); color: var(--fg); border: 1px solid var(--border-color); font-size: var(--m-font-16); border-radius: 4px; width: 100%; } div.colorSelectorContainer { margin-top: 8px; margin-bottom: 12px; } -.save_button { - width: 100%; - min-height: 46px; +form.container { + display: flex; + flex-direction: column; + flex: 1; } diff --git a/web/modals/threads/settings/thread-settings-general-tab.react.js b/web/modals/threads/settings/thread-settings-general-tab.react.js index 4de7cdc2a..2e11809b9 100644 --- a/web/modals/threads/settings/thread-settings-general-tab.react.js +++ b/web/modals/threads/settings/thread-settings-general-tab.react.js @@ -1,197 +1,198 @@ // @flow import * as React from 'react'; import tinycolor from 'tinycolor2'; import { changeThreadSettingsActionTypes, changeThreadSettings, } from 'lib/actions/thread-actions'; import { threadHasPermission } from 'lib/shared/thread-utils'; import { type SetState } from 'lib/types/hook-types'; import { type ThreadInfo, type ThreadChanges, threadPermissions, } from 'lib/types/thread-types'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; import { firstLine } from 'lib/utils/string-utils'; -import Button from '../../../components/button.react'; import LoadingIndicator from '../../../loading-indicator.react'; import Input from '../../input.react'; import ColorSelector from '../color-selector.react'; +import SubmitSection from './submit-section.react'; import css from './thread-settings-general-tab.css'; type ThreadSettingsGeneralTabProps = { +threadSettingsOperationInProgress: boolean, +threadInfo: ThreadInfo, +threadNamePlaceholder: string, +queuedChanges: ThreadChanges, +setQueuedChanges: SetState, - +setErrorMessage: SetState, + +setErrorMessage: SetState, + +errorMessage?: ?string, }; function ThreadSettingsGeneralTab( props: ThreadSettingsGeneralTabProps, ): React.Node { const { threadSettingsOperationInProgress, threadInfo, threadNamePlaceholder, queuedChanges, setQueuedChanges, setErrorMessage, + errorMessage, } = props; const dispatchActionPromise = useDispatchActionPromise(); const callChangeThreadSettings = useServerCall(changeThreadSettings); const nameInputRef = React.useRef(); React.useEffect(() => { nameInputRef.current?.focus(); }, [threadSettingsOperationInProgress]); 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; const newName = firstLine(target.value); setQueuedChanges(prevQueuedChanges => Object.freeze({ ...prevQueuedChanges, name: newName !== threadInfo.name ? newName : undefined, }), ); }, [setQueuedChanges, threadInfo.name], ); const onChangeDescription = React.useCallback( (event: SyntheticEvent) => { const target = event.currentTarget; setQueuedChanges(prevQueuedChanges => Object.freeze({ ...prevQueuedChanges, description: target.value !== threadInfo.description ? target.value : undefined, }), ); }, [setQueuedChanges, threadInfo.description], ); const onChangeColor = React.useCallback( (color: string) => { setQueuedChanges(prevQueuedChanges => Object.freeze({ ...prevQueuedChanges, color: !tinycolor.equals(color, threadInfo.color) ? color : undefined, }), ); }, [setQueuedChanges, threadInfo.color], ); const changeThreadSettingsAction = React.useCallback(async () => { try { setErrorMessage(''); return await callChangeThreadSettings({ threadID: threadInfo.id, changes: queuedChanges, }); } catch (e) { setErrorMessage('unknown_error'); throw e; } finally { setQueuedChanges(Object.freeze({})); } }, [ callChangeThreadSettings, queuedChanges, setErrorMessage, setQueuedChanges, threadInfo.id, ]); const onSubmit = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); dispatchActionPromise( changeThreadSettingsActionTypes, changeThreadSettingsAction(), ); }, [changeThreadSettingsAction, dispatchActionPromise], ); const threadNameInputDisabled = !threadHasPermission( threadInfo, threadPermissions.EDIT_THREAD_NAME, ); const saveButtonContent = React.useMemo(() => { if (threadSettingsOperationInProgress) { return ; } return 'Save'; }, [threadSettingsOperationInProgress]); return ( -
+
Chat name
Description