Merge branch 'feature/quoting-statuses' into 'develop'
Feature/quoting statuses See merge request gab/social/gab-social!36
This commit is contained in:
commit
aed10a5a3b
@ -55,7 +55,8 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
application: doorkeeper_token.application,
|
||||
poll: status_params[:poll],
|
||||
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
|
||||
end
|
||||
@ -82,6 +83,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
params.permit(
|
||||
:status,
|
||||
:in_reply_to_id,
|
||||
:quote_of_id,
|
||||
:sensitive,
|
||||
:spoiler_text,
|
||||
:visibility,
|
||||
|
@ -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'], ''),
|
||||
|
@ -71,6 +71,10 @@ export function importFetchedStatuses(statuses) {
|
||||
processStatus(status.reblog);
|
||||
}
|
||||
|
||||
if (status.quote && status.quote.id) {
|
||||
processStatus(status.quote);
|
||||
}
|
||||
|
||||
if (status.poll && status.poll.id) {
|
||||
pushUnique(polls, normalizePoll(status.poll));
|
||||
}
|
||||
|
@ -43,6 +43,10 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||
normalStatus.reblog = status.reblog.id;
|
||||
}
|
||||
|
||||
if (status.quote && status.quote.id) {
|
||||
normalStatus.quote = status.quote.id;
|
||||
}
|
||||
|
||||
if (status.poll && status.poll.id) {
|
||||
normalStatus.poll = status.poll.id;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import AvatarComposite from './avatar_composite';
|
||||
import RelativeTimestamp from './relative_timestamp';
|
||||
import DisplayName from './display_name';
|
||||
import StatusContent from './status_content';
|
||||
import StatusQuote from './status_quote';
|
||||
import StatusActionBar from './status_action_bar';
|
||||
import AttachmentList from './attachment_list';
|
||||
import Card from '../features/status/components/card';
|
||||
@ -66,6 +67,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,
|
||||
@ -444,6 +446,10 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
{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']) && (
|
||||
<button className='status__content__read-more-button' onClick={this.handleClick}>
|
||||
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
|
||||
|
@ -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 {
|
||||
<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>}
|
||||
</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'>
|
||||
<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>}
|
||||
|
42
app/javascript/gabsocial/components/status_quote.js
Normal file
42
app/javascript/gabsocial/components/status_quote.js
Normal file
@ -0,0 +1,42 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
|
@ -21,6 +21,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { length } from 'stringz';
|
||||
import { countableText } from '../util/counter';
|
||||
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 maxPostCharacterCount = 3000;
|
||||
@ -199,7 +200,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
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 disabled = this.props.isSubmitting;
|
||||
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
|
||||
@ -271,6 +272,8 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
}
|
||||
</AutosuggestTextarea>
|
||||
|
||||
{quoteOfId && <QuotedStatusPreviewContainer id={quoteOfId} />}
|
||||
|
||||
{
|
||||
!condensed &&
|
||||
<div className='compose-form__buttons-wrapper'>
|
||||
|
@ -0,0 +1,26 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ const mapStateToProps = state => ({
|
||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
isModalOpen: state.get('modal').modalType === 'COMPOSE',
|
||||
quoteOfId: state.getIn(['compose', 'quote_of_id']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
@ -0,0 +1,9 @@
|
||||
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);
|
@ -15,9 +15,11 @@ const messages = defineMessages({
|
||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
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' },
|
||||
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
|
||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
@ -49,6 +51,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,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onDirect: PropTypes.func.isRequired,
|
||||
@ -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 = () => {
|
||||
if (me) {
|
||||
this.props.onFavourite(this.props.status);
|
||||
@ -216,6 +227,7 @@ class ActionBar extends React.PureComponent {
|
||||
<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 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>
|
||||
{shareButton}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import StatusContent from '../../../components/status_content';
|
||||
import StatusQuote from '../../../components/status_quote';
|
||||
import MediaGallery from '../../../components/media_gallery';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { FormattedDate, FormattedNumber } from 'react-intl';
|
||||
@ -195,6 +196,10 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||
|
||||
{media}
|
||||
|
||||
{status.get('quote') && <StatusQuote
|
||||
id={status.get('quote')}
|
||||
/>}
|
||||
|
||||
<div className='detailed-status__meta'>
|
||||
<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' />
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
replyCompose,
|
||||
mentionCompose,
|
||||
directCompose,
|
||||
quoteCompose,
|
||||
} from '../../actions/compose';
|
||||
import { blockAccount } from '../../actions/accounts';
|
||||
import {
|
||||
@ -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) => {
|
||||
this.props.dispatch(reblog(status));
|
||||
}
|
||||
@ -489,6 +508,7 @@ class Status extends ImmutablePureComponent {
|
||||
status={status}
|
||||
onReply={this.handleReplyClick}
|
||||
onFavourite={this.handleFavouriteClick}
|
||||
onQuote={this.handleQuoteClick}
|
||||
onReblog={this.handleReblogClick}
|
||||
onDelete={this.handleDeleteClick}
|
||||
onDirect={this.handleDirectClick}
|
||||
|
@ -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());
|
||||
|
@ -566,6 +566,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__quote {
|
||||
display: block;
|
||||
color: $primary-text-color;
|
||||
text-decoration: none;
|
||||
border: 1px solid $ui-secondary-color;
|
||||
border-radius: 10px;
|
||||
padding: 10px 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
font-size: 14px;
|
||||
color: $gab-secondary-text;
|
||||
|
@ -282,4 +282,17 @@
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__quote-preview {
|
||||
font-size: 14px;
|
||||
padding: 5px 10px 0;
|
||||
background: darken($simple-background-color, 8%);
|
||||
border-bottom: 1px solid darken($simple-background-color, 22%);
|
||||
color: #555;
|
||||
|
||||
.status__content {
|
||||
font-size: 0.9em;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
} // end .compose-form
|
@ -23,6 +23,7 @@
|
||||
# in_reply_to_account_id :bigint(8)
|
||||
# poll_id :bigint(8)
|
||||
# group_id :integer
|
||||
# quote_id :bigint(8)
|
||||
#
|
||||
|
||||
class Status < ApplicationRecord
|
||||
@ -51,9 +52,11 @@ class Status < ApplicationRecord
|
||||
|
||||
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
|
||||
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
|
||||
belongs_to :quote, foreign_key: 'quote_of_id', class_name: 'Status', inverse_of: :quotes, optional: true
|
||||
|
||||
has_many :favourites, inverse_of: :status, dependent: :destroy
|
||||
has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy
|
||||
has_many :quotes, foreign_key: 'quote_of_id', class_name: 'Status', inverse_of: :quote, dependent: :nullify
|
||||
has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread
|
||||
has_many :mentions, dependent: :destroy, inverse_of: :status
|
||||
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
|
||||
|
@ -11,7 +11,7 @@ class StatusRelationshipsPresenter
|
||||
@pins_map = {}
|
||||
else
|
||||
statuses = statuses.compact
|
||||
status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact
|
||||
status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id, s.quote_of_id] }.uniq.compact
|
||||
conversation_ids = statuses.map(&:conversation_id).compact.uniq
|
||||
pinnable_status_ids = statuses.map(&:proper).select { |s| s.account_id == current_account_id && %w(public unlisted).include?(s.visibility) }.map(&:id)
|
||||
|
||||
|
@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
|
||||
:sensitive, :spoiler_text, :visibility, :language,
|
||||
:uri, :url, :replies_count, :reblogs_count,
|
||||
:favourites_count
|
||||
:favourites_count, :quote_of_id
|
||||
|
||||
attribute :favourited, if: :current_user?
|
||||
attribute :reblogged, if: :current_user?
|
||||
@ -15,6 +15,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
attribute :text, if: :source_requested?
|
||||
|
||||
belongs_to :reblog, serializer: REST::StatusSerializer
|
||||
belongs_to :quote, serializer: REST::StatusSerializer
|
||||
belongs_to :application, if: :show_application?
|
||||
belongs_to :account, serializer: REST::AccountSerializer
|
||||
belongs_to :group, serializer: REST::GroupSerializer
|
||||
@ -39,6 +40,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
object.in_reply_to_account_id&.to_s
|
||||
end
|
||||
|
||||
def quote_of_id
|
||||
object.quote_of_id&.to_s
|
||||
end
|
||||
|
||||
def current_user?
|
||||
!current_user.nil?
|
||||
end
|
||||
|
@ -164,6 +164,7 @@ class PostStatusService < BaseService
|
||||
{
|
||||
text: @text,
|
||||
group_id: @options[:group_id],
|
||||
quote_of_id: @options[:quote_of_id],
|
||||
media_attachments: @media || [],
|
||||
thread: @in_reply_to,
|
||||
poll_attributes: poll_attributes,
|
||||
|
7
db/migrate/20190804115634_add_quote_id_to_statuses.rb
Normal file
7
db/migrate/20190804115634_add_quote_id_to_statuses.rb
Normal file
@ -0,0 +1,7 @@
|
||||
class AddQuoteIdToStatuses < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
safety_assured {
|
||||
add_reference :statuses, :quote_of, foreign_key: { on_delete: :nullify, to_table: :statuses }
|
||||
}
|
||||
end
|
||||
end
|
15
db/schema.rb
15
db/schema.rb
@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2019_07_22_003649) do
|
||||
ActiveRecord::Schema.define(version: 2019_08_04_115634) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@ -93,7 +93,7 @@ ActiveRecord::Schema.define(version: 2019_07_22_003649) do
|
||||
t.bigint "account_id"
|
||||
t.string "image_file_name"
|
||||
t.string "image_content_type"
|
||||
t.bigint "image_file_size"
|
||||
t.integer "image_file_size"
|
||||
t.datetime "image_updated_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
@ -157,10 +157,10 @@ ActiveRecord::Schema.define(version: 2019_07_22_003649) do
|
||||
t.string "actor_type"
|
||||
t.boolean "discoverable"
|
||||
t.string "also_known_as", array: true
|
||||
t.datetime "silenced_at"
|
||||
t.datetime "suspended_at"
|
||||
t.boolean "is_pro", default: false, null: false
|
||||
t.datetime "pro_expires_at"
|
||||
t.datetime "silenced_at"
|
||||
t.datetime "suspended_at"
|
||||
t.boolean "is_verified", default: false, null: false
|
||||
t.boolean "is_donor", default: false, null: false
|
||||
t.boolean "is_investor", default: false, null: false
|
||||
@ -658,8 +658,8 @@ ActiveRecord::Schema.define(version: 2019_07_22_003649) do
|
||||
create_table "status_pins", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.bigint "status_id", null: false
|
||||
t.datetime "created_at", default: -> { "now()" }, null: false
|
||||
t.datetime "updated_at", default: -> { "now()" }, null: false
|
||||
t.datetime "created_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
|
||||
t.datetime "updated_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
|
||||
t.index ["account_id", "status_id"], name: "index_status_pins_on_account_id_and_status_id", unique: true
|
||||
end
|
||||
|
||||
@ -693,10 +693,12 @@ ActiveRecord::Schema.define(version: 2019_07_22_003649) do
|
||||
t.bigint "in_reply_to_account_id"
|
||||
t.bigint "poll_id"
|
||||
t.integer "group_id"
|
||||
t.bigint "quote_of_id"
|
||||
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20180106", order: { id: :desc }
|
||||
t.index ["group_id"], name: "index_statuses_on_group_id"
|
||||
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id"
|
||||
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"
|
||||
t.index ["quote_of_id"], name: "index_statuses_on_quote_of_id"
|
||||
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
|
||||
t.index ["uri"], name: "index_statuses_on_uri", unique: true
|
||||
end
|
||||
@ -905,6 +907,7 @@ ActiveRecord::Schema.define(version: 2019_07_22_003649) do
|
||||
add_foreign_key "statuses", "accounts", name: "fk_9bda1543f7", on_delete: :cascade
|
||||
add_foreign_key "statuses", "groups", on_delete: :nullify
|
||||
add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify
|
||||
add_foreign_key "statuses", "statuses", column: "quote_of_id", on_delete: :nullify
|
||||
add_foreign_key "statuses", "statuses", column: "reblog_of_id", on_delete: :cascade
|
||||
add_foreign_key "statuses_tags", "statuses", on_delete: :cascade
|
||||
add_foreign_key "statuses_tags", "tags", name: "fk_3081861e21", on_delete: :cascade
|
||||
|
Loading…
x
Reference in New Issue
Block a user