diff options
10 files changed, 339 insertions, 1 deletions
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 000000000..c1cce3193 --- /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 000000000..e7fd4c5ff --- /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 = ( + <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> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index c7b54649c..a5abf38ae 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -12,6 +12,7 @@ import Button from 'flavours/glitch/components/button'; 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' }, @@ -46,6 +47,7 @@ const messages = defineMessages({ 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 = { @@ -65,6 +67,7 @@ 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, }; @@ -121,6 +124,8 @@ class Header extends ImmutablePureComponent { return null; } + const accountNote = account.getIn(['relationship', 'note']); + let info = []; let actionBtn = ''; let lockedIcon = ''; @@ -172,6 +177,10 @@ 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) { if (profileLink) menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink }); if (preferencesLink) menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink }); @@ -278,6 +287,8 @@ class Header extends ImmutablePureComponent { </h1> </div> + <AccountNoteContainer account={account} /> + <div className='account__header__extra'> <div className='account__header__bio'> { (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 000000000..f1d007ecb --- /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 0faa8a424..1bab05c72 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 fff5e097f..225910292 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: <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> }} />, 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 000000000..b1cf2e0aa --- /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 852abe9dd..cadbd01a3 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 4652bbc14..dcaeefcae 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 e0239ff79..774254a4c 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; + } + } } |