diff --git a/app/javascript/gabsocial/actions/compose.js b/app/javascript/gabsocial/actions/compose.js
index e377ece9..bd5424fc 100644
--- a/app/javascript/gabsocial/actions/compose.js
+++ b/app/javascript/gabsocial/actions/compose.js
@@ -20,6 +20,7 @@ export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
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_QUOTE = 'COMPOSE_QUOTE';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
@@ -91,6 +92,17 @@ export function replyCompose(status, routerHistory) {
};
};
+export function quoteCompose(status, routerHistory) {
+ return (dispatch, getState) => {
+ dispatch({
+ type: COMPOSE_QUOTE,
+ status: status,
+ });
+
+ dispatch(openModal('COMPOSE'));
+ };
+};
+
export function cancelReplyCompose() {
return {
type: COMPOSE_REPLY_CANCEL,
@@ -142,6 +154,7 @@ export function submitCompose(routerHistory, group) {
api(getState).post('/api/v1/statuses', {
status,
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
+ quote_of_id: getState().getIn(['compose', 'quote_of_id'], null),
media_ids: media.map(item => item.get('id')),
sensitive: getState().getIn(['compose', 'sensitive']),
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
diff --git a/app/javascript/gabsocial/components/status.js b/app/javascript/gabsocial/components/status.js
index 61a5a06c..5f6a225b 100644
--- a/app/javascript/gabsocial/components/status.js
+++ b/app/javascript/gabsocial/components/status.js
@@ -66,6 +66,7 @@ class Status extends ImmutablePureComponent {
otherAccounts: ImmutablePropTypes.list,
onClick: PropTypes.func,
onReply: PropTypes.func,
+ onQuote: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onDelete: PropTypes.func,
diff --git a/app/javascript/gabsocial/components/status_action_bar.js b/app/javascript/gabsocial/components/status_action_bar.js
index e65fd689..ac2f3b22 100644
--- a/app/javascript/gabsocial/components/status_action_bar.js
+++ b/app/javascript/gabsocial/components/status_action_bar.js
@@ -23,9 +23,11 @@ const messages = defineMessages({
more: { id: 'status.more', defaultMessage: 'More' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
+ quote: { id: 'status.quote', defaultMessage: 'Quote' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
+ cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
@@ -51,6 +53,7 @@ class StatusActionBar extends ImmutablePureComponent {
status: ImmutablePropTypes.map.isRequired,
onOpenUnauthorizedModal: PropTypes.func.isRequired,
onReply: PropTypes.func,
+ onQuote: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onDelete: PropTypes.func,
@@ -82,6 +85,14 @@ class StatusActionBar extends ImmutablePureComponent {
}
}
+ handleQuoteClick = () => {
+ if (me) {
+ this.props.onQuote(this.props.status, this.context.router.history);
+ } else {
+ this.props.onOpenUnauthorizedModal();
+ }
+ }
+
handleShareClick = () => {
navigator.share({
text: this.props.status.get('search_index'),
@@ -283,6 +294,9 @@ class StatusActionBar extends ImmutablePureComponent {
{reblogCount !== 0 && {reblogCount}}
+
+
+
{favoriteCount !== 0 && {favoriteCount}}
diff --git a/app/javascript/gabsocial/containers/status_container.js b/app/javascript/gabsocial/containers/status_container.js
index f4bfc496..707c90e6 100644
--- a/app/javascript/gabsocial/containers/status_container.js
+++ b/app/javascript/gabsocial/containers/status_container.js
@@ -6,6 +6,7 @@ import {
replyCompose,
mentionCompose,
directCompose,
+ quoteCompose,
} from '../actions/compose';
import {
reblog,
@@ -42,6 +43,8 @@ const messages = defineMessages({
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
+ quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
+ quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
});
@@ -72,6 +75,21 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
});
},
+ onQuote (status, router) {
+ dispatch((_, getState) => {
+ let state = getState();
+ if (state.getIn(['compose', 'text']).trim().length !== 0) {
+ dispatch(openModal('CONFIRM', {
+ message: intl.formatMessage(messages.quoteMessage),
+ confirm: intl.formatMessage(messages.quoteConfirm),
+ onConfirm: () => dispatch(quoteCompose(status, router)),
+ }));
+ } else {
+ dispatch(quoteCompose(status, router));
+ }
+ });
+ },
+
onModalReblog (status) {
if (status.get('reblogged')) {
dispatch(unreblog(status));
diff --git a/app/javascript/gabsocial/reducers/compose.js b/app/javascript/gabsocial/reducers/compose.js
index 134ccdcd..d65b2998 100644
--- a/app/javascript/gabsocial/reducers/compose.js
+++ b/app/javascript/gabsocial/reducers/compose.js
@@ -4,6 +4,7 @@ import {
COMPOSE_CHANGE,
COMPOSE_REPLY,
COMPOSE_REPLY_CANCEL,
+ COMPOSE_QUOTE,
COMPOSE_DIRECT,
COMPOSE_MENTION,
COMPOSE_SUBMIT_REQUEST,
@@ -55,6 +56,7 @@ const initialState = ImmutableMap({
caretPosition: null,
preselectDate: null,
in_reply_to: null,
+ quote_of_id: null,
is_composing: false,
is_submitting: false,
is_changing_upload: false,
@@ -95,6 +97,7 @@ function clearAll(state) {
map.set('is_submitting', false);
map.set('is_changing_upload', false);
map.set('in_reply_to', null);
+ map.set('quote_of_id', null);
map.set('privacy', state.get('default_privacy'));
map.set('sensitive', false);
map.update('media_attachments', list => list.clear());
@@ -247,6 +250,24 @@ export default function compose(state = initialState, action) {
map.set('preselectDate', new Date());
map.set('idempotencyKey', uuid());
+ 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_of_id', action.status.get('id'));
+ map.set('text', '');
+ map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
+ map.set('focusDate', new Date());
+ map.set('caretPosition', null);
+ map.set('preselectDate', new Date());
+ map.set('idempotencyKey', uuid());
+
if (action.status.get('spoiler_text').length > 0) {
map.set('spoiler', true);
map.set('spoiler_text', action.status.get('spoiler_text'));
@@ -258,6 +279,7 @@ export default function compose(state = initialState, action) {
case COMPOSE_REPLY_CANCEL:
case COMPOSE_RESET:
return state.withMutations(map => {
+ map.set('quote_of_id', null);
map.set('in_reply_to', null);
map.set('text', '');
map.set('spoiler', false);
@@ -333,6 +355,7 @@ export default function compose(state = initialState, action) {
return state.withMutations(map => {
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
map.set('in_reply_to', action.status.get('in_reply_to_id'));
+ map.set('quote_of_id', action.status.get('quote_of_id'));
map.set('privacy', action.status.get('visibility'));
map.set('media_attachments', action.status.get('media_attachments'));
map.set('focusDate', new Date());