Commit aed10a5a authored by Rob Colbert's avatar Rob Colbert

Merge branch 'feature/quoting-statuses' into 'develop'

Feature/quoting statuses

See merge request gab/social/gab-social!36
parents 9f43f74b 92ebfbd0
...@@ -55,7 +55,8 @@ class Api::V1::StatusesController < Api::BaseController ...@@ -55,7 +55,8 @@ class Api::V1::StatusesController < Api::BaseController
application: doorkeeper_token.application, application: doorkeeper_token.application,
poll: status_params[:poll], poll: status_params[:poll],
idempotency: request.headers['Idempotency-Key'], idempotency: request.headers['Idempotency-Key'],
group_id: status_params[:group_id]) group_id: status_params[:group_id],
quote_of_id: status_params[:quote_of_id])
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end end
...@@ -82,6 +83,7 @@ class Api::V1::StatusesController < Api::BaseController ...@@ -82,6 +83,7 @@ class Api::V1::StatusesController < Api::BaseController
params.permit( params.permit(
:status, :status,
:in_reply_to_id, :in_reply_to_id,
:quote_of_id,
:sensitive, :sensitive,
:spoiler_text, :spoiler_text,
:visibility, :visibility,
......
...@@ -20,6 +20,7 @@ export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; ...@@ -20,6 +20,7 @@ export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS'; export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL'; export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY'; export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_MENTION = 'COMPOSE_MENTION';
...@@ -91,6 +92,17 @@ export function replyCompose(status, routerHistory) { ...@@ -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() { export function cancelReplyCompose() {
return { return {
type: COMPOSE_REPLY_CANCEL, type: COMPOSE_REPLY_CANCEL,
...@@ -142,6 +154,7 @@ export function submitCompose(routerHistory, group) { ...@@ -142,6 +154,7 @@ export function submitCompose(routerHistory, group) {
api(getState).post('/api/v1/statuses', { api(getState).post('/api/v1/statuses', {
status, status,
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), 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')), media_ids: media.map(item => item.get('id')),
sensitive: getState().getIn(['compose', 'sensitive']), sensitive: getState().getIn(['compose', 'sensitive']),
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
......
...@@ -71,6 +71,10 @@ export function importFetchedStatuses(statuses) { ...@@ -71,6 +71,10 @@ export function importFetchedStatuses(statuses) {
processStatus(status.reblog); processStatus(status.reblog);
} }
if (status.quote && status.quote.id) {
processStatus(status.quote);
}
if (status.poll && status.poll.id) { if (status.poll && status.poll.id) {
pushUnique(polls, normalizePoll(status.poll)); pushUnique(polls, normalizePoll(status.poll));
} }
......
...@@ -43,6 +43,10 @@ export function normalizeStatus(status, normalOldStatus) { ...@@ -43,6 +43,10 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.reblog = status.reblog.id; normalStatus.reblog = status.reblog.id;
} }
if (status.quote && status.quote.id) {
normalStatus.quote = status.quote.id;
}
if (status.poll && status.poll.id) { if (status.poll && status.poll.id) {
normalStatus.poll = status.poll.id; normalStatus.poll = status.poll.id;
} }
......
...@@ -7,6 +7,7 @@ import AvatarComposite from './avatar_composite'; ...@@ -7,6 +7,7 @@ import AvatarComposite from './avatar_composite';
import RelativeTimestamp from './relative_timestamp'; import RelativeTimestamp from './relative_timestamp';
import DisplayName from './display_name'; import DisplayName from './display_name';
import StatusContent from './status_content'; import StatusContent from './status_content';
import StatusQuote from './status_quote';
import StatusActionBar from './status_action_bar'; import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list'; import AttachmentList from './attachment_list';
import Card from '../features/status/components/card'; import Card from '../features/status/components/card';
...@@ -66,6 +67,7 @@ class Status extends ImmutablePureComponent { ...@@ -66,6 +67,7 @@ class Status extends ImmutablePureComponent {
otherAccounts: ImmutablePropTypes.list, otherAccounts: ImmutablePropTypes.list,
onClick: PropTypes.func, onClick: PropTypes.func,
onReply: PropTypes.func, onReply: PropTypes.func,
onQuote: PropTypes.func,
onFavourite: PropTypes.func, onFavourite: PropTypes.func,
onReblog: PropTypes.func, onReblog: PropTypes.func,
onDelete: PropTypes.func, onDelete: PropTypes.func,
...@@ -444,6 +446,10 @@ class Status extends ImmutablePureComponent { ...@@ -444,6 +446,10 @@ class Status extends ImmutablePureComponent {
{media} {media}
{status.get('quote') && <StatusQuote
id={status.get('quote')}
/>}
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && ( {showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
<button className='status__content__read-more-button' onClick={this.handleClick}> <button className='status__content__read-more-button' onClick={this.handleClick}>
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' /> <FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
......
...@@ -23,9 +23,11 @@ const messages = defineMessages({ ...@@ -23,9 +23,11 @@ const messages = defineMessages({
more: { id: 'status.more', defaultMessage: 'More' }, more: { id: 'status.more', defaultMessage: 'More' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Repost' }, reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' }, 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' }, favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
open: { id: 'status.open', defaultMessage: 'Expand this status' }, open: { id: 'status.open', defaultMessage: 'Expand this status' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' },
...@@ -51,6 +53,7 @@ class StatusActionBar extends ImmutablePureComponent { ...@@ -51,6 +53,7 @@ class StatusActionBar extends ImmutablePureComponent {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
onOpenUnauthorizedModal: PropTypes.func.isRequired, onOpenUnauthorizedModal: PropTypes.func.isRequired,
onReply: PropTypes.func, onReply: PropTypes.func,
onQuote: PropTypes.func,
onFavourite: PropTypes.func, onFavourite: PropTypes.func,
onReblog: PropTypes.func, onReblog: PropTypes.func,
onDelete: PropTypes.func, onDelete: PropTypes.func,
...@@ -82,6 +85,14 @@ class StatusActionBar extends ImmutablePureComponent { ...@@ -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 = () => { handleShareClick = () => {
navigator.share({ navigator.share({
text: this.props.status.get('search_index'), text: this.props.status.get('search_index'),
...@@ -283,6 +294,9 @@ class StatusActionBar extends ImmutablePureComponent { ...@@ -283,6 +294,9 @@ class StatusActionBar extends ImmutablePureComponent {
<IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> <IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
{reblogCount !== 0 && <Link to={`/${status.getIn(['account', 'acct'])}/posts/${status.get('id')}/reblogs`} className='detailed-status__link'>{reblogCount}</Link>} {reblogCount !== 0 && <Link to={`/${status.getIn(['account', 'acct'])}/posts/${status.get('id')}/reblogs`} className='detailed-status__link'>{reblogCount}</Link>}
</div> </div>
<div className='status__action-bar__counter'>
<IconButton className='status__action-bar-button' disabled={!publicStatus} title={!publicStatus ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-left' onClick={this.handleQuoteClick} />
</div>
<div className='status__action-bar__counter'> <div className='status__action-bar__counter'>
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{favoriteCount !== 0 && <span className='detailed-status__link'>{favoriteCount}</span>} {favoriteCount !== 0 && <span className='detailed-status__link'>{favoriteCount}</span>}
......
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import StatusContent from './status_content';
import DisplayName from './display_name';
import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
const mapStateToProps = (state, { id }) => ({
status: state.getIn(['statuses', id]),
account: state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
});
@connect(mapStateToProps)
export default class StatusQuote extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
};
render() {
const { status, account } = this.props;
const statusUrl = `/${account.get('acct')}/posts/${status.get('id')}`;
return (
<NavLink to={statusUrl} className="status__quote">
<DisplayName account={account} />
<StatusContent
status={status}
expanded={false}
/>
</NavLink>
);
}
}
\ No newline at end of file
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
replyCompose, replyCompose,
mentionCompose, mentionCompose,
directCompose, directCompose,
quoteCompose,
} from '../actions/compose'; } from '../actions/compose';
import { import {
reblog, reblog,
...@@ -42,6 +43,8 @@ const messages = defineMessages({ ...@@ -42,6 +43,8 @@ const messages = defineMessages({
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, 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?' }, 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' }, blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
}); });
...@@ -72,6 +75,21 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ ...@@ -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) { onModalReblog (status) {
if (status.get('reblogged')) { if (status.get('reblogged')) {
dispatch(unreblog(status)); dispatch(unreblog(status));
......
...@@ -21,6 +21,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; ...@@ -21,6 +21,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz'; import { length } from 'stringz';
import { countableText } from '../util/counter'; import { countableText } from '../util/counter';
import Icon from 'gabsocial/components/icon'; import Icon from 'gabsocial/components/icon';
import QuotedStatusPreviewContainer from '../containers/quoted_status_preview_container';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d'; const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
const maxPostCharacterCount = 3000; const maxPostCharacterCount = 3000;
...@@ -199,7 +200,7 @@ class ComposeForm extends ImmutablePureComponent { ...@@ -199,7 +200,7 @@ class ComposeForm extends ImmutablePureComponent {
} }
render () { render () {
const { intl, onPaste, showSearch, anyMedia, shouldCondense, autoFocus, isModalOpen } = this.props; const { intl, onPaste, showSearch, anyMedia, shouldCondense, autoFocus, isModalOpen, quoteOfId } = this.props;
const condensed = shouldCondense && !this.props.text && !this.state.composeFocused; const condensed = shouldCondense && !this.props.text && !this.state.composeFocused;
const disabled = this.props.isSubmitting; const disabled = this.props.isSubmitting;
const text = [this.props.spoilerText, countableText(this.props.text)].join(''); const text = [this.props.spoilerText, countableText(this.props.text)].join('');
...@@ -271,6 +272,8 @@ class ComposeForm extends ImmutablePureComponent { ...@@ -271,6 +272,8 @@ class ComposeForm extends ImmutablePureComponent {
} }
</AutosuggestTextarea> </AutosuggestTextarea>
{quoteOfId && <QuotedStatusPreviewContainer id={quoteOfId} />}
{ {
!condensed && !condensed &&
<div className='compose-form__buttons-wrapper'> <div className='compose-form__buttons-wrapper'>
......
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DisplayName from '../../../components/display_name';
import StatusContent from '../../../components/status_content';
export default class QuotedStatusPreview extends React.PureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
}
render() {
const { status, account } = this.props;
return (
<div className='compose-form__quote-preview'>
<DisplayName account={account} />
<StatusContent
status={status}
expanded={false}
/>
</div>
);
}
}
\ No newline at end of file
...@@ -26,6 +26,7 @@ const mapStateToProps = state => ({ ...@@ -26,6 +26,7 @@ const mapStateToProps = state => ({
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
isModalOpen: state.get('modal').modalType === 'COMPOSE', isModalOpen: state.get('modal').modalType === 'COMPOSE',
quoteOfId: state.getIn(['compose', 'quote_of_id']),
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
......
import { connect } from 'react-redux';
import QuotedStatusPreview from '../components/quoted_status_preview';
const mapStateToProps = (state, { id }) => ({
status: state.getIn(['statuses', id]),
account: state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
});
export default connect(mapStateToProps)(QuotedStatusPreview);
\ No newline at end of file
...@@ -15,9 +15,11 @@ const messages = defineMessages({ ...@@ -15,9 +15,11 @@ const messages = defineMessages({
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' }, reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Repost' }, reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' }, 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' }, favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' }, mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
...@@ -49,6 +51,7 @@ class ActionBar extends React.PureComponent { ...@@ -49,6 +51,7 @@ class ActionBar extends React.PureComponent {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
onReply: PropTypes.func.isRequired, onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired,
onQuote: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired,
...@@ -79,6 +82,14 @@ class ActionBar extends React.PureComponent { ...@@ -79,6 +82,14 @@ class ActionBar extends React.PureComponent {
} }
} }
handleQuoteClick = (e) => {
if (me) {
this.props.onQuote(this.props.status, e);
} else {
this.props.onOpenUnauthorizedModal();
}
}
handleFavouriteClick = () => { handleFavouriteClick = () => {
if (me) { if (me) {
this.props.onFavourite(this.props.status); this.props.onFavourite(this.props.status);
...@@ -216,6 +227,7 @@ class ActionBar extends React.PureComponent { ...@@ -216,6 +227,7 @@ class ActionBar extends React.PureComponent {
<div className='detailed-status__action-bar'> <div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div> <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} title={reblog_disabled ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-left' onClick={this.handleQuoteClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div> <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
{shareButton} {shareButton}
......
...@@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; ...@@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from '../../../components/avatar'; import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name'; import DisplayName from '../../../components/display_name';
import StatusContent from '../../../components/status_content'; import StatusContent from '../../../components/status_content';
import StatusQuote from '../../../components/status_quote';
import MediaGallery from '../../../components/media_gallery'; import MediaGallery from '../../../components/media_gallery';
import { Link, NavLink } from 'react-router-dom'; import { Link, NavLink } from 'react-router-dom';
import { FormattedDate, FormattedNumber } from 'react-intl'; import { FormattedDate, FormattedNumber } from 'react-intl';
...@@ -195,6 +196,10 @@ export default class DetailedStatus extends ImmutablePureComponent { ...@@ -195,6 +196,10 @@ export default class DetailedStatus extends ImmutablePureComponent {
{media} {media}
{status.get('quote') && <StatusQuote
id={status.get('quote')}
/>}
<div className='detailed-status__meta'> <div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'> <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
......
...@@ -21,6 +21,7 @@ import { ...@@ -21,6 +21,7 @@ import {
replyCompose, replyCompose,
mentionCompose, mentionCompose,
directCompose, directCompose,
quoteCompose,
} from '../../actions/compose'; } from '../../actions/compose';
import { blockAccount } from '../../actions/accounts'; import { blockAccount } from '../../actions/accounts';
import { import {
...@@ -189,6 +190,24 @@ class Status extends ImmutablePureComponent { ...@@ -189,6 +190,24 @@ class Status extends ImmutablePureComponent {
} }
} }
handleQuoteClick = (status) => {
let { dispatch, intl } = this.props;
const router = this.context.router.history;
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));
}
});
}
handleModalReblog = (status) => { handleModalReblog = (status) => {
this.props.dispatch(reblog(status)); this.props.dispatch(reblog(status));
} }
...@@ -489,6 +508,7 @@ class Status extends ImmutablePureComponent { ...@@ -489,6 +508,7 @@ class Status extends ImmutablePureComponent {
status={status} status={status}
onReply={this.handleReplyClick} onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick} onFavourite={this.handleFavouriteClick}
onQuote={this.handleQuoteClick}
onReblog={this.handleReblogClick} onReblog={this.handleReblogClick}
onDelete={this.handleDeleteClick} onDelete={this.handleDeleteClick}
onDirect={this.handleDirectClick} onDirect={this.handleDirectClick}
......
...@@ -4,6 +4,7 @@ import { ...@@ -4,6 +4,7 @@ import {
COMPOSE_CHANGE, COMPOSE_CHANGE,
COMPOSE_REPLY, COMPOSE_REPLY,
COMPOSE_REPLY_CANCEL, COMPOSE_REPLY_CANCEL,
COMPOSE_QUOTE,
COMPOSE_DIRECT, COMPOSE_DIRECT,
COMPOSE_MENTION, COMPOSE_MENTION,
COMPOSE_SUBMIT_REQUEST, COMPOSE_SUBMIT_REQUEST,
...@@ -55,6 +56,7 @@ const initialState = ImmutableMap({ ...@@ -55,6 +56,7 @@ const initialState = ImmutableMap({
caretPosition: null, caretPosition: null,
preselectDate: null, preselectDate: null,
in_reply_to: null, in_reply_to: null,
quote_of_id: null,
is_composing: false, is_composing: false,
is_submitting: false, is_submitting: false,
is_changing_upload: false, is_changing_upload: false,
...@@ -95,6 +97,7 @@ function clearAll(state) { ...@@ -95,6 +97,7 @@ function clearAll(state) {
map.set('is_submitting', false); map.set('is_submitting', false);
map.set('is_changing_upload', false); map.set('is_changing_upload', false);
map.set('in_reply_to', null); map.set('in_reply_to', null);
map.set('quote_of_id', null);
map.set('privacy', state.get('default_privacy')); map.set('privacy', state.get('default_privacy'));
map.set('sensitive', false); map.set('sensitive', false);
map.update('media_attachments', list => list.clear()); map.update('media_attachments', list => list.clear());
...@@ -247,6 +250,24 @@ export default function compose(state = initialState, action) { ...@@ -247,6 +250,24 @@ export default function compose(state = initialState, action) {
map.set('preselectDate', new Date()); map.set('preselectDate', new Date());
map.set('idempotencyKey', uuid()); 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());