about summary refs log tree commit diff
path: root/app/javascript/mastodon/features/account/components/account_note.js
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2020-07-07 01:24:03 +0200
committerGitHub <noreply@github.com>2020-07-07 01:24:03 +0200
commitc3187411c26aa00eb5844e4a7f9889f2cb19867e (patch)
treeaa5b0676119c27d73680c5e7dd0d709ebbaaadde /app/javascript/mastodon/features/account/components/account_note.js
parent83fd046107999cc8ce166c997afee74409359002 (diff)
Change design of account notes in web UI (#14208)
* Change design of account notes in web UI

* Fix `for` -> `htmlFor`
Diffstat (limited to 'app/javascript/mastodon/features/account/components/account_note.js')
-rw-r--r--app/javascript/mastodon/features/account/components/account_note.js183
1 files changed, 125 insertions, 58 deletions
diff --git a/app/javascript/mastodon/features/account/components/account_note.js b/app/javascript/mastodon/features/account/components/account_note.js
index 832a96a6a..1787ce1ab 100644
--- a/app/javascript/mastodon/features/account/components/account_note.js
+++ b/app/javascript/mastodon/features/account/components/account_note.js
@@ -3,99 +3,166 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import Icon from 'mastodon/components/icon';
 import Textarea from 'react-textarea-autosize';
+import { is } from 'immutable';
 
 const messages = defineMessages({
-  placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' },
+  placeholder: { id: 'account_note.placeholder', defaultMessage: 'Click to add a note' },
 });
 
+class InlineAlert extends React.PureComponent {
+
+  static propTypes = {
+    show: PropTypes.bool,
+  };
+
+  state = {
+    mountMessage: false,
+  };
+
+  static TRANSITION_DELAY = 200;
+
+  componentWillReceiveProps (nextProps) {
+    if (!this.props.show && nextProps.show) {
+      this.setState({ mountMessage: true });
+    } else if (this.props.show && !nextProps.show) {
+      setTimeout(() => this.setState({ mountMessage: false }), InlineAlert.TRANSITION_DELAY);
+    }
+  }
+
+  render () {
+    const { show } = this.props;
+    const { mountMessage } = this.state;
+
+    return (
+      <span aria-live='polite' role='status' className='inline-alert' style={{ opacity: show ? 1 : 0 }}>
+        {mountMessage && <FormattedMessage id='generic.saved' defaultMessage='Saved' />}
+      </span>
+    );
+  }
+
+}
+
 export default @injectIntl
-class Header extends ImmutablePureComponent {
+class AccountNote extends ImmutablePureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map.isRequired,
-    isEditing: PropTypes.bool,
-    isSubmitting: PropTypes.bool,
-    accountNote: PropTypes.string,
-    onEditAccountNote: PropTypes.func.isRequired,
-    onCancelAccountNote: PropTypes.func.isRequired,
-    onSaveAccountNote: PropTypes.func.isRequired,
-    onChangeAccountNote: PropTypes.func.isRequired,
+    value: PropTypes.string,
+    onSave: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
   };
 
-  handleChangeAccountNote = (e) => {
-    this.props.onChangeAccountNote(e.target.value);
+  state = {
+    value: null,
+    saving: false,
+    saved: false,
   };
 
+  componentWillMount () {
+    this._reset();
+  }
+
+  componentWillReceiveProps (nextProps) {
+    const accountWillChange = !is(this.props.account, nextProps.account);
+    const newState = {};
+
+    if (accountWillChange && this._isDirty()) {
+      this._save(false);
+    }
+
+    if (accountWillChange || nextProps.value === this.state.value) {
+      newState.saving = false;
+    }
+
+    if (this.props.value !== nextProps.value) {
+      newState.value = nextProps.value;
+    }
+
+    this.setState(newState);
+  }
+
   componentWillUnmount () {
-    if (this.props.isEditing) {
-      this.props.onCancelAccountNote();
+    if (this._isDirty()) {
+      this._save(false);
     }
   }
 
+  setTextareaRef = c => {
+    this.textarea = c;
+  }
+
+  handleChange = e => {
+    this.setState({ value: e.target.value, saving: false });
+  };
+
   handleKeyDown = e => {
     if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
-      this.props.onSaveAccountNote();
+      e.preventDefault();
+
+      this._save();
+
+      if (this.textarea) {
+        this.textarea.blur();
+      }
     } else if (e.keyCode === 27) {
-      this.props.onCancelAccountNote();
+      e.preventDefault();
+
+      this._reset(() => {
+        if (this.textarea) {
+          this.textarea.blur();
+        }
+      });
     }
   }
 
+  handleBlur = () => {
+    if (this._isDirty()) {
+      this._save();
+    }
+  }
+
+  _save (showMessage = true) {
+    this.setState({ saving: true }, () => this.props.onSave(this.state.value));
+
+    if (showMessage) {
+      this.setState({ saved: true }, () => setTimeout(() => this.setState({ saved: false }), 2000));
+    }
+  }
+
+  _reset (callback) {
+    this.setState({ value: this.props.value }, callback);
+  }
+
+  _isDirty () {
+    return !this.state.saving && this.props.value !== null && this.state.value !== null && this.state.value !== this.props.value;
+  }
+
   render () {
-    const { account, accountNote, isEditing, isSubmitting, intl } = this.props;
+    const { account, intl } = this.props;
+    const { value, saved } = this.state;
 
-    if (!account || (!accountNote && !isEditing)) {
+    if (!account) {
       return null;
     }
 
-    let action_buttons = null;
-    if (isEditing) {
-      action_buttons = (
-        <div className='account__header__account-note__buttons'>
-          <button className='text-btn' tabIndex='0' onClick={this.props.onCancelAccountNote} disabled={isSubmitting}>
-            <Icon id='times' size={15} /> <FormattedMessage id='account_note.cancel' defaultMessage='Cancel' />
-          </button>
-          <div className='flex-spacer' />
-          <button className='text-btn' tabIndex='0' onClick={this.props.onSaveAccountNote} disabled={isSubmitting}>
-            <Icon id='check' size={15} /> <FormattedMessage id='account_note.save' defaultMessage='Save' />
-          </button>
-        </div>
-      );
-    }
+    return (
+      <div className='account__header__account-note'>
+        <label htmlFor={`account-note-${account.get('id')}`}>
+          <FormattedMessage id='account.account_note_header' defaultMessage='Note' /> <InlineAlert show={saved} />
+        </label>
 
-    let note_container = null;
-    if (isEditing) {
-      note_container = (
         <Textarea
+          id={`account-note-${account.get('id')}`}
           className='account__header__account-note__content'
-          disabled={isSubmitting}
+          disabled={this.props.value === null || value === null}
           placeholder={intl.formatMessage(messages.placeholder)}
-          value={accountNote}
-          onChange={this.handleChangeAccountNote}
+          value={value || ''}
+          onChange={this.handleChange}
           onKeyDown={this.handleKeyDown}
-          autoFocus
+          onBlur={this.handleBlur}
+          ref={this.setTextareaRef}
         />
-      );
-    } else {
-      note_container = (<div className='account__header__account-note__content'>{accountNote}</div>);
-    }
-
-    return (
-      <div className='account__header__account-note'>
-        <div className='account__header__account-note__header'>
-          <strong><FormattedMessage id='account.account_note_header' defaultMessage='Your note for @{name}' values={{ name: account.get('username') }} /></strong>
-          {!isEditing && (
-            <div>
-              <button className='text-btn' tabIndex='0' onClick={this.props.onEditAccountNote} disabled={isSubmitting}>
-                <Icon id='pencil' size={15} /> <FormattedMessage id='account_note.edit' defaultMessage='Edit' />
-              </button>
-            </div>
-          )}
-        </div>
-        {note_container}
-        {action_buttons}
       </div>
     );
   }