From c0bee9be3c6c9cc2c9e095d2419e6f5822b7353b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 23 Jan 2022 17:55:03 +0100 Subject: [PATCH 01/16] Display quoted posts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/importer/index.js | 4 + app/soapbox/components/status.js | 8 ++ .../status/components/detailed_status.js | 8 ++ .../status/components/quoted_status.js | 132 ++++++++++++++++++ app/styles/components/status.scss | 62 ++++++++ app/styles/ui.scss | 3 +- 6 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 app/soapbox/features/status/components/quoted_status.js diff --git a/app/soapbox/actions/importer/index.js b/app/soapbox/actions/importer/index.js index 407bb7c30..62e9e7860 100644 --- a/app/soapbox/actions/importer/index.js +++ b/app/soapbox/actions/importer/index.js @@ -75,6 +75,10 @@ export function importFetchedStatus(status, idempotencyKey) { dispatch(importFetchedStatus(status.reblog)); } + if (status.pleroma && status.pleroma.quote && status.pleroma.quote.id) { + dispatch(importFetchedStatus(status.pleroma.quote)); + } + if (status.poll && status.poll.id) { dispatch(importFetchedPoll(status.poll)); } diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index 2f5e12705..5637061e2 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -10,6 +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 { getDomain } from 'soapbox/utils/accounts'; import Card from '../features/status/components/card'; @@ -472,6 +473,12 @@ class Status extends ImmutablePureComponent { ); } + let quote; + + if (status.getIn(['pleroma', 'quote'])) { + quote = ; + } + if (otherAccounts && otherAccounts.size > 1) { statusAvatar = ; } else if (account === undefined || account === null) { @@ -551,6 +558,7 @@ class Status extends ImmutablePureComponent { {media} {poll} + {quote} ; } + let quote; + + if (status.getIn(['pleroma', 'quote'])) { + quote = ; + } + if (status.get('visibility') === 'direct') { statusTypeIcon = ; } else if (status.get('visibility') === 'private') { @@ -201,6 +208,7 @@ class DetailedStatus extends ImmutablePureComponent { /> {media} + {quote}
diff --git a/app/soapbox/features/status/components/quoted_status.js b/app/soapbox/features/status/components/quoted_status.js new file mode 100644 index 000000000..c62e287f8 --- /dev/null +++ b/app/soapbox/features/status/components/quoted_status.js @@ -0,0 +1,132 @@ +import PropTypes from 'prop-types'; +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'; +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 +class QuotedStatus extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + status: ImmutablePropTypes.map, + }; + + handleExpandClick = (e) => { + if (e.button === 0) { + if (!this.context.router) { + return; + } + + this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/posts/${this.props.status.get('id')}`); + } + } + + renderReplyMentions = () => { + const { status } = this.props; + + if (!status.get('in_reply_to_id')) { + return null; + } + + const to = status.get('mentions', []); + + if (to.size === 0) { + if (status.get('in_reply_to_account_id') === status.getIn(['account', 'id'])) { + return ( +
+ +
+ ); + } else { + return ( +
+ +
+ ); + } + } + + return ( +
+ `@${account.get('username')} `), + more: to.size > 2 && , + }} + /> +
+ ); + } + + render() { + const { status } = this.props; + + if (!status) { + return null; + } + + const content = { __html: status.get('contentHtml') }; + const style = { + direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr', + }; + + return ( +
+
+
+ +
+ +
+ +
+
+ + {this.renderReplyMentions()} + +
+ + {status.get('media_attachments').size > 0 && ( + + )} +
+ ); + } + +} diff --git a/app/styles/components/status.scss b/app/styles/components/status.scss index c8fd1e55b..b3c982554 100644 --- a/app/styles/components/status.scss +++ b/app/styles/components/status.scss @@ -746,3 +746,65 @@ a.status-card.compact:hover { .pending-status { opacity: 0.5; } + +.quoted-status { + margin-top: 14px; + border: 1px solid var(--brand-color--med); + border-radius: 10px; + padding: 12px; + overflow-y: auto; + flex: 0 2 auto; + transition: background 0.2s; + cursor: pointer; + + &:hover, + &:focus, + &:active { + background: var(--brand-color--faint); + } + + &__relative-time { + padding-top: 4px; + } + + &__display-name { + color: var(--primary-text-color); + display: block; + max-width: 100%; + line-height: 24px; + overflow: hidden; + padding-right: 25px; + text-decoration: none; + + .display-name__account { + color: var(--primary-text-color--faint); + } + } + + &__display-avatar { + float: left; + margin-right: 5px; + } + + .reply-mentions { + font-size: 14px; + } + + &__content { + margin-top: 5px; + font-size: 14px; + + a { + color: var(--highlight-text-color); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + + .attachment-thumbs .media-gallery { + margin-top: 5px !important; + } +} diff --git a/app/styles/ui.scss b/app/styles/ui.scss index a594752e5..de2ce5e3b 100644 --- a/app/styles/ui.scss +++ b/app/styles/ui.scss @@ -221,7 +221,8 @@ } .status__relative-time, -.notification__relative_time { +.notification__relative_time, +.quoted-status__relative-time { color: var(--primary-text-color--faint); float: right; font-size: 14px; From 15d4910db53fd977e2bbdae0aa39ddcbd4d6cecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 23 Jan 2022 18:44:17 +0100 Subject: [PATCH 02/16] wip quote post composing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/compose.js | 26 +++++++++++++++++++ app/soapbox/components/modal_root.js | 1 + app/soapbox/components/status.js | 3 ++- app/soapbox/components/status_action_bar.js | 13 ++++++++++ app/soapbox/containers/status_container.js | 16 ++++++++++++ .../compose/components/compose_form.js | 3 +++ .../containers/quoted_status_container.js | 16 ++++++++++++ .../status/components/detailed_status.js | 2 +- .../status/components/quoted_status.js | 15 +---------- .../containers/quoted_status_container.js | 17 ++++++++++++ app/soapbox/features/status/index.js | 15 +++++++++++ .../features/ui/components/compose_modal.js | 6 ++++- app/soapbox/reducers/compose.js | 26 +++++++++++++++++++ app/soapbox/reducers/search.js | 2 ++ app/styles/components/reply-mentions.scss | 3 ++- 15 files changed, 146 insertions(+), 18 deletions(-) create mode 100644 app/soapbox/features/compose/containers/quoted_status_container.js create mode 100644 app/soapbox/features/status/containers/quoted_status_container.js 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; From 361831f0211196f7ec9d56ba580e56f9c7223f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 23 Jan 2022 18:59:57 +0100 Subject: [PATCH 03/16] Update @tabler/icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0df38b2da..72ad7dfb5 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@sentry/browser": "^6.12.0", "@sentry/react": "^6.12.0", "@sentry/tracing": "^6.12.0", - "@tabler/icons": "^1.41.2", + "@tabler/icons": "^1.53.0", "array-includes": "^3.0.3", "autoprefixer": "^10.0.0", "axios": "^0.21.4", diff --git a/yarn.lock b/yarn.lock index b1dedfe15..48f6a651c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1519,10 +1519,10 @@ remark "^13.0.0" unist-util-find-all-after "^3.0.2" -"@tabler/icons@^1.41.2": - version "1.41.2" - resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.41.2.tgz#effccbb261539b68609cc7dc660a058683170ee1" - integrity sha512-X6cQmMC24hiwg0p2BzasvU3IeCCdOk0f/9d6gNNtJM4lzG2TCloTns1bVvU5MAFkITGukxUqjPFE3Ecd6kGsfw== +"@tabler/icons@^1.53.0": + version "1.53.0" + resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.53.0.tgz#51536e01b343cfaf26b701df306b2c0369769e3c" + integrity sha512-Skk1BqXEOEhiRsXJgZBYtjFa/+4dMSFA5UyzTUW20oyyUSd3iizhEWrYt0jT87iFu771gWoqVV2/OGobBcGjgQ== "@tootallnate/once@1": version "1.1.2" From 289cdcc776420dc09ca6d481aff39e6f1a6788d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 23 Jan 2022 22:15:25 +0100 Subject: [PATCH 04/16] Use dropdown for reposts, do not display cards for quote posts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/dropdown_menu.js | 9 ++- app/soapbox/components/status.js | 2 +- app/soapbox/components/status_action_bar.js | 51 ++++++++++++++-- .../features/status/components/action_bar.js | 61 ++++++++++++++++--- .../status/components/detailed_status.js | 2 +- .../status/components/quoted_status.js | 2 + app/soapbox/utils/features.js | 1 + app/styles/components/status.scss | 10 +++ 8 files changed, 120 insertions(+), 18 deletions(-) diff --git a/app/soapbox/components/dropdown_menu.js b/app/soapbox/components/dropdown_menu.js index fa0d1ffd5..453f24d71 100644 --- a/app/soapbox/components/dropdown_menu.js +++ b/app/soapbox/components/dropdown_menu.js @@ -207,6 +207,8 @@ export default class Dropdown extends React.PureComponent { src: PropTypes.string, items: PropTypes.array.isRequired, size: PropTypes.number, + active: PropTypes.bool, + pressed: PropTypes.bool, title: PropTypes.string, disabled: PropTypes.bool, status: ImmutablePropTypes.map, @@ -217,6 +219,7 @@ export default class Dropdown extends React.PureComponent { dropdownPlacement: PropTypes.string, openDropdownId: PropTypes.number, openedViaKeyboard: PropTypes.bool, + text: PropTypes.string, }; static defaultProps = { @@ -302,7 +305,7 @@ export default class Dropdown extends React.PureComponent { } render() { - const { icon, src, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props; + const { icon, src, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard, active, pressed, text } = this.props; const open = this.state.id === openDropdownId; return ( @@ -311,9 +314,11 @@ export default class Dropdown extends React.PureComponent { icon={icon} src={src} title={title} - active={open} + active={open || active} + pressed={pressed} disabled={disabled} size={size} + text={text} ref={this.setTargetRef} onClick={this.handleClick} onMouseDown={this.handleMouseDown} diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index 06285a8f4..6bfb2be18 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -458,7 +458,7 @@ class Status extends ImmutablePureComponent { ); } - } else if (status.get('spoiler_text').length === 0 && status.get('card')) { + } else if (status.get('spoiler_text').length === 0 && !status.getIn(['pleroma', 'quote']) && status.get('card')) { media = ( + ); + } else { + reblogButton = ( + + ); + } + const menu = this._makeMenu(publicStatus); let reblogIcon = require('feather-icons/dist/icons/repeat.svg'); let replyTitle; @@ -563,13 +605,10 @@ class StatusActionBar extends ImmutablePureComponent { {replyCount !== 0 && {replyCount}}
-
- +
+ {reblogButton} {reblogCount !== 0 && {reblogCount}}
-
- -
{ @@ -87,6 +88,7 @@ class ActionBar extends React.PureComponent { status: ImmutablePropTypes.map.isRequired, onReply: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired, + onQuote: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired, onEmojiReact: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, @@ -143,6 +145,15 @@ class ActionBar extends React.PureComponent { } } + handleQuoteClick = () => { + const { me, onQuote, onOpenUnauthorizedModal, status } = this.props; + if (me) { + onQuote(status, this.context.router.history); + } else { + onOpenUnauthorizedModal('REBLOG'); + } + } + handleBookmarkClick = () => { this.props.onBookmark(this.props.status); } @@ -319,6 +330,47 @@ class ActionBar extends React.PureComponent { '😩': messages.reactionWeary, }[meEmojiReact] || messages.favourite); + let reblogButton; + + if (me && features.quotePosts) { + const reblogMenu = [ + { + text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog), + action: this.handleReblogClick, + icon: require('@tabler/icons/icons/repeat.svg'), + }, + { + text: intl.formatMessage(messages.quotePost), + action: this.handleQuoteClick, + icon: require('@tabler/icons/icons/quote.svg'), + }, + ]; + + reblogButton = ( + + ); + } else { + reblogButton = ( + + ); + } + const menu = []; if (publicStatus) { @@ -497,14 +549,7 @@ class ActionBar extends React.PureComponent { />
- + {reblogButton}
); } - } else if (status.get('spoiler_text').length === 0) { + } else if (status.get('spoiler_text').length === 0 && !status.getIn(['pleroma', 'quote'])) { media = ; } diff --git a/app/soapbox/features/status/components/quoted_status.js b/app/soapbox/features/status/components/quoted_status.js index f7878adcf..0825052c5 100644 --- a/app/soapbox/features/status/components/quoted_status.js +++ b/app/soapbox/features/status/components/quoted_status.js @@ -29,6 +29,8 @@ class QuotedStatus extends ImmutablePureComponent { } this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/posts/${this.props.status.get('id')}`); + + e.preventDefault(); } } diff --git a/app/soapbox/utils/features.js b/app/soapbox/utils/features.js index 36aba0d6b..fc3dc68ab 100644 --- a/app/soapbox/utils/features.js +++ b/app/soapbox/utils/features.js @@ -78,6 +78,7 @@ export const getFeatures = createSelector([ remoteInteractionsAPI: v.software === PLEROMA && gte(v.version, '2.4.50'), explicitAddressing: v.software === PLEROMA && gte(v.version, '1.0.0'), accountEndorsements: v.software === PLEROMA && gte(v.version, '2.4.50'), + quotePosts: v.software === PLEROMA && gte(v.version, '2.4.50'), }; }); diff --git a/app/styles/components/status.scss b/app/styles/components/status.scss index b3c982554..274252d24 100644 --- a/app/styles/components/status.scss +++ b/app/styles/components/status.scss @@ -338,6 +338,16 @@ font-weight: 500; color: var(--brand-color); } + + &--reblog { + > div { + display: flex; + + > .icon-button { + margin-right: 8px; + } + } + } } } From b0363607c81e175ac7d903287b19420f586f725a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 24 Jan 2022 00:06:35 +0100 Subject: [PATCH 05/16] Quote posts: do not mention anyone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/reducers/compose.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/reducers/compose.js b/app/soapbox/reducers/compose.js index 29b73aefe..e30caf874 100644 --- a/app/soapbox/reducers/compose.js +++ b/app/soapbox/reducers/compose.js @@ -347,8 +347,8 @@ export default function compose(state = initialState, action) { 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('to', undefined); + map.set('text', ''); map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); map.set('focusDate', new Date()); map.set('caretPosition', null); From c5e34de79a7fd9262893c99e223e767187694b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 24 Jan 2022 00:17:32 +0100 Subject: [PATCH 06/16] Do not use reblogIcon before initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/status_action_bar.js | 20 ++--- .../features/status/components/action_bar.js | 82 +++++++++---------- app/soapbox/reducers/compose.js | 10 +-- 3 files changed, 53 insertions(+), 59 deletions(-) diff --git a/app/soapbox/components/status_action_bar.js b/app/soapbox/components/status_action_bar.js index 005aa8b1a..16b8934cd 100644 --- a/app/soapbox/components/status_action_bar.js +++ b/app/soapbox/components/status_action_bar.js @@ -531,6 +531,16 @@ class StatusActionBar extends ImmutablePureComponent { '😩': messages.reactionWeary, }[meEmojiReact] || messages.favourite); + const menu = this._makeMenu(publicStatus); + let reblogIcon = require('feather-icons/dist/icons/repeat.svg'); + let replyTitle; + + if (status.get('visibility') === 'direct') { + reblogIcon = require('@tabler/icons/icons/mail.svg'); + } else if (status.get('visibility') === 'private') { + reblogIcon = require('@tabler/icons/icons/lock.svg'); + } + let reblogButton; if (me && features.quotePosts) { @@ -572,16 +582,6 @@ class StatusActionBar extends ImmutablePureComponent { ); } - const menu = this._makeMenu(publicStatus); - let reblogIcon = require('feather-icons/dist/icons/repeat.svg'); - let replyTitle; - - if (status.get('visibility') === 'direct') { - reblogIcon = require('@tabler/icons/icons/mail.svg'); - } else if (status.get('visibility') === 'private') { - reblogIcon = require('@tabler/icons/icons/lock.svg'); - } - if (status.get('in_reply_to_id', null) === null) { replyTitle = intl.formatMessage(messages.reply); } else { diff --git a/app/soapbox/features/status/components/action_bar.js b/app/soapbox/features/status/components/action_bar.js index 5b1f23d88..57fce8e22 100644 --- a/app/soapbox/features/status/components/action_bar.js +++ b/app/soapbox/features/status/components/action_bar.js @@ -330,47 +330,6 @@ class ActionBar extends React.PureComponent { '😩': messages.reactionWeary, }[meEmojiReact] || messages.favourite); - let reblogButton; - - if (me && features.quotePosts) { - const reblogMenu = [ - { - text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog), - action: this.handleReblogClick, - icon: require('@tabler/icons/icons/repeat.svg'), - }, - { - text: intl.formatMessage(messages.quotePost), - action: this.handleQuoteClick, - icon: require('@tabler/icons/icons/quote.svg'), - }, - ]; - - reblogButton = ( - - ); - } else { - reblogButton = ( - - ); - } - const menu = []; if (publicStatus) { @@ -538,6 +497,47 @@ class ActionBar extends React.PureComponent { const reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private'); + let reblogButton; + + if (me && features.quotePosts) { + const reblogMenu = [ + { + text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog), + action: this.handleReblogClick, + icon: require('@tabler/icons/icons/repeat.svg'), + }, + { + text: intl.formatMessage(messages.quotePost), + action: this.handleQuoteClick, + icon: require('@tabler/icons/icons/quote.svg'), + }, + ]; + + reblogButton = ( + + ); + } else { + reblogButton = ( + + ); + } + return (
diff --git a/app/soapbox/reducers/compose.js b/app/soapbox/reducers/compose.js index e30caf874..c0ee4af0b 100644 --- a/app/soapbox/reducers/compose.js +++ b/app/soapbox/reducers/compose.js @@ -354,14 +354,8 @@ export default function compose(state = initialState, action) { 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')); - } else { - map.set('spoiler', false); - map.set('spoiler_text', ''); - } + map.set('spoiler', false); + map.set('spoiler_text', ''); }); case COMPOSE_SUBMIT_REQUEST: return state.set('is_submitting', true); From 67006fdc343fc18b22c6f4789e47458d79aef502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 24 Jan 2022 16:31:55 +0100 Subject: [PATCH 07/16] Fix normal reblogging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/containers/status_container.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/containers/status_container.js b/app/soapbox/containers/status_container.js index 659c92c68..e6dec8beb 100644 --- a/app/soapbox/containers/status_container.js +++ b/app/soapbox/containers/status_container.js @@ -101,7 +101,7 @@ const mapDispatchToProps = (dispatch, { intl }) => { onReblog(status, e) { dispatch((_, getState) => { const boostModal = getSettings(getState()).get('boostModal'); - if (e.shiftKey || !boostModal) { + if ((e && e.shiftKey) || !boostModal) { onModalReblog(status); } else { dispatch(openModal('BOOST', { status, onReblog: onModalReblog })); From fc0a5aea9cf823adbf78310ace89ac5d663ad140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 24 Jan 2022 16:52:15 +0100 Subject: [PATCH 08/16] Normalization, process quotes in importFetchedStatuses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/importer/index.js | 4 ++++ app/soapbox/actions/importer/normalizer.js | 4 ++++ app/soapbox/components/status.js | 6 +++--- app/soapbox/features/status/components/detailed_status.js | 6 +++--- .../features/status/containers/quoted_status_container.js | 6 +++--- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/soapbox/actions/importer/index.js b/app/soapbox/actions/importer/index.js index 62e9e7860..2b5828e79 100644 --- a/app/soapbox/actions/importer/index.js +++ b/app/soapbox/actions/importer/index.js @@ -124,6 +124,10 @@ export function importFetchedStatuses(statuses) { processStatus(status.reblog); } + if (status.pleroma && status.pleroma.quote && status.pleroma.quote.id) { + processStatus(status.pleroma.quote); + } + if (status.poll && status.poll.id) { pushUnique(polls, normalizePoll(status.poll)); } diff --git a/app/soapbox/actions/importer/normalizer.js b/app/soapbox/actions/importer/normalizer.js index 240abddcc..6a721b53f 100644 --- a/app/soapbox/actions/importer/normalizer.js +++ b/app/soapbox/actions/importer/normalizer.js @@ -49,6 +49,10 @@ export function normalizeStatus(status, normalOldStatus, expandSpoilers) { normalStatus.poll = status.poll.id; } + if (status.pleroma && status.pleroma.quote && status.pleroma.quote.id) { + normalStatus.quote = status.pleroma.quote.id; + } + // Only calculate these values when status first encountered // Otherwise keep the ones already in the reducer if (normalOldStatus) { diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index 6bfb2be18..fe23bae02 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -458,7 +458,7 @@ class Status extends ImmutablePureComponent { ); } - } else if (status.get('spoiler_text').length === 0 && !status.getIn(['pleroma', 'quote']) && status.get('card')) { + } else if (status.get('spoiler_text').length === 0 && !status.get('quote') && status.get('card')) { media = ( ; + if (status.get('quote')) { + quote = ; } if (otherAccounts && otherAccounts.size > 1) { diff --git a/app/soapbox/features/status/components/detailed_status.js b/app/soapbox/features/status/components/detailed_status.js index c1cce7550..a54c9bda5 100644 --- a/app/soapbox/features/status/components/detailed_status.js +++ b/app/soapbox/features/status/components/detailed_status.js @@ -157,14 +157,14 @@ class DetailedStatus extends ImmutablePureComponent { /> ); } - } else if (status.get('spoiler_text').length === 0 && !status.getIn(['pleroma', 'quote'])) { + } else if (status.get('spoiler_text').length === 0 && !status.get('quote')) { media = ; } let quote; - if (status.getIn(['pleroma', 'quote'])) { - quote = ; + if (status.get('quote')) { + quote = ; } if (status.get('visibility') === 'direct') { diff --git a/app/soapbox/features/status/containers/quoted_status_container.js b/app/soapbox/features/status/containers/quoted_status_container.js index 99b7763c2..a375b2562 100644 --- a/app/soapbox/features/status/containers/quoted_status_container.js +++ b/app/soapbox/features/status/containers/quoted_status_container.js @@ -7,11 +7,11 @@ import QuotedStatus from '../components/quoted_status'; const makeMapStateToProps = () => { const getStatus = makeGetStatus(); - const mapStateToProps = (state, props) => ({ - status: getStatus(state, { id: props.statusId }), + const mapStateToProps = (state, { statusId }) => ({ + status: getStatus(state, { id: statusId }), }); return mapStateToProps; }; -export default connect(makeMapStateToProps)(QuotedStatus); \ No newline at end of file +export default connect(makeMapStateToProps)(QuotedStatus); From 22a70a7595f2c7636dcaf7879014f3cf23ae387b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 24 Jan 2022 18:17:07 +0100 Subject: [PATCH 09/16] Quote posts: Compose box improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../containers/quoted_status_container.js | 12 +++- .../status/components/quoted_status.js | 69 +++++++++++++++---- app/styles/components/compose-form.scss | 16 ++++- app/styles/components/dropdown-menu.scss | 2 +- app/styles/components/status.scss | 2 - 5 files changed, 83 insertions(+), 18 deletions(-) diff --git a/app/soapbox/features/compose/containers/quoted_status_container.js b/app/soapbox/features/compose/containers/quoted_status_container.js index 907a4caa3..c15fa1764 100644 --- a/app/soapbox/features/compose/containers/quoted_status_container.js +++ b/app/soapbox/features/compose/containers/quoted_status_container.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; +import { cancelQuoteCompose } from 'soapbox/actions/compose'; import QuotedStatus from 'soapbox/features/status/components/quoted_status'; import { makeGetStatus } from 'soapbox/selectors'; @@ -8,9 +9,18 @@ const makeMapStateToProps = () => { const mapStateToProps = state => ({ status: getStatus(state, { id: state.getIn(['compose', 'quote']) }), + compose: true, }); return mapStateToProps; }; -export default connect(makeMapStateToProps)(QuotedStatus); \ No newline at end of file +const mapDispatchToProps = dispatch => ({ + + onCancel() { + dispatch(cancelQuoteCompose()); + }, + +}); + +export default connect(makeMapStateToProps, mapDispatchToProps)(QuotedStatus); \ No newline at end of file diff --git a/app/soapbox/features/status/components/quoted_status.js b/app/soapbox/features/status/components/quoted_status.js index 0825052c5..3bb8b2367 100644 --- a/app/soapbox/features/status/components/quoted_status.js +++ b/app/soapbox/features/status/components/quoted_status.js @@ -2,15 +2,20 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { NavLink } from 'react-router-dom'; import AttachmentThumbs from 'soapbox/components/attachment_thumbs'; import Avatar from 'soapbox/components/avatar'; import DisplayName from 'soapbox/components/display_name'; +import IconButton from 'soapbox/components/icon_button'; import RelativeTimestamp from 'soapbox/components/relative_timestamp'; import { isRtl } from 'soapbox/rtl'; +const messages = defineMessages({ + cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, +}); + export default @injectIntl class QuotedStatus extends ImmutablePureComponent { @@ -20,20 +25,31 @@ class QuotedStatus extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map, + onCancel: PropTypes.func, + intl: PropTypes.object.isRequired, + compose: PropTypes.bool, }; - handleExpandClick = (e) => { - if (e.button === 0) { + handleExpandClick = e => { + const { compose, status } = this.props; + + if (!compose && e.button === 0) { if (!this.context.router) { return; } - this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/posts/${this.props.status.get('id')}`); + this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`); e.preventDefault(); } } + handleClose = e => { + this.props.onCancel(); + + e.preventDefault(); + } + renderReplyMentions = () => { const { status } = this.props; @@ -81,7 +97,7 @@ class QuotedStatus extends ImmutablePureComponent { } render() { - const { status } = this.props; + const { status, onCancel, intl, compose } = this.props; if (!status) { return null; @@ -92,16 +108,33 @@ class QuotedStatus extends ImmutablePureComponent { direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr', }; - return ( + const displayName = (<> +
+ + ); + + const quotedStatus = (
-
- -
- -
- -
+ {onCancel + ? ( +
+ +
+ ) : ( +
+ +
+ )} + {compose ? ( +
+ {displayName} +
+ ) : ( + + {displayName} + + )}
{this.renderReplyMentions()} @@ -116,6 +149,16 @@ class QuotedStatus extends ImmutablePureComponent { )}
); + + if (compose) { + return ( +
+ {quotedStatus} +
+ ); + } + + return quotedStatus; } } diff --git a/app/styles/components/compose-form.scss b/app/styles/components/compose-form.scss index 8c583edff..4c1537f4f 100644 --- a/app/styles/components/compose-form.scss +++ b/app/styles/components/compose-form.scss @@ -84,13 +84,27 @@ .spoiler-input__input { border-radius: 4px; } + &__quoted-status-wrapper { + background: var(--background-color); + + .quoted-status { + &:hover, + &:focus, + &:active { + background: transparent; + cursor: unset; + } + } + } + &.condensed { .autosuggest-textarea__textarea { min-height: 46px; border-radius: 5px; } - .compose-form__buttons-wrapper { + .compose-form__buttons-wrapper, + .compose-form__quoted-status-wrapper { height: 0; padding: 0; overflow: hidden; diff --git a/app/styles/components/dropdown-menu.scss b/app/styles/components/dropdown-menu.scss index 66d5832c5..5c705db81 100644 --- a/app/styles/components/dropdown-menu.scss +++ b/app/styles/components/dropdown-menu.scss @@ -62,7 +62,7 @@ box-sizing: border-box; overflow: hidden; margin: 0 6px; - padding: 4px 4px; + padding: 4px; border-radius: 6px; font-size: 15px; text-decoration: none; diff --git a/app/styles/components/status.scss b/app/styles/components/status.scss index 274252d24..25bdfec78 100644 --- a/app/styles/components/status.scss +++ b/app/styles/components/status.scss @@ -762,8 +762,6 @@ a.status-card.compact:hover { border: 1px solid var(--brand-color--med); border-radius: 10px; padding: 12px; - overflow-y: auto; - flex: 0 2 auto; transition: background 0.2s; cursor: pointer; From af9b69271ba6be92cf9c1802a43f42604bc873b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 24 Jan 2022 18:47:10 +0100 Subject: [PATCH 10/16] change reblogIcon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/status_action_bar.js | 2 +- app/soapbox/features/status/components/action_bar.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/components/status_action_bar.js b/app/soapbox/components/status_action_bar.js index 16b8934cd..3960e6bf7 100644 --- a/app/soapbox/components/status_action_bar.js +++ b/app/soapbox/components/status_action_bar.js @@ -564,7 +564,7 @@ class StatusActionBar extends ImmutablePureComponent { active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} - src={require('@tabler/icons/icons/repeat.svg')} + src={reblogIcon} direction='right' /> ); diff --git a/app/soapbox/features/status/components/action_bar.js b/app/soapbox/features/status/components/action_bar.js index 57fce8e22..e154b2d43 100644 --- a/app/soapbox/features/status/components/action_bar.js +++ b/app/soapbox/features/status/components/action_bar.js @@ -520,7 +520,7 @@ class ActionBar extends React.PureComponent { active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} - src={require('@tabler/icons/icons/repeat.svg')} + src={reblogIcon} direction='right' text={intl.formatMessage(messages.reblog)} /> From 9f98fcff092187b1a7017f378ee30d386fae8059 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Jan 2022 12:58:39 -0600 Subject: [PATCH 11/16] Fedibird compatibility, because why not --- app/soapbox/actions/importer/index.js | 5 +++++ app/soapbox/actions/importer/normalizer.js | 3 +++ app/styles/components/status.scss | 8 ++++++++ 3 files changed, 16 insertions(+) diff --git a/app/soapbox/actions/importer/index.js b/app/soapbox/actions/importer/index.js index 2b5828e79..1b15956f2 100644 --- a/app/soapbox/actions/importer/index.js +++ b/app/soapbox/actions/importer/index.js @@ -75,6 +75,11 @@ export function importFetchedStatus(status, idempotencyKey) { dispatch(importFetchedStatus(status.reblog)); } + // Fedibird quotes + if (status.quote && status.quote.id) { + dispatch(importFetchedStatus(status.quote)); + } + if (status.pleroma && status.pleroma.quote && status.pleroma.quote.id) { dispatch(importFetchedStatus(status.pleroma.quote)); } diff --git a/app/soapbox/actions/importer/normalizer.js b/app/soapbox/actions/importer/normalizer.js index 6a721b53f..b9ea87ee9 100644 --- a/app/soapbox/actions/importer/normalizer.js +++ b/app/soapbox/actions/importer/normalizer.js @@ -51,6 +51,9 @@ export function normalizeStatus(status, normalOldStatus, expandSpoilers) { if (status.pleroma && status.pleroma.quote && status.pleroma.quote.id) { normalStatus.quote = status.pleroma.quote.id; + } else if (status.quote && status.quote.id) { + // Fedibird compatibility, because why not + normalStatus.quote = status.quote.id; } // Only calculate these values when status first encountered diff --git a/app/styles/components/status.scss b/app/styles/components/status.scss index 25bdfec78..2e8f75617 100644 --- a/app/styles/components/status.scss +++ b/app/styles/components/status.scss @@ -816,3 +816,11 @@ a.status-card.compact:hover { margin-top: 5px !important; } } + +/* Fedibird quote post compatibility */ +.status__content, +.quoted-status__content { + .quote-inline { + display: none; + } +} From 5c755c8f9b46f7d396c95dc628fa97caa8a857ac Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Jan 2022 13:23:29 -0600 Subject: [PATCH 12/16] Quote post performance improvements --- app/soapbox/actions/importer/normalizer.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/soapbox/actions/importer/normalizer.js b/app/soapbox/actions/importer/normalizer.js index b9ea87ee9..fae8b2787 100644 --- a/app/soapbox/actions/importer/normalizer.js +++ b/app/soapbox/actions/importer/normalizer.js @@ -37,7 +37,12 @@ export function normalizeAccount(account) { } export function normalizeStatus(status, normalOldStatus, expandSpoilers) { - const normalStatus = { ...status }; + const normalStatus = { ...status }; + + // Copy the pleroma object too, so we can modify our copy + if (status.pleroma) { + normalStatus.pleroma = { ...status.pleroma }; + } normalStatus.account = status.account.id; @@ -50,7 +55,9 @@ export function normalizeStatus(status, normalOldStatus, expandSpoilers) { } if (status.pleroma && status.pleroma.quote && status.pleroma.quote.id) { + // Normalize quote to the top-level, so delete the original for performance normalStatus.quote = status.pleroma.quote.id; + delete normalStatus.pleroma.quote; } else if (status.quote && status.quote.id) { // Fedibird compatibility, because why not normalStatus.quote = status.quote.id; From 9abeb2a324e12dbecfdbba5e8305fff3f291835e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Jan 2022 13:27:13 -0600 Subject: [PATCH 13/16] Importer: handle multiple statuses from Fedibird --- app/soapbox/actions/importer/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/soapbox/actions/importer/index.js b/app/soapbox/actions/importer/index.js index 1b15956f2..4c8a98247 100644 --- a/app/soapbox/actions/importer/index.js +++ b/app/soapbox/actions/importer/index.js @@ -129,6 +129,11 @@ export function importFetchedStatuses(statuses) { processStatus(status.reblog); } + // Fedibird quotes + if (status.quote && status.quote.id) { + processStatus(status.quote); + } + if (status.pleroma && status.pleroma.quote && status.pleroma.quote.id) { processStatus(status.pleroma.quote); } From 93e7efba056a42d2e8134711bf81087a51c50f71 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Jan 2022 13:58:52 -0600 Subject: [PATCH 14/16] Fedibird: fall back to quote_id --- app/soapbox/actions/importer/normalizer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/soapbox/actions/importer/normalizer.js b/app/soapbox/actions/importer/normalizer.js index fae8b2787..c8f1a83ef 100644 --- a/app/soapbox/actions/importer/normalizer.js +++ b/app/soapbox/actions/importer/normalizer.js @@ -61,6 +61,9 @@ export function normalizeStatus(status, normalOldStatus, expandSpoilers) { } else if (status.quote && status.quote.id) { // Fedibird compatibility, because why not normalStatus.quote = status.quote.id; + } else if (status.quote_id) { + // Fedibird: fall back to quote_id + normalStatus.quote = status.quote_id; } // Only calculate these values when status first encountered From aebc653bd91282942046364a5e8e5ddc444ca955 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Jan 2022 14:29:30 -0600 Subject: [PATCH 15/16] Importer: remove the extremely stupid pushUnique function --- app/soapbox/actions/importer/index.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/app/soapbox/actions/importer/index.js b/app/soapbox/actions/importer/index.js index 4c8a98247..aee579695 100644 --- a/app/soapbox/actions/importer/index.js +++ b/app/soapbox/actions/importer/index.js @@ -13,12 +13,6 @@ export const STATUSES_IMPORT = 'STATUSES_IMPORT'; export const POLLS_IMPORT = 'POLLS_IMPORT'; export const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP'; -function pushUnique(array, object) { - if (array.every(element => element.id !== object.id)) { - array.push(object); - } -} - export function importAccount(account) { return { type: ACCOUNT_IMPORT, account }; } @@ -49,7 +43,7 @@ export function importFetchedAccounts(accounts) { function processAccount(account) { if (!account.id) return; - pushUnique(normalAccounts, normalizeAccount(account)); + normalAccounts.push(normalizeAccount(account)); if (account.moved) { processAccount(account.moved); @@ -122,8 +116,8 @@ export function importFetchedStatuses(statuses) { const normalOldStatus = getState().getIn(['statuses', status.id]); const expandSpoilers = getSettings(getState()).get('expandSpoilers'); - pushUnique(normalStatuses, normalizeStatus(status, normalOldStatus, expandSpoilers)); - pushUnique(accounts, status.account); + normalStatuses.push(normalizeStatus(status, normalOldStatus, expandSpoilers)); + accounts.push(status.account); if (status.reblog && status.reblog.id) { processStatus(status.reblog); @@ -139,7 +133,7 @@ export function importFetchedStatuses(statuses) { } if (status.poll && status.poll.id) { - pushUnique(polls, normalizePoll(status.poll)); + polls.push(normalizePoll(status.poll)); } } From bd3c74f566b66497e146fea242c2080d169f434f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Jan 2022 15:08:22 -0600 Subject: [PATCH 16/16] Prevent a quoted status overriding the quote of its parent --- .../fedibird-quote-of-quote-post.json | 109 +++++ .../__fixtures__/fedibird-quote-post.json | 108 +++++ .../pleroma-quote-of-quote-post.json | 371 ++++++++++++++++++ .../__fixtures__/pleroma-quote-post.json | 364 +++++++++++++++++ .../reducers/__tests__/statuses-test.js | 12 + app/soapbox/reducers/statuses.js | 20 +- 6 files changed, 982 insertions(+), 2 deletions(-) create mode 100644 app/soapbox/__fixtures__/fedibird-quote-of-quote-post.json create mode 100644 app/soapbox/__fixtures__/fedibird-quote-post.json create mode 100644 app/soapbox/__fixtures__/pleroma-quote-of-quote-post.json create mode 100644 app/soapbox/__fixtures__/pleroma-quote-post.json diff --git a/app/soapbox/__fixtures__/fedibird-quote-of-quote-post.json b/app/soapbox/__fixtures__/fedibird-quote-of-quote-post.json new file mode 100644 index 000000000..c00c81860 --- /dev/null +++ b/app/soapbox/__fixtures__/fedibird-quote-of-quote-post.json @@ -0,0 +1,109 @@ +{ + "id": "107673570598783346", + "created_at": "2022-01-23T20:05:01.372Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://fedibird.com/users/alex/statuses/107673570598783346", + "url": "https://fedibird.com/@alex/107673570598783346", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "emoji_reactions_count": 0, + "emoji_reactions": [], + "content": "

test quote of a quote
QT: https://fedibird.com/@alex/107673570082615319

", + "quote_id": "107673570082615319", + "reblog": null, + "application": { + "name": "Web", + "website": null + }, + "account": { + "id": "66768", + "username": "alex", + "acct": "alex", + "display_name": "", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2020-01-27T00:00:00.000Z", + "note": "

", + "url": "https://fedibird.com/@alex", + "avatar": "https://fedibird.com/avatars/original/missing.png", + "avatar_static": "https://fedibird.com/avatars/original/missing.png", + "header": "https://fedibird.com/headers/original/missing.png", + "header_static": "https://fedibird.com/headers/original/missing.png", + "followers_count": 0, + "following_count": 1, + "subscribing_count": 0, + "statuses_count": 3, + "last_status_at": "2022-01-23", + "emojis": [], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null, + "quote": { + "id": "107673570082615319", + "created_at": "2022-01-23T20:04:53.494Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://fedibird.com/users/alex/statuses/107673570082615319", + "url": "https://fedibird.com/@alex/107673570082615319", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "emoji_reactions_count": 0, + "emoji_reactions": [], + "content": "

test quote
QT: https://fedibird.com/@alex/107673569214329435

", + "quote_id": "107673569214329435", + "quote": null, + "reblog": null, + "application": { + "name": "Web", + "website": null + }, + "account": { + "id": "66768", + "username": "alex", + "acct": "alex", + "display_name": "", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2020-01-27T00:00:00.000Z", + "note": "

", + "url": "https://fedibird.com/@alex", + "avatar": "https://fedibird.com/avatars/original/missing.png", + "avatar_static": "https://fedibird.com/avatars/original/missing.png", + "header": "https://fedibird.com/headers/original/missing.png", + "header_static": "https://fedibird.com/headers/original/missing.png", + "followers_count": 0, + "following_count": 1, + "subscribing_count": 0, + "statuses_count": 3, + "last_status_at": "2022-01-23", + "emojis": [], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null + } +} diff --git a/app/soapbox/__fixtures__/fedibird-quote-post.json b/app/soapbox/__fixtures__/fedibird-quote-post.json new file mode 100644 index 000000000..610ab45c6 --- /dev/null +++ b/app/soapbox/__fixtures__/fedibird-quote-post.json @@ -0,0 +1,108 @@ +{ + "id": "107673570082615319", + "created_at": "2022-01-23T20:04:53.494Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://fedibird.com/users/alex/statuses/107673570082615319", + "url": "https://fedibird.com/@alex/107673570082615319", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "emoji_reactions_count": 0, + "emoji_reactions": [], + "content": "

test quote
QT: https://fedibird.com/@alex/107673569214329435

", + "quote_id": "107673569214329435", + "reblog": null, + "application": { + "name": "Web", + "website": null + }, + "account": { + "id": "66768", + "username": "alex", + "acct": "alex", + "display_name": "", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2020-01-27T00:00:00.000Z", + "note": "

", + "url": "https://fedibird.com/@alex", + "avatar": "https://fedibird.com/avatars/original/missing.png", + "avatar_static": "https://fedibird.com/avatars/original/missing.png", + "header": "https://fedibird.com/headers/original/missing.png", + "header_static": "https://fedibird.com/headers/original/missing.png", + "followers_count": 0, + "following_count": 1, + "subscribing_count": 0, + "statuses_count": 3, + "last_status_at": "2022-01-23", + "emojis": [], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null, + "quote": { + "id": "107673569214329435", + "created_at": "2022-01-23T20:04:40.249Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://fedibird.com/users/alex/statuses/107673569214329435", + "url": "https://fedibird.com/@alex/107673569214329435", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "emoji_reactions_count": 0, + "emoji_reactions": [], + "content": "

test post

", + "quote": null, + "reblog": null, + "application": { + "name": "Web", + "website": null + }, + "account": { + "id": "66768", + "username": "alex", + "acct": "alex", + "display_name": "", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2020-01-27T00:00:00.000Z", + "note": "

", + "url": "https://fedibird.com/@alex", + "avatar": "https://fedibird.com/avatars/original/missing.png", + "avatar_static": "https://fedibird.com/avatars/original/missing.png", + "header": "https://fedibird.com/headers/original/missing.png", + "header_static": "https://fedibird.com/headers/original/missing.png", + "followers_count": 0, + "following_count": 1, + "subscribing_count": 0, + "statuses_count": 3, + "last_status_at": "2022-01-23", + "emojis": [], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null + } +} diff --git a/app/soapbox/__fixtures__/pleroma-quote-of-quote-post.json b/app/soapbox/__fixtures__/pleroma-quote-of-quote-post.json new file mode 100644 index 000000000..1156cdb3a --- /dev/null +++ b/app/soapbox/__fixtures__/pleroma-quote-of-quote-post.json @@ -0,0 +1,371 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2220, + "following_count": 1544, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-01-24T21:02:44", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23004, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

Quote of quote post

", + "created_at": "2022-01-24T21:02:43.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "AFmFNKmfrR9CxtV01g", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "Quote of quote post" + }, + "conversation_id": "AFmFNKkXzLRirIVIi8", + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2220, + "following_count": 1544, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-01-24T21:02:44", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23004, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

Quote post

", + "created_at": "2022-01-24T21:02:34.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "AFmFMSpITT9xcOJKcK", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "Quote post" + }, + "conversation_id": "AFmFMSnWa3k3WtTur2", + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 1, + "me": false, + "name": "👍" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": null, + "quote_url": "https://gleasonator.com/objects/4f35159c-3794-4037-9269-a7c84f7137c7", + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/54d93075-7d04-4016-a128-81f3843bca79", + "url": "https://gleasonator.com/notice/AFmFMSpITT9xcOJKcK", + "visibility": "public" + }, + "quote_url": "https://gleasonator.com/objects/54d93075-7d04-4016-a128-81f3843bca79", + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 1, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/1e2cfb5a-ece5-42df-9ec1-13e5de6d9f5b", + "url": "https://gleasonator.com/notice/AFmFNKmfrR9CxtV01g", + "visibility": "public" +} diff --git a/app/soapbox/__fixtures__/pleroma-quote-post.json b/app/soapbox/__fixtures__/pleroma-quote-post.json new file mode 100644 index 000000000..994671ce4 --- /dev/null +++ b/app/soapbox/__fixtures__/pleroma-quote-post.json @@ -0,0 +1,364 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2220, + "following_count": 1544, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-01-24T21:02:44", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23004, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

Quote post

", + "created_at": "2022-01-24T21:02:34.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "AFmFMSpITT9xcOJKcK", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "Quote post" + }, + "conversation_id": "AFmFMSnWa3k3WtTur2", + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 1, + "me": false, + "name": "👍" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2220, + "following_count": 1544, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-01-24T21:02:44", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23004, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

Test post

", + "created_at": "2022-01-24T21:02:25.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "AFmFLcd6XYVdjWCrOS", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "Test post" + }, + "conversation_id": "AFmFLcaGi6EzaisayO", + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": null, + "quote_url": null, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/4f35159c-3794-4037-9269-a7c84f7137c7", + "url": "https://gleasonator.com/notice/AFmFLcd6XYVdjWCrOS", + "visibility": "public" + }, + "quote_url": "https://gleasonator.com/objects/4f35159c-3794-4037-9269-a7c84f7137c7", + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/54d93075-7d04-4016-a128-81f3843bca79", + "url": "https://gleasonator.com/notice/AFmFMSpITT9xcOJKcK", + "visibility": "public" +} diff --git a/app/soapbox/reducers/__tests__/statuses-test.js b/app/soapbox/reducers/__tests__/statuses-test.js index 37a47256d..255d3d0a7 100644 --- a/app/soapbox/reducers/__tests__/statuses-test.js +++ b/app/soapbox/reducers/__tests__/statuses-test.js @@ -1,6 +1,7 @@ import { Map as ImmutableMap, fromJS } from 'immutable'; import { STATUS_IMPORT } from 'soapbox/actions/importer'; +import { normalizeStatus } from 'soapbox/actions/importer/normalizer'; import { STATUS_CREATE_REQUEST, STATUS_CREATE_FAIL, @@ -27,6 +28,17 @@ describe('statuses reducer', () => { expect(result).toEqual(expected); }); + + it('preserves the quote', () => { + const quotePost = require('soapbox/__fixtures__/pleroma-quote-post.json'); + const quotedQuotePost = require('soapbox/__fixtures__/pleroma-quote-of-quote-post.json'); + + let state = undefined; + state = reducer(state, { type: STATUS_IMPORT, status: normalizeStatus(quotePost) }); + state = reducer(state, { type: STATUS_IMPORT, status: normalizeStatus(quotedQuotePost.pleroma.quote) }); + + expect(state.getIn(['AFmFMSpITT9xcOJKcK', 'quote'])).toEqual('AFmFLcd6XYVdjWCrOS'); + }); }); describe('STATUS_CREATE_REQUEST', () => { diff --git a/app/soapbox/reducers/statuses.js b/app/soapbox/reducers/statuses.js index 144eb8eef..9c1f61109 100644 --- a/app/soapbox/reducers/statuses.js +++ b/app/soapbox/reducers/statuses.js @@ -43,13 +43,29 @@ const fixMentions = status => { return status.set('mentions', sorted); }; -const fixStatus = status => { +const isQuote = status => { + return Boolean(status.get('quote_id') || status.getIn(['pleroma', 'quote_url'])); +}; + +// Preserve quote if an existing status already has it +const fixQuote = (state, status) => { + const oldStatus = state.get(status.get('id')); + + if (oldStatus && !status.get('quote') && isQuote(status)) { + return status.set('quote', oldStatus.get('quote')); + } else { + return status; + } +}; + +const fixStatus = (state, status) => { return status.withMutations(status => { fixMentions(status); + fixQuote(state, status); }); }; -const importStatus = (state, status) => state.set(status.id, fixStatus(fromJS(status))); +const importStatus = (state, status) => state.set(status.id, fixStatus(state, fromJS(status))); const importStatuses = (state, statuses) => state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status)));