diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js index 86af6f5c1..4eca355d4 100644 --- a/app/soapbox/actions/compose.js +++ b/app/soapbox/actions/compose.js @@ -27,6 +27,8 @@ export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS'; export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL'; export const COMPOSE_REPLY = 'COMPOSE_REPLY'; export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; +export const COMPOSE_QUOTE = 'COMPOSE_QUOTE'; +export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL'; export const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_RESET = 'COMPOSE_RESET'; @@ -119,6 +121,29 @@ export function cancelReplyCompose() { }; } +export function quoteCompose(status, routerHistory) { + return (dispatch, getState) => { + const state = getState(); + const instance = state.get('instance'); + const { explicitAddressing } = getFeatures(instance); + + dispatch({ + type: COMPOSE_QUOTE, + status: status, + account: state.getIn(['accounts', state.get('me')]), + explicitAddressing, + }); + + dispatch(openModal('COMPOSE')); + }; +} + +export function cancelQuoteCompose() { + return { + type: COMPOSE_QUOTE_CANCEL, + }; +} + export function resetCompose() { return { type: COMPOSE_RESET, @@ -226,6 +251,7 @@ export function submitCompose(routerHistory, force = false) { const params = { status, in_reply_to_id: state.getIn(['compose', 'in_reply_to'], null), + quote_id: state.getIn(['compose', 'quote'], null), media_ids: media.map(item => item.get('id')), sensitive: state.getIn(['compose', 'sensitive']), spoiler_text: state.getIn(['compose', 'spoiler_text'], ''), diff --git a/app/soapbox/components/modal_root.js b/app/soapbox/components/modal_root.js index 220d22f61..3b622e3e0 100644 --- a/app/soapbox/components/modal_root.js +++ b/app/soapbox/components/modal_root.js @@ -18,6 +18,7 @@ const checkComposeContent = compose => { compose.get('spoiler_text').length > 0, compose.get('media_attachments').size > 0, compose.get('in_reply_to') !== null, + compose.get('quote') !== null, compose.get('poll') !== null, ].some(check => check === true); }; diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index 5637061e2..06285a8f4 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -10,7 +10,7 @@ import { Link, NavLink } from 'react-router-dom'; import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; import Icon from 'soapbox/components/icon'; import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card'; -import QuotedStatus from 'soapbox/features/status/components/quoted_status'; +import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; import { getDomain } from 'soapbox/utils/accounts'; import Card from '../features/status/components/card'; @@ -71,6 +71,7 @@ class Status extends ImmutablePureComponent { onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, + onQuote: PropTypes.func, onDelete: PropTypes.func, onDirect: PropTypes.func, onChat: PropTypes.func, diff --git a/app/soapbox/components/status_action_bar.js b/app/soapbox/components/status_action_bar.js index 92e700820..327faf53d 100644 --- a/app/soapbox/components/status_action_bar.js +++ b/app/soapbox/components/status_action_bar.js @@ -78,6 +78,7 @@ class StatusActionBar extends ImmutablePureComponent { onFavourite: PropTypes.func, onBookmark: PropTypes.func, onReblog: PropTypes.func, + onQuote: PropTypes.func, onDelete: PropTypes.func, onDirect: PropTypes.func, onChat: PropTypes.func, @@ -203,6 +204,15 @@ class StatusActionBar extends ImmutablePureComponent { } } + handleQuoteClick = () => { + const { me, onQuote, onOpenUnauthorizedModal, status } = this.props; + if (me) { + onQuote(status, this.context.router.history); + } else { + onOpenUnauthorizedModal('REBLOG'); + } + } + handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); } @@ -557,6 +567,9 @@ class StatusActionBar extends ImmutablePureComponent { {reblogCount !== 0 && {reblogCount}} +
+ +
{ }); }, + onQuote(status, router) { + dispatch((_, getState) => { + const state = getState(); + if (state.getIn(['compose', 'text']).trim().length !== 0) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(quoteCompose(status, router)), + })); + } else { + dispatch(quoteCompose(status, router)); + } + }); + }, + onFavourite(status) { if (status.get('favourited')) { dispatch(unfavourite(status)); diff --git a/app/soapbox/features/compose/components/compose_form.js b/app/soapbox/features/compose/components/compose_form.js index 1cda4063e..d509be69e 100644 --- a/app/soapbox/features/compose/components/compose_form.js +++ b/app/soapbox/features/compose/components/compose_form.js @@ -21,6 +21,7 @@ import MarkdownButtonContainer from '../containers/markdown_button_container'; import PollButtonContainer from '../containers/poll_button_container'; import PollFormContainer from '../containers/poll_form_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; +import QuotedStatusContainer from '../containers/quoted_status_container'; import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import ReplyMentions from '../containers/reply_mentions_container'; import ScheduleButtonContainer from '../containers/schedule_button_container'; @@ -361,6 +362,8 @@ export default class ComposeForm extends ImmutablePureComponent { } + +
diff --git a/app/soapbox/features/compose/containers/quoted_status_container.js b/app/soapbox/features/compose/containers/quoted_status_container.js new file mode 100644 index 000000000..907a4caa3 --- /dev/null +++ b/app/soapbox/features/compose/containers/quoted_status_container.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; + +import QuotedStatus from 'soapbox/features/status/components/quoted_status'; +import { makeGetStatus } from 'soapbox/selectors'; + +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = state => ({ + status: getStatus(state, { id: state.getIn(['compose', 'quote']) }), + }); + + return mapStateToProps; +}; + +export default connect(makeMapStateToProps)(QuotedStatus); \ No newline at end of file diff --git a/app/soapbox/features/status/components/detailed_status.js b/app/soapbox/features/status/components/detailed_status.js index f45e3e994..4b3a0ff42 100644 --- a/app/soapbox/features/status/components/detailed_status.js +++ b/app/soapbox/features/status/components/detailed_status.js @@ -9,7 +9,7 @@ import { Link, NavLink } from 'react-router-dom'; import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; import Icon from 'soapbox/components/icon'; -import QuotedStatus from 'soapbox/features/status/components/quoted_status'; +import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; import { getDomain } from 'soapbox/utils/accounts'; import Avatar from '../../../components/avatar'; diff --git a/app/soapbox/features/status/components/quoted_status.js b/app/soapbox/features/status/components/quoted_status.js index c62e287f8..f7878adcf 100644 --- a/app/soapbox/features/status/components/quoted_status.js +++ b/app/soapbox/features/status/components/quoted_status.js @@ -3,7 +3,6 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; import { NavLink } from 'react-router-dom'; import AttachmentThumbs from 'soapbox/components/attachment_thumbs'; @@ -11,20 +10,8 @@ import Avatar from 'soapbox/components/avatar'; import DisplayName from 'soapbox/components/display_name'; import RelativeTimestamp from 'soapbox/components/relative_timestamp'; import { isRtl } from 'soapbox/rtl'; -import { makeGetStatus } from 'soapbox/selectors'; -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - - const mapStateToProps = (state, props) => ({ - status: getStatus(state, { id: props.statusId }), - }); - - return mapStateToProps; -}; - -export default @connect(makeMapStateToProps) -@injectIntl +export default @injectIntl class QuotedStatus extends ImmutablePureComponent { static contextTypes = { diff --git a/app/soapbox/features/status/containers/quoted_status_container.js b/app/soapbox/features/status/containers/quoted_status_container.js new file mode 100644 index 000000000..99b7763c2 --- /dev/null +++ b/app/soapbox/features/status/containers/quoted_status_container.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; + +import { makeGetStatus } from 'soapbox/selectors'; + +import QuotedStatus from '../components/quoted_status'; + +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = (state, props) => ({ + status: getStatus(state, { id: props.statusId }), + }); + + return mapStateToProps; +}; + +export default connect(makeMapStateToProps)(QuotedStatus); \ No newline at end of file diff --git a/app/soapbox/features/status/index.js b/app/soapbox/features/status/index.js index e7839213a..eb6c670c7 100644 --- a/app/soapbox/features/status/index.js +++ b/app/soapbox/features/status/index.js @@ -28,6 +28,7 @@ import { replyCompose, mentionCompose, directCompose, + quoteCompose, } from '../../actions/compose'; import { simpleEmojiReact } from '../../actions/emoji_reacts'; import { @@ -258,6 +259,19 @@ class Status extends ImmutablePureComponent { }); } + handleQuoteClick = (status, e) => { + const { askReplyConfirmation, dispatch, intl } = this.props; + if (askReplyConfirmation) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(quoteCompose(status, this.context.router.history)), + })); + } else { + dispatch(quoteCompose(status, this.context.router.history)); + } + } + handleDeleteClick = (status, history, withRedraft = false) => { const { dispatch, intl } = this.props; @@ -681,6 +695,7 @@ class Status extends ImmutablePureComponent { onFavourite={this.handleFavouriteClick} onEmojiReact={this.handleEmojiReactClick} onReblog={this.handleReblogClick} + onQuote={this.handleQuoteClick} onDelete={this.handleDeleteClick} onDirect={this.handleDirectClick} onChat={this.handleChatClick} diff --git a/app/soapbox/features/ui/components/compose_modal.js b/app/soapbox/features/ui/components/compose_modal.js index 5503735cb..6c76ee57f 100644 --- a/app/soapbox/features/ui/components/compose_modal.js +++ b/app/soapbox/features/ui/components/compose_modal.js @@ -23,6 +23,7 @@ const mapStateToProps = state => { composeText: state.getIn(['compose', 'text']), privacy: state.getIn(['compose', 'privacy']), inReplyTo: state.getIn(['compose', 'in_reply_to']), + quote: state.getIn(['compose', 'quote']), }; }; @@ -35,6 +36,7 @@ class ComposeModal extends ImmutablePureComponent { composeText: PropTypes.string, privacy: PropTypes.string, inReplyTo: PropTypes.string, + quote: PropTypes.string, dispatch: PropTypes.func.isRequired, }; @@ -56,12 +58,14 @@ class ComposeModal extends ImmutablePureComponent { }; renderTitle = () => { - const { privacy, inReplyTo } = this.props; + const { privacy, inReplyTo, quote } = this.props; if (privacy === 'direct') { return ; } else if (inReplyTo) { return ; + } else if (quote) { + return ; } else { return ; } diff --git a/app/soapbox/reducers/compose.js b/app/soapbox/reducers/compose.js index b11a62020..29b73aefe 100644 --- a/app/soapbox/reducers/compose.js +++ b/app/soapbox/reducers/compose.js @@ -8,6 +8,8 @@ import { COMPOSE_CHANGE, COMPOSE_REPLY, COMPOSE_REPLY_CANCEL, + COMPOSE_QUOTE, + COMPOSE_QUOTE_CANCEL, COMPOSE_DIRECT, COMPOSE_MENTION, COMPOSE_SUBMIT_REQUEST, @@ -65,6 +67,7 @@ const initialState = ImmutableMap({ focusDate: null, caretPosition: null, in_reply_to: null, + quote: null, is_composing: false, is_submitting: false, is_changing_upload: false, @@ -128,6 +131,7 @@ function clearAll(state) { map.set('is_submitting', false); map.set('is_changing_upload', false); map.set('in_reply_to', null); + map.set('quote', null); map.set('privacy', state.get('default_privacy')); map.set('sensitive', false); map.set('media_attachments', ImmutableList()); @@ -332,6 +336,25 @@ export default function compose(state = initialState, action) { map.set('idempotencyKey', uuid()); map.set('content_type', state.get('default_content_type')); + if (action.status.get('spoiler_text', '').length > 0) { + map.set('spoiler', true); + map.set('spoiler_text', action.status.get('spoiler_text')); + } else { + map.set('spoiler', false); + map.set('spoiler_text', ''); + } + }); + case COMPOSE_QUOTE: + return state.withMutations(map => { + map.set('quote', action.status.get('id')); + map.set('to', action.explicitAddressing ? statusToMentionsArray(state, action.status, action.account) : undefined); + map.set('text', !action.explicitAddressing ? statusToTextMentions(state, action.status, action.account) : ''); + map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); + map.set('focusDate', new Date()); + map.set('caretPosition', null); + map.set('idempotencyKey', uuid()); + map.set('content_type', state.get('default_content_type')); + if (action.status.get('spoiler_text', '').length > 0) { map.set('spoiler', true); map.set('spoiler_text', action.status.get('spoiler_text')); @@ -345,6 +368,7 @@ export default function compose(state = initialState, action) { case COMPOSE_UPLOAD_CHANGE_REQUEST: return state.set('is_changing_upload', true); case COMPOSE_REPLY_CANCEL: + case COMPOSE_QUOTE_CANCEL: case COMPOSE_RESET: case COMPOSE_SUBMIT_SUCCESS: return clearAll(state); @@ -390,6 +414,8 @@ export default function compose(state = initialState, action) { case TIMELINE_DELETE: if (action.id === state.get('in_reply_to')) { return state.set('in_reply_to', null); + } if (action.id === state.get('quote')) { + return state.set('quote', null); } else { return state; } diff --git a/app/soapbox/reducers/search.js b/app/soapbox/reducers/search.js index 1d3b52432..86e4222a5 100644 --- a/app/soapbox/reducers/search.js +++ b/app/soapbox/reducers/search.js @@ -4,6 +4,7 @@ import { COMPOSE_MENTION, COMPOSE_REPLY, COMPOSE_DIRECT, + COMPOSE_QUOTE, } from '../actions/compose'; import { SEARCH_CHANGE, @@ -78,6 +79,7 @@ export default function search(state = initialState, action) { case COMPOSE_REPLY: case COMPOSE_MENTION: case COMPOSE_DIRECT: + case COMPOSE_QUOTE: return state.set('hidden', true); case SEARCH_FETCH_REQUEST: return handleSubmitted(state, action.value); diff --git a/app/styles/components/reply-mentions.scss b/app/styles/components/reply-mentions.scss index 9eebb19d0..cd693803e 100644 --- a/app/styles/components/reply-mentions.scss +++ b/app/styles/components/reply-mentions.scss @@ -16,7 +16,8 @@ } .status__wrapper, -.detailed-status { +.detailed-status, +.quoted-status { .reply-mentions { display: block; margin: 4px 0 0 0;