--- /dev/null
+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,
+ };
+};
--- /dev/null
+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 = (
+ <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>
+ );
+ }
+
+ let note_container = null;
+ if (isEditing) {
+ note_container = (
+ <Textarea
+ className='account__header__account-note__content'
+ disabled={isSubmitting}
+ placeholder={intl.formatMessage(messages.placeholder)}
+ value={accountNote}
+ onChange={this.handleChangeAccountNote}
+ onKeyDown={this.handleKeyDown}
+ autoFocus
+ />
+ );
+ } 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>
+ );
+ }
+
+}
import { shortNumberFormat } from 'flavours/glitch/util/numbers';
import { NavLink } from 'react-router-dom';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
+import AccountNoteContainer from '../containers/account_note_container';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
+ add_account_note: { id: 'account.add_account_note', defaultMessage: 'Add note for @{name}' },
});
const dateFormatOptions = {
identity_props: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
+ onEditAccountNote: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired,
};
return null;
}
+ const accountNote = account.getIn(['relationship', 'note']);
+
let info = [];
let actionBtn = '';
let lockedIcon = '';
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) {
if (profileLink) menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink });
if (preferencesLink) menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink });
</h1>
</div>
+ <AccountNoteContainer account={account} />
+
<div className='account__header__extra'>
<div className='account__header__bio'>
{ (fields.size > 0 || identity_proofs.size > 0) && (
--- /dev/null
+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);
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
+ onEditAccountNote: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired,
};
this.props.onAddToList(this.props.account);
}
+ handleEditAccountNote = () => {
+ this.props.onEditAccountNote(this.props.account);
+ }
+
render () {
const { account, hideTabs, identity_proofs } = this.props;
onUnblockDomain={this.handleUnblockDomain}
onEndorseToggle={this.handleEndorseToggle}
onAddToList={this.handleAddToList}
+ onEditAccountNote={this.handleEditAccountNote}
domain={this.props.domain}
/>
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';
}
},
+ 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.' values={{ domain: <strong>{domain}</strong> }} />,
--- /dev/null
+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;
+ }
+}
import trends from './trends';
import announcements from './announcements';
import markers from './markers';
+import account_notes from './account_notes';
const reducers = {
announcements,
polls,
trends,
markers,
+ account_notes,
};
export default combineReducers(reducers);
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));
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);
color: $primary-text-color;
margin-bottom: 4px;
display: block;
- vertical-align: top;
background-color: $base-overlay-background;
text-transform: uppercase;
font-size: 11px;
}
}
}
+
+ &__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;
+ }
+ }
}