Rewrite AccountNote as Typescript functional component (#34925)
				
					
				
			Co-authored-by: diondiondion <mail@diondiondion.com>
This commit is contained in:
		@@ -1,181 +0,0 @@
 | 
				
			|||||||
import PropTypes from 'prop-types';
 | 
					 | 
				
			||||||
import { PureComponent } from 'react';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { is } from 'immutable';
 | 
					 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import Textarea from 'react-textarea-autosize';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const messages = defineMessages({
 | 
					 | 
				
			||||||
  placeholder: { id: 'account_note.placeholder', defaultMessage: 'Click to add a note' },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class InlineAlert extends PureComponent {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static propTypes = {
 | 
					 | 
				
			||||||
    show: PropTypes.bool,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  state = {
 | 
					 | 
				
			||||||
    mountMessage: false,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static TRANSITION_DELAY = 200;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  UNSAFE_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>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AccountNote extends ImmutablePureComponent {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static propTypes = {
 | 
					 | 
				
			||||||
    accountId: PropTypes.string.isRequired,
 | 
					 | 
				
			||||||
    value: PropTypes.string,
 | 
					 | 
				
			||||||
    onSave: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    intl: PropTypes.object.isRequired,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  state = {
 | 
					 | 
				
			||||||
    value: null,
 | 
					 | 
				
			||||||
    saving: false,
 | 
					 | 
				
			||||||
    saved: false,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  UNSAFE_componentWillMount () {
 | 
					 | 
				
			||||||
    this._reset();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  UNSAFE_componentWillReceiveProps (nextProps) {
 | 
					 | 
				
			||||||
    const accountWillChange = !is(this.props.accountId, nextProps.accountId);
 | 
					 | 
				
			||||||
    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._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)) {
 | 
					 | 
				
			||||||
      e.preventDefault();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (this.textarea) {
 | 
					 | 
				
			||||||
        this.textarea.blur();
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        this._save();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else if (e.keyCode === 27) {
 | 
					 | 
				
			||||||
      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 { accountId, intl } = this.props;
 | 
					 | 
				
			||||||
    const { value, saved } = this.state;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!accountId) {
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <div className='account__header__account-note'>
 | 
					 | 
				
			||||||
        <label htmlFor={`account-note-${accountId}`}>
 | 
					 | 
				
			||||||
          <FormattedMessage id='account.account_note_header' defaultMessage='Personal note' /> <InlineAlert show={saved} />
 | 
					 | 
				
			||||||
        </label>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        {this.props.value === undefined ? (
 | 
					 | 
				
			||||||
          <div className='account__header__account-note__loading-indicator-wrapper'>
 | 
					 | 
				
			||||||
            <LoadingIndicator />
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        ) : (
 | 
					 | 
				
			||||||
          <Textarea
 | 
					 | 
				
			||||||
            id={`account-note-${accountId}`}
 | 
					 | 
				
			||||||
            className='account__header__account-note__content'
 | 
					 | 
				
			||||||
            disabled={value === null}
 | 
					 | 
				
			||||||
            placeholder={intl.formatMessage(messages.placeholder)}
 | 
					 | 
				
			||||||
            value={value || ''}
 | 
					 | 
				
			||||||
            onChange={this.handleChange}
 | 
					 | 
				
			||||||
            onKeyDown={this.handleKeyDown}
 | 
					 | 
				
			||||||
            onBlur={this.handleBlur}
 | 
					 | 
				
			||||||
            ref={this.setTextareaRef}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default injectIntl(AccountNote);
 | 
					 | 
				
			||||||
@@ -0,0 +1,131 @@
 | 
				
			|||||||
 | 
					import type { ChangeEventHandler, KeyboardEventHandler } from 'react';
 | 
				
			||||||
 | 
					import { useState, useRef, useCallback, useId } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Textarea from 'react-textarea-autosize';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { submitAccountNote } from '@/mastodon/actions/account_notes';
 | 
				
			||||||
 | 
					import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
 | 
				
			||||||
 | 
					import { useAppDispatch, useAppSelector } from '@/mastodon/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  placeholder: {
 | 
				
			||||||
 | 
					    id: 'account_note.placeholder',
 | 
				
			||||||
 | 
					    defaultMessage: 'Click to add a note',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AccountNoteUI: React.FC<{
 | 
				
			||||||
 | 
					  initialValue: string | undefined;
 | 
				
			||||||
 | 
					  onSubmit: (newNote: string) => void;
 | 
				
			||||||
 | 
					  wasSaved: boolean;
 | 
				
			||||||
 | 
					}> = ({ initialValue, onSubmit, wasSaved }) => {
 | 
				
			||||||
 | 
					  const intl = useIntl();
 | 
				
			||||||
 | 
					  const uniqueId = useId();
 | 
				
			||||||
 | 
					  const [value, setValue] = useState(initialValue ?? '');
 | 
				
			||||||
 | 
					  const isLoading = initialValue === undefined;
 | 
				
			||||||
 | 
					  const canSubmitOnBlurRef = useRef(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleChange = useCallback<ChangeEventHandler<HTMLTextAreaElement>>(
 | 
				
			||||||
 | 
					    (e) => {
 | 
				
			||||||
 | 
					      setValue(e.target.value);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
 | 
				
			||||||
 | 
					    (e) => {
 | 
				
			||||||
 | 
					      if (e.key === 'Escape') {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setValue(initialValue ?? '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        canSubmitOnBlurRef.current = false;
 | 
				
			||||||
 | 
					        e.currentTarget.blur();
 | 
				
			||||||
 | 
					      } else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        onSubmit(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        canSubmitOnBlurRef.current = false;
 | 
				
			||||||
 | 
					        e.currentTarget.blur();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [initialValue, onSubmit, value],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleBlur = useCallback(() => {
 | 
				
			||||||
 | 
					    if (initialValue !== value && canSubmitOnBlurRef.current) {
 | 
				
			||||||
 | 
					      onSubmit(value);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    canSubmitOnBlurRef.current = true;
 | 
				
			||||||
 | 
					  }, [initialValue, onSubmit, value]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className='account__header__account-note'>
 | 
				
			||||||
 | 
					      <label htmlFor={`account-note-${uniqueId}`}>
 | 
				
			||||||
 | 
					        <FormattedMessage
 | 
				
			||||||
 | 
					          id='account.account_note_header'
 | 
				
			||||||
 | 
					          defaultMessage='Personal note'
 | 
				
			||||||
 | 
					        />{' '}
 | 
				
			||||||
 | 
					        <span
 | 
				
			||||||
 | 
					          aria-live='polite'
 | 
				
			||||||
 | 
					          role='status'
 | 
				
			||||||
 | 
					          className='inline-alert'
 | 
				
			||||||
 | 
					          style={{ opacity: wasSaved ? 1 : 0 }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {wasSaved && (
 | 
				
			||||||
 | 
					            <FormattedMessage id='generic.saved' defaultMessage='Saved' />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </label>
 | 
				
			||||||
 | 
					      {isLoading ? (
 | 
				
			||||||
 | 
					        <div className='account__header__account-note__loading-indicator-wrapper'>
 | 
				
			||||||
 | 
					          <LoadingIndicator />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <Textarea
 | 
				
			||||||
 | 
					          id={`account-note-${uniqueId}`}
 | 
				
			||||||
 | 
					          className='account__header__account-note__content'
 | 
				
			||||||
 | 
					          placeholder={intl.formatMessage(messages.placeholder)}
 | 
				
			||||||
 | 
					          value={value}
 | 
				
			||||||
 | 
					          onChange={handleChange}
 | 
				
			||||||
 | 
					          onKeyDown={handleKeyDown}
 | 
				
			||||||
 | 
					          onBlur={handleBlur}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AccountNote: React.FC<{
 | 
				
			||||||
 | 
					  accountId: string;
 | 
				
			||||||
 | 
					}> = ({ accountId }) => {
 | 
				
			||||||
 | 
					  const dispatch = useAppDispatch();
 | 
				
			||||||
 | 
					  const initialValue = useAppSelector((state) =>
 | 
				
			||||||
 | 
					    state.relationships.get(accountId)?.get('note'),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const [wasSaved, setWasSaved] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit = useCallback(
 | 
				
			||||||
 | 
					    (note: string) => {
 | 
				
			||||||
 | 
					      setWasSaved(true);
 | 
				
			||||||
 | 
					      void dispatch(submitAccountNote({ accountId, note }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        setWasSaved(false);
 | 
				
			||||||
 | 
					      }, 2000);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [dispatch, accountId],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <AccountNoteUI
 | 
				
			||||||
 | 
					      key={`${accountId}-${initialValue}`}
 | 
				
			||||||
 | 
					      initialValue={initialValue}
 | 
				
			||||||
 | 
					      wasSaved={wasSaved}
 | 
				
			||||||
 | 
					      onSubmit={handleSubmit}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -1,19 +0,0 @@
 | 
				
			|||||||
import { connect } from 'react-redux';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { submitAccountNote } from 'mastodon/actions/account_notes';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import AccountNote from '../components/account_note';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const mapStateToProps = (state, { accountId }) => ({
 | 
					 | 
				
			||||||
  value: state.relationships.getIn([accountId, 'note']),
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const mapDispatchToProps = (dispatch, { accountId }) => ({
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onSave (value) {
 | 
					 | 
				
			||||||
    dispatch(submitAccountNote({ accountId: accountId, note: value }));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default connect(mapStateToProps, mapDispatchToProps)(AccountNote);
 | 
					 | 
				
			||||||
@@ -44,8 +44,8 @@ import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
 | 
				
			|||||||
import { Icon } from 'mastodon/components/icon';
 | 
					import { Icon } from 'mastodon/components/icon';
 | 
				
			||||||
import { IconButton } from 'mastodon/components/icon_button';
 | 
					import { IconButton } from 'mastodon/components/icon_button';
 | 
				
			||||||
import { ShortNumber } from 'mastodon/components/short_number';
 | 
					import { ShortNumber } from 'mastodon/components/short_number';
 | 
				
			||||||
 | 
					import { AccountNote } from 'mastodon/features/account/components/account_note';
 | 
				
			||||||
import { DomainPill } from 'mastodon/features/account/components/domain_pill';
 | 
					import { DomainPill } from 'mastodon/features/account/components/domain_pill';
 | 
				
			||||||
import AccountNoteContainer from 'mastodon/features/account/containers/account_note_container';
 | 
					 | 
				
			||||||
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
 | 
					import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
 | 
				
			||||||
import { useLinks } from 'mastodon/hooks/useLinks';
 | 
					import { useLinks } from 'mastodon/hooks/useLinks';
 | 
				
			||||||
import { useIdentity } from 'mastodon/identity_context';
 | 
					import { useIdentity } from 'mastodon/identity_context';
 | 
				
			||||||
@@ -923,7 +923,7 @@ export const AccountHeader: React.FC<{
 | 
				
			|||||||
                onClickCapture={handleLinkClick}
 | 
					                onClickCapture={handleLinkClick}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                {account.id !== me && signedIn && (
 | 
					                {account.id !== me && signedIn && (
 | 
				
			||||||
                  <AccountNoteContainer accountId={accountId} />
 | 
					                  <AccountNote accountId={accountId} />
 | 
				
			||||||
                )}
 | 
					                )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                {account.note.length > 0 && account.note !== '<p></p>' && (
 | 
					                {account.note.length > 0 && account.note !== '<p></p>' && (
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user