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());