diff --git a/app/javascript/flavours/glitch/actions/account_notes.js b/app/javascript/flavours/glitch/actions/account_notes.js
new file mode 100644
index 0000000000..c1cce31939
--- /dev/null
+++ b/app/javascript/flavours/glitch/actions/account_notes.js
@@ -0,0 +1,69 @@
+import api from 'flavours/glitch/util/api';
+
+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() {
+ 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']),
+ }).then(response => {
+ dispatch(submitAccountNoteSuccess(response.data));
+ }).catch(error => dispatch(submitAccountNoteFail(error)));
+ };
+};
+
+export function submitAccountNoteRequest() {
+ return {
+ type: ACCOUNT_NOTE_SUBMIT_REQUEST,
+ };
+};
+
+export function submitAccountNoteSuccess(relationship) {
+ return {
+ type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
+ relationship,
+ };
+};
+
+export function submitAccountNoteFail(error) {
+ return {
+ type: ACCOUNT_NOTE_SUBMIT_FAIL,
+ 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/flavours/glitch/features/account/components/account_note.js b/app/javascript/flavours/glitch/features/account/components/account_note.js
new file mode 100644
index 0000000000..e7fd4c5ff2
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/account/components/account_note.js
@@ -0,0 +1,103 @@
+import React from 'react';
+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 'flavours/glitch/components/icon';
+import Textarea from 'react-textarea-autosize';
+
+const messages = defineMessages({
+ placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' },
+});
+
+export default @injectIntl
+class Header 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,
+ intl: PropTypes.object.isRequired,
+ };
+
+ handleChangeAccountNote = (e) => {
+ this.props.onChangeAccountNote(e.target.value);
+ };
+
+ componentWillUnmount () {
+ if (this.props.isEditing) {
+ this.props.onCancelAccountNote();
+ }
+ }
+
+ handleKeyDown = e => {
+ if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
+ this.props.onSaveAccountNote();
+ } else if (e.keyCode === 27) {
+ this.props.onCancelAccountNote();
+ }
+ }
+
+ render () {
+ const { account, accountNote, isEditing, isSubmitting, intl } = this.props;
+
+ if (!account || (!accountNote && !isEditing)) {
+ return null;
+ }
+
+ let action_buttons = null;
+ if (isEditing) {
+ action_buttons = (
+
{ (fields.size > 0 || identity_proofs.size > 0) && (
diff --git a/app/javascript/flavours/glitch/features/account/containers/account_note_container.js b/app/javascript/flavours/glitch/features/account/containers/account_note_container.js
new file mode 100644
index 0000000000..f1d007ecb0
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/account/containers/account_note_container.js
@@ -0,0 +1,34 @@
+import { connect } from 'react-redux';
+import { changeAccountNoteComment, submitAccountNote, initEditAccountNote, cancelAccountNote } from 'flavours/glitch/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 mapDispatchToProps = (dispatch, { account }) => ({
+
+ onEditAccountNote() {
+ dispatch(initEditAccountNote(account));
+ },
+
+ onSaveAccountNote() {
+ dispatch(submitAccountNote());
+ },
+
+ onCancelAccountNote() {
+ dispatch(cancelAccountNote());
+ },
+
+ onChangeAccountNote(comment) {
+ dispatch(changeAccountNoteComment(comment));
+ },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(AccountNote);
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
index 0faa8a4245..1bab05c721 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
@@ -24,6 +24,7 @@ export default class Header extends ImmutablePureComponent {
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
+ onEditAccountNote: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired,
};
@@ -84,6 +85,10 @@ export default class Header extends ImmutablePureComponent {
this.props.onAddToList(this.props.account);
}
+ handleEditAccountNote = () => {
+ this.props.onEditAccountNote(this.props.account);
+ }
+
render () {
const { account, hideTabs, identity_proofs } = this.props;
@@ -109,6 +114,7 @@ export default class Header extends ImmutablePureComponent {
onUnblockDomain={this.handleUnblockDomain}
onEndorseToggle={this.handleEndorseToggle}
onAddToList={this.handleAddToList}
+ onEditAccountNote={this.handleEditAccountNote}
domain={this.props.domain}
/>
diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
index fff5e097f7..2259102928 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
@@ -19,6 +19,7 @@ import { initBlockModal } from 'flavours/glitch/actions/blocks';
import { initReport } from 'flavours/glitch/actions/reports';
import { openModal } from 'flavours/glitch/actions/modal';
import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks';
+import { initEditAccountNote } from 'flavours/glitch/actions/account_notes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { unfollowModal } from 'flavours/glitch/util/initial_state';
import { List as ImmutableList } from 'immutable';
@@ -106,6 +107,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
+ onEditAccountNote (account) {
+ dispatch(initEditAccountNote(account));
+ },
+
onBlockDomain (domain) {
dispatch(openModal('CONFIRM', {
message: {domain} }} />,
diff --git a/app/javascript/flavours/glitch/reducers/account_notes.js b/app/javascript/flavours/glitch/reducers/account_notes.js
new file mode 100644
index 0000000000..b1cf2e0aa8
--- /dev/null
+++ b/app/javascript/flavours/glitch/reducers/account_notes.js
@@ -0,0 +1,44 @@
+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/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js
index 852abe9dda..cadbd01a3b 100644
--- a/app/javascript/flavours/glitch/reducers/index.js
+++ b/app/javascript/flavours/glitch/reducers/index.js
@@ -37,6 +37,7 @@ import identity_proofs from './identity_proofs';
import trends from './trends';
import announcements from './announcements';
import markers from './markers';
+import account_notes from './account_notes';
const reducers = {
announcements,
@@ -77,6 +78,7 @@ const reducers = {
polls,
trends,
markers,
+ account_notes,
};
export default combineReducers(reducers);
diff --git a/app/javascript/flavours/glitch/reducers/relationships.js b/app/javascript/flavours/glitch/reducers/relationships.js
index 4652bbc14c..dcaeefcae8 100644
--- a/app/javascript/flavours/glitch/reducers/relationships.js
+++ b/app/javascript/flavours/glitch/reducers/relationships.js
@@ -13,6 +13,9 @@ import {
DOMAIN_BLOCK_SUCCESS,
DOMAIN_UNBLOCK_SUCCESS,
} from 'flavours/glitch/actions/domain_blocks';
+import {
+ ACCOUNT_NOTE_SUBMIT_SUCCESS,
+} from 'flavours/glitch/actions/account_notes';
import { Map as ImmutableMap, fromJS } from 'immutable';
const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship));
@@ -45,6 +48,7 @@ export default function relationships(state = initialState, action) {
case ACCOUNT_UNMUTE_SUCCESS:
case ACCOUNT_PIN_SUCCESS:
case ACCOUNT_UNPIN_SUCCESS:
+ case ACCOUNT_NOTE_SUBMIT_SUCCESS:
return normalizeRelationship(state, action.relationship);
case RELATIONSHIPS_FETCH_SUCCESS:
return normalizeRelationships(state, action.relationships);
diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss
index e0239ff79d..774254a4c1 100644
--- a/app/javascript/flavours/glitch/styles/components/accounts.scss
+++ b/app/javascript/flavours/glitch/styles/components/accounts.scss
@@ -379,7 +379,6 @@
color: $primary-text-color;
margin-bottom: 4px;
display: block;
- vertical-align: top;
background-color: $base-overlay-background;
text-transform: uppercase;
font-size: 11px;
@@ -713,4 +712,65 @@
}
}
}
+
+ &__account-note {
+ margin: 5px;
+ padding: 10px;
+ background: $ui-highlight-color;
+ color: $primary-text-color;
+ display: flex;
+ flex-direction: column;
+ border-radius: 4px;
+ font-size: 14px;
+ font-weight: 400;
+
+ &__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;
+ }
+ }
+
+ strong {
+ font-size: 15px;
+ font-weight: 500;
+ }
+
+ button:hover span {
+ text-decoration: underline;
+ }
+
+ textarea {
+ display: block;
+ box-sizing: border-box;
+ width: 100%;
+ margin: 0;
+ margin-top: 5px;
+ color: $inverted-text-color;
+ background: $simple-background-color;
+ padding: 10px;
+ font-family: inherit;
+ font-size: 14px;
+ resize: none;
+ border: 0;
+ outline: 0;
+ border-radius: 4px;
+ }
+ }
}