diff --git a/app/javascript/mastodon/actions/account_notes.js b/app/javascript/mastodon/actions/account_notes.js
index 059ed9e803..d174410002 100644
--- a/app/javascript/mastodon/actions/account_notes.js
+++ b/app/javascript/mastodon/actions/account_notes.js
@@ -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)));
@@ -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,
- };
-};
diff --git a/app/javascript/mastodon/features/account/components/account_note.js b/app/javascript/mastodon/features/account/components/account_note.js
index 832a96a6ae..1787ce1abc 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 (
+
+ {mountMessage && }
+
+ );
+ }
+
+}
+
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 = (
-
- );
- }
+ return (
+
+
- let note_container = null;
- if (isEditing) {
- note_container = (
- );
- } else {
- note_container = (
{accountNote}
);
- }
-
- return (
-
-
-
- {!isEditing && (
-
-
-
- )}
-
- {note_container}
- {action_buttons}
);
}
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 144f6bd94c..b5aca574f1 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -67,7 +67,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,
};
@@ -132,8 +131,6 @@ class Header extends ImmutablePureComponent {
return null;
}
- const accountNote = account.getIn(['relationship', 'note']);
-
let info = [];
let actionBtn = '';
let lockedIcon = '';
@@ -184,10 +181,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' });
@@ -294,8 +287,6 @@ class Header extends ImmutablePureComponent {
-
-
{ (fields.size > 0 || identity_proofs.size > 0) && (
@@ -324,6 +315,8 @@ class Header extends ImmutablePureComponent {
)}
+ {account.get('id') !== me &&
}
+
{account.get('note').length > 0 && account.get('note') !== '
' &&
}
diff --git a/app/javascript/mastodon/features/account/containers/account_note_container.js b/app/javascript/mastodon/features/account/containers/account_note_container.js
index 92d470982b..969af553a3 100644
--- a/app/javascript/mastodon/features/account/containers/account_note_container.js
+++ b/app/javascript/mastodon/features/account/containers/account_note_container.js
@@ -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);
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
index e480fb2aa4..8728b48068 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -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';
@@ -103,10 +102,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
- onEditAccountNote (account) {
- dispatch(initEditAccountNote(account));
- },
-
onBlockDomain (domain) {
dispatch(openModal('CONFIRM', {
message: {domain} }} />,
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index fc624dcc03..0aeefecab7 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -666,24 +666,16 @@
{
"descriptors": [
{
- "defaultMessage": "No comment provided",
+ "defaultMessage": "Click to add a note",
"id": "account_note.placeholder"
},
{
- "defaultMessage": "Cancel",
- "id": "account_note.cancel"
- },
- {
- "defaultMessage": "Save",
- "id": "account_note.save"
+ "defaultMessage": "Saved",
+ "id": "generic.saved"
},
{
- "defaultMessage": "Your note for @{name}",
+ "defaultMessage": "Note",
"id": "account.account_note_header"
- },
- {
- "defaultMessage": "Edit",
- "id": "account_note.edit"
}
],
"path": "app/javascript/mastodon/features/account/components/account_note.json"
@@ -818,10 +810,6 @@
"defaultMessage": "Open moderation interface for @{name}",
"id": "status.admin_account"
},
- {
- "defaultMessage": "Add note for @{name}",
- "id": "account.add_account_note"
- },
{
"defaultMessage": "Follows you",
"id": "account.follows_you"
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 1212f2b366..b562b2afcb 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -1,6 +1,5 @@
{
- "account.account_note_header": "Your note for @{name}",
- "account.add_account_note": "Add note for @{name}",
+ "account.account_note_header": "Note",
"account.add_or_remove_from_list": "Add or Remove from lists",
"account.badges.bot": "Bot",
"account.badges.group": "Group",
@@ -42,10 +41,7 @@
"account.unfollow": "Unfollow",
"account.unmute": "Unmute @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}",
- "account_note.cancel": "Cancel",
- "account_note.edit": "Edit",
- "account_note.placeholder": "No comment provided",
- "account_note.save": "Save",
+ "account_note.placeholder": "Click to add note",
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
"alert.rate_limited.title": "Rate limited",
"alert.unexpected.message": "An unexpected error occurred.",
@@ -174,6 +170,7 @@
"follow_request.authorize": "Authorize",
"follow_request.reject": "Reject",
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
+ "generic.saved": "Saved",
"getting_started.developers": "Developers",
"getting_started.directory": "Profile directory",
"getting_started.documentation": "Documentation",
diff --git a/app/javascript/mastodon/reducers/account_notes.js b/app/javascript/mastodon/reducers/account_notes.js
deleted file mode 100644
index b1cf2e0aa8..0000000000
--- a/app/javascript/mastodon/reducers/account_notes.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Map as ImmutableMap } from 'immutable';
-
-import {
- ACCOUNT_NOTE_INIT_EDIT,
- ACCOUNT_NOTE_CANCEL,
- ACCOUNT_NOTE_CHANGE_COMMENT,
- ACCOUNT_NOTE_SUBMIT_REQUEST,
- ACCOUNT_NOTE_SUBMIT_FAIL,
- ACCOUNT_NOTE_SUBMIT_SUCCESS,
-} from '../actions/account_notes';
-
-const initialState = ImmutableMap({
- edit: ImmutableMap({
- isSubmitting: false,
- account_id: null,
- comment: null,
- }),
-});
-
-export default function account_notes(state = initialState, action) {
- switch (action.type) {
- case ACCOUNT_NOTE_INIT_EDIT:
- return state.withMutations((state) => {
- state.setIn(['edit', 'isSubmitting'], false);
- state.setIn(['edit', 'account_id'], action.account.get('id'));
- state.setIn(['edit', 'comment'], action.comment);
- });
- case ACCOUNT_NOTE_CHANGE_COMMENT:
- return state.setIn(['edit', 'comment'], action.comment);
- case ACCOUNT_NOTE_SUBMIT_REQUEST:
- return state.setIn(['edit', 'isSubmitting'], true);
- case ACCOUNT_NOTE_SUBMIT_FAIL:
- return state.setIn(['edit', 'isSubmitting'], false);
- case ACCOUNT_NOTE_SUBMIT_SUCCESS:
- case ACCOUNT_NOTE_CANCEL:
- return state.withMutations((state) => {
- state.setIn(['edit', 'isSubmitting'], false);
- state.setIn(['edit', 'account_id'], null);
- state.setIn(['edit', 'comment'], null);
- });
- default:
- return state;
- }
-}
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index 690349b85f..3823bb05e0 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -36,7 +36,6 @@ import trends from './trends';
import missed_updates from './missed_updates';
import announcements from './announcements';
import markers from './markers';
-import account_notes from './account_notes';
const reducers = {
announcements,
@@ -76,7 +75,6 @@ const reducers = {
trends,
missed_updates,
markers,
- account_notes,
};
export default combineReducers(reducers);
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index b322472977..560a7fffe1 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -11,6 +11,15 @@
position: relative;
}
+.inline-alert {
+ color: $valid-value-color;
+ font-weight: 400;
+
+ .no-reduce-motion & {
+ transition: opacity 200ms ease;
+ }
+}
+
.link-button {
display: block;
font-size: 15px;
@@ -6557,6 +6566,11 @@ noscript {
padding: 20px 15px;
padding-bottom: 5px;
color: $primary-text-color;
+
+ .columns-area--mobile & {
+ padding-left: 20px;
+ padding-right: 20px;
+ }
}
.account__header__fields {
@@ -6601,63 +6615,51 @@ noscript {
}
&__account-note {
- margin: 5px;
- padding: 10px;
- background: $ui-highlight-color;
+ padding: 15px;
+ padding-bottom: 10px;
color: $primary-text-color;
- display: flex;
- flex-direction: column;
border-radius: 4px;
font-size: 14px;
font-weight: 400;
+ border-bottom: 1px solid lighten($ui-base-color, 12%);
- &__header {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- }
-
- &__content {
- white-space: pre-wrap;
- margin-top: 5px;
- }
-
- &__buttons {
- display: flex;
- flex-direction: row;
- justify-content: flex-end;
- margin-top: 5px;
-
- .flex-spacer {
- flex: 0 0 20px;
- background: transparent;
- }
+ .columns-area--mobile & {
+ padding-left: 20px;
+ padding-right: 20px;
}
- strong {
- font-size: 15px;
+ label {
+ display: block;
+ font-size: 12px;
font-weight: 500;
- }
-
- button:hover span {
- text-decoration: underline;
+ color: $darker-text-color;
+ text-transform: uppercase;
+ margin-bottom: 5px;
}
textarea {
display: block;
box-sizing: border-box;
- width: 100%;
- margin: 0;
- margin-top: 5px;
- color: $inverted-text-color;
- background: $simple-background-color;
+ width: calc(100% + 20px);
+ color: $secondary-text-color;
+ background: transparent;
padding: 10px;
+ margin: 0 -10px;
font-family: inherit;
font-size: 14px;
resize: none;
border: 0;
outline: 0;
border-radius: 4px;
+
+ &::placeholder {
+ color: $dark-text-color;
+ opacity: 1;
+ }
+
+ &:focus {
+ background: $ui-base-color;
+ }
}
}
}
diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb
index e295fb8477..c2f3c9a112 100644
--- a/app/serializers/rest/relationship_serializer.rb
+++ b/app/serializers/rest/relationship_serializer.rb
@@ -52,6 +52,6 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
end
def note
- (instance_options[:relationships].account_note[object.id] || {})[:comment]
+ (instance_options[:relationships].account_note[object.id] || {})[:comment] || ''
end
end