diff --git a/app/soapbox/actions/account_notes.js b/app/soapbox/actions/account_notes.js new file mode 100644 index 000000000..d6aeefc49 --- /dev/null +++ b/app/soapbox/actions/account_notes.js @@ -0,0 +1,67 @@ +import api from '../api'; + +import { openModal, closeModal } from './modals'; + +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_MODAL = 'ACCOUNT_NOTE_INIT_MODAL'; + +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(closeModal()); + 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 initAccountNoteModal(account) { + return (dispatch, getState) => { + const comment = getState().getIn(['relationships', account.get('id'), 'note']); + + dispatch({ + type: ACCOUNT_NOTE_INIT_MODAL, + account, + comment, + }); + + dispatch(openModal('ACCOUNT_NOTE')); + }; +} + +export function changeAccountNoteComment(comment) { + return { + type: ACCOUNT_NOTE_CHANGE_COMMENT, + comment, + }; +} \ No newline at end of file diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 884d57034..d639838aa 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -62,6 +62,8 @@ const messages = defineMessages({ mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, + createNote: { id: 'account.create_note', defaultMessage: 'Create a note' }, + editNote: { id: 'account.edit_note', defaultMessage: 'Edit a note' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, deactivateUser: { id: 'admin.users.actions.deactivate_user', defaultMessage: 'Deactivate @{name}' }, @@ -268,6 +270,14 @@ class Header extends ImmutablePureComponent { }); } + if (features.notes) { + menu.push({ + text: intl.formatMessage(account.getIn(['relationship', 'note']) ? messages.editNote : messages.createNote), + action: this.props.onShowNote, + icon: require('@tabler/icons/icons/note.svg'), + }); + } + if (account.getIn(['relationship', 'following'])) { if (account.getIn(['relationship', 'showing_reblogs'])) { menu.push({ diff --git a/app/soapbox/features/account_timeline/components/header.js b/app/soapbox/features/account_timeline/components/header.js index 51120dfcf..588697943 100644 --- a/app/soapbox/features/account_timeline/components/header.js +++ b/app/soapbox/features/account_timeline/components/header.js @@ -131,6 +131,10 @@ export default class Header extends ImmutablePureComponent { this.props.onUnsuggestUser(this.props.account); } + handleShowNote = () => { + this.props.onShowNote(this.props.account); + } + render() { const { account, identity_proofs } = this.props; const moved = (account) ? account.get('moved') : false; @@ -165,6 +169,7 @@ export default class Header extends ImmutablePureComponent { onDemoteToUser={this.handleDemoteToUser} onSuggestUser={this.handleSuggestUser} onUnsuggestUser={this.handleUnsuggestUser} + onShowNote={this.handleShowNote} username={this.props.username} /> diff --git a/app/soapbox/features/account_timeline/containers/header_container.js b/app/soapbox/features/account_timeline/containers/header_container.js index cec3c29fe..6087217ca 100644 --- a/app/soapbox/features/account_timeline/containers/header_container.js +++ b/app/soapbox/features/account_timeline/containers/header_container.js @@ -3,21 +3,7 @@ import React from 'react'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { - verifyUser, - unverifyUser, - promoteToAdmin, - promoteToModerator, - demoteToUser, - suggestUsers, - unsuggestUsers, -} from 'soapbox/actions/admin'; -import { launchChat } from 'soapbox/actions/chats'; -import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation'; -import { getSettings } from 'soapbox/actions/settings'; -import snackbar from 'soapbox/actions/snackbar'; -import { isAdmin } from 'soapbox/utils/accounts'; - +import { initAccountNoteModal } from 'soapbox/actions/account_notes'; import { followAccount, unfollowAccount, @@ -28,16 +14,31 @@ import { unpinAccount, subscribeAccount, unsubscribeAccount, -} from '../../../actions/accounts'; +} from 'soapbox/actions/accounts'; +import { + verifyUser, + unverifyUser, + promoteToAdmin, + promoteToModerator, + demoteToUser, + suggestUsers, + unsuggestUsers, +} from 'soapbox/actions/admin'; +import { launchChat } from 'soapbox/actions/chats'; import { mentionCompose, directCompose, -} from '../../../actions/compose'; -import { blockDomain, unblockDomain } from '../../../actions/domain_blocks'; -import { openModal } from '../../../actions/modals'; -import { initMuteModal } from '../../../actions/mutes'; -import { initReport } from '../../../actions/reports'; -import { makeGetAccount } from '../../../selectors'; +} from 'soapbox/actions/compose'; +import { blockDomain, unblockDomain } from 'soapbox/actions/domain_blocks'; +import { openModal } from 'soapbox/actions/modals'; +import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation'; +import { initMuteModal } from 'soapbox/actions/mutes'; +import { initReport } from 'soapbox/actions/reports'; +import { getSettings } from 'soapbox/actions/settings'; +import snackbar from 'soapbox/actions/snackbar'; +import { makeGetAccount } from 'soapbox/selectors'; +import { isAdmin } from 'soapbox/utils/accounts'; + import Header from '../components/header'; const messages = defineMessages({ @@ -246,6 +247,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ .then(() => dispatch(snackbar.success(message))) .catch(() => {}); }, + + onShowNote(account) { + dispatch(initAccountNoteModal(account)); + }, }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); diff --git a/app/soapbox/features/ui/components/account_note_modal.js b/app/soapbox/features/ui/components/account_note_modal.js new file mode 100644 index 000000000..4fa61f438 --- /dev/null +++ b/app/soapbox/features/ui/components/account_note_modal.js @@ -0,0 +1,109 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { connect } from 'react-redux'; + +import { changeAccountNoteComment, submitAccountNote } from 'soapbox/actions/account_notes'; +import { closeModal } from 'soapbox/actions/modals'; +import Button from 'soapbox/components/button'; +import Icon from 'soapbox/components/icon'; +import { makeGetAccount } from 'soapbox/selectors'; + + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, + placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' }, + save: { id: 'account_note.save', defaultMessage: 'Save' }, +}); + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = state => ({ + isSubmitting: state.getIn(['account_notes', 'edit', 'isSubmitting']), + account: getAccount(state, state.getIn(['account_notes', 'edit', 'account_id'])), + comment: state.getIn(['account_notes', 'edit', 'comment']), + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = dispatch => { + return { + onConfirm() { + dispatch(submitAccountNote()); + }, + + onClose() { + dispatch(closeModal()); + }, + + onCommentChange(comment) { + dispatch(changeAccountNoteComment(comment)); + }, + }; +}; + +export default @connect(makeMapStateToProps, mapDispatchToProps) +@injectIntl +class AccountNoteModal extends React.PureComponent { + + static propTypes = { + isSubmitting: PropTypes.bool, + account: PropTypes.object.isRequired, + onClose: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + onCommentChange: PropTypes.func.isRequired, + comment: PropTypes.string, + intl: PropTypes.object.isRequired, + }; + + handleCommentChange = e => { + this.props.onCommentChange(e.target.value); + } + + handleSubmit = () => { + this.props.onConfirm(); + } + + handleKeyDown = e => { + if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { + this.handleSubmit(); + } + } + + + render() { + const { account, isSubmitting, comment, onClose, intl } = this.props; + + return ( +
+
+ + +
+ +
+

+ +