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 { IconButton } from 'mastodon/components/icon_button';
 | 
			
		||||
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 AccountNoteContainer from 'mastodon/features/account/containers/account_note_container';
 | 
			
		||||
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
 | 
			
		||||
import { useLinks } from 'mastodon/hooks/useLinks';
 | 
			
		||||
import { useIdentity } from 'mastodon/identity_context';
 | 
			
		||||
@@ -923,7 +923,7 @@ export const AccountHeader: React.FC<{
 | 
			
		||||
                onClickCapture={handleLinkClick}
 | 
			
		||||
              >
 | 
			
		||||
                {account.id !== me && signedIn && (
 | 
			
		||||
                  <AccountNoteContainer accountId={accountId} />
 | 
			
		||||
                  <AccountNote accountId={accountId} />
 | 
			
		||||
                )}
 | 
			
		||||
 | 
			
		||||
                {account.note.length > 0 && account.note !== '<p></p>' && (
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user