Skip to content

Commit

Permalink
Change design of account notes in web UI (mastodon#14208)
Browse files Browse the repository at this point in the history
* Change design of account notes in web UI

* Fix `for` -> `htmlFor`
  • Loading branch information
Gargron authored and Mage committed Jan 14, 2022
1 parent 4f3026a commit 50b9b67
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 235 deletions.
36 changes: 2 additions & 34 deletions app/javascript/mastodon/actions/account_notes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,12 @@ export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL';

export const ACCOUNT_NOTE_INIT_EDIT = 'ACCOUNT_NOTE_INIT_EDIT';
export const ACCOUNT_NOTE_CANCEL = 'ACCOUNT_NOTE_CANCEL';

export const ACCOUNT_NOTE_CHANGE_COMMENT = 'ACCOUNT_NOTE_CHANGE_COMMENT';

export function submitAccountNote() {
export function submitAccountNote(id, value) {
return (dispatch, getState) => {
dispatch(submitAccountNoteRequest());

const id = getState().getIn(['account_notes', 'edit', 'account_id']);

api(getState).post(`/api/v1/accounts/${id}/note`, {
comment: getState().getIn(['account_notes', 'edit', 'comment']),
comment: value,
}).then(response => {
dispatch(submitAccountNoteSuccess(response.data));
}).catch(error => dispatch(submitAccountNoteFail(error)));
Expand All @@ -42,28 +35,3 @@ export function submitAccountNoteFail(error) {
error,
};
};

export function initEditAccountNote(account) {
return (dispatch, getState) => {
const comment = getState().getIn(['relationships', account.get('id'), 'note']);

dispatch({
type: ACCOUNT_NOTE_INIT_EDIT,
account,
comment,
});
};
};

export function cancelAccountNote() {
return {
type: ACCOUNT_NOTE_CANCEL,
};
};

export function changeAccountNoteComment(comment) {
return {
type: ACCOUNT_NOTE_CHANGE_COMMENT,
comment,
};
};
183 changes: 125 additions & 58 deletions app/javascript/mastodon/features/account/components/account_note.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
}
Expand Down
11 changes: 2 additions & 9 deletions app/javascript/mastodon/features/account/components/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ class Header extends ImmutablePureComponent {
identity_props: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onEditAccountNote: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired,
};
Expand Down Expand Up @@ -158,8 +157,6 @@ class Header extends ImmutablePureComponent {
return null;
}

const accountNote = account.getIn(['relationship', 'note']);

let info = [];
let actionBtn = '';
let lockedIcon = '';
Expand Down Expand Up @@ -210,10 +207,6 @@ class Header extends ImmutablePureComponent {
menu.push(null);
}

if (accountNote === null) {
menu.push({ text: intl.formatMessage(messages.add_account_note, { name: account.get('username') }), action: this.props.onEditAccountNote });
}

if (account.get('id') === me) {
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
Expand Down Expand Up @@ -322,8 +315,6 @@ class Header extends ImmutablePureComponent {
{this.pawooRenderOauthAthenticationsIcon(account)}
</div>

<AccountNoteContainer account={account} />

<div className='account__header__extra'>
<div className='account__header__bio'>
{ (fields.size > 0 || identity_proofs.size > 0) && (
Expand Down Expand Up @@ -352,6 +343,8 @@ class Header extends ImmutablePureComponent {
</div>
)}

{account.get('id') !== me && <AccountNoteContainer account={account} />}

{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={content} />}
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
import { connect } from 'react-redux';
import { changeAccountNoteComment, submitAccountNote, initEditAccountNote, cancelAccountNote } from 'mastodon/actions/account_notes';
import { submitAccountNote } from 'mastodon/actions/account_notes';
import AccountNote from '../components/account_note';

const mapStateToProps = (state, { account }) => {
const isEditing = state.getIn(['account_notes', 'edit', 'account_id']) === account.get('id');

return {
isSubmitting: state.getIn(['account_notes', 'edit', 'isSubmitting']),
accountNote: isEditing ? state.getIn(['account_notes', 'edit', 'comment']) : account.getIn(['relationship', 'note']),
isEditing,
};
};
const mapStateToProps = (state, { account }) => ({
value: account.getIn(['relationship', 'note']),
});

const mapDispatchToProps = (dispatch, { account }) => ({

onEditAccountNote() {
dispatch(initEditAccountNote(account));
},

onSaveAccountNote() {
dispatch(submitAccountNote());
onSave (value) {
dispatch(submitAccountNote(account.get('id'), value));
},

onCancelAccountNote() {
dispatch(cancelAccountNote());
},

onChangeAccountNote(comment) {
dispatch(changeAccountNoteComment(comment));
},
});

export default connect(mapStateToProps, mapDispatchToProps)(AccountNote);
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { initBlockModal } from '../../../actions/blocks';
import { initReport } from '../../../actions/reports';
import { openModal } from '../../../actions/modal';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
import { initEditAccountNote } from 'mastodon/actions/account_notes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { unfollowModal } from '../../../initial_state';
import { List as ImmutableList } from 'immutable';
Expand Down Expand Up @@ -103,10 +102,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},

onEditAccountNote (account) {
dispatch(initEditAccountNote(account));
},

onBlockDomain (domain) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
Expand Down
Loading

0 comments on commit 50b9b67

Please sign in to comment.