th: quotes
Maintained-by: kouhai <kouhai@treehouse.systems> Bug-fixed-by: ptrcnull <git@ptrcnull.me>main
parent
56e347d149
commit
6f292ec185
|
@ -76,7 +76,8 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
content_type: status_params[:content_type],
|
content_type: status_params[:content_type],
|
||||||
allowed_mentions: status_params[:allowed_mentions],
|
allowed_mentions: status_params[:allowed_mentions],
|
||||||
idempotency: request.headers['Idempotency-Key'],
|
idempotency: request.headers['Idempotency-Key'],
|
||||||
with_rate_limit: true
|
with_rate_limit: true,
|
||||||
|
quote_id: status_params[:quote_id].presence
|
||||||
)
|
)
|
||||||
|
|
||||||
render json: @status, serializer: serializer_for_status
|
render json: @status, serializer: serializer_for_status
|
||||||
|
@ -159,6 +160,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
:visibility,
|
:visibility,
|
||||||
:language,
|
:language,
|
||||||
:scheduled_at,
|
:scheduled_at,
|
||||||
|
:quote_id,
|
||||||
:content_type,
|
:content_type,
|
||||||
allowed_mentions: [],
|
allowed_mentions: [],
|
||||||
media_ids: [],
|
media_ids: [],
|
||||||
|
|
|
@ -42,6 +42,7 @@ module ContextHelper
|
||||||
'cipherText' => 'toot:cipherText',
|
'cipherText' => 'toot:cipherText',
|
||||||
},
|
},
|
||||||
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
||||||
|
quote_uri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' },
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def full_context
|
def full_context
|
||||||
|
|
|
@ -19,7 +19,17 @@ module FormattingHelper
|
||||||
module_function :extract_status_plain_text
|
module_function :extract_status_plain_text
|
||||||
|
|
||||||
def status_content_format(status)
|
def status_content_format(status)
|
||||||
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
|
base = html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
|
||||||
|
|
||||||
|
if status.quote? && status.local?
|
||||||
|
after_html = begin
|
||||||
|
"<span class=\"quote-inline\"><a href=\"#{status.quote.to_log_permalink}\" class=\"status-link unhandled-link\" target=\"_blank\">#{status.quote.to_log_permalink}</a></span>"
|
||||||
|
end.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
|
|
||||||
|
base + after_html
|
||||||
|
else
|
||||||
|
base
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def rss_status_content_format(status)
|
def rss_status_content_format(status)
|
||||||
|
|
|
@ -86,6 +86,9 @@ export const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER';
|
||||||
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
||||||
export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
|
export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
|
||||||
|
|
||||||
|
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
|
||||||
|
export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
|
@ -136,6 +139,25 @@ export function cancelReplyCompose() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function quoteCompose(status, router) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
|
type: COMPOSE_QUOTE,
|
||||||
|
status: status,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!getState().getIn(['compose', 'mounted'])) {
|
||||||
|
router.push('/publish');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelQuoteCompose() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_QUOTE_CANCEL,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function resetCompose() {
|
export function resetCompose() {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_RESET,
|
type: COMPOSE_RESET,
|
||||||
|
@ -218,6 +240,7 @@ export function submitCompose(routerHistory, overridePrivacy = null) {
|
||||||
status,
|
status,
|
||||||
content_type: getState().getIn(['compose', 'content_type']),
|
content_type: getState().getIn(['compose', 'content_type']),
|
||||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||||
|
quote_id: getState().getIn(['compose', 'quote_id'], null),
|
||||||
media_ids: media.map(item => item.get('id')),
|
media_ids: media.map(item => item.get('id')),
|
||||||
media_attributes,
|
media_attributes,
|
||||||
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
|
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
|
||||||
|
|
|
@ -59,6 +59,8 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
||||||
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
|
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
|
||||||
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
||||||
normalStatus.hidden = normalOldStatus.get('hidden');
|
normalStatus.hidden = normalOldStatus.get('hidden');
|
||||||
|
normalStatus.quote = normalOldStatus.get('quote');
|
||||||
|
normalStatus.quote_hidden = normalOldStatus.get('quote_hidden');
|
||||||
|
|
||||||
if (normalOldStatus.get('translation')) {
|
if (normalOldStatus.get('translation')) {
|
||||||
normalStatus.translation = normalOldStatus.get('translation');
|
normalStatus.translation = normalOldStatus.get('translation');
|
||||||
|
@ -72,6 +74,35 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
||||||
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
||||||
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
||||||
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
|
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
|
||||||
|
|
||||||
|
if (status.quote && status.quote.id) {
|
||||||
|
const quote_spoilerText = status.quote.spoiler_text || '';
|
||||||
|
const quote_searchContent = [quote_spoilerText, status.quote.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||||
|
|
||||||
|
const quote_emojiMap = makeEmojiMap(normalStatus.quote.emojis);
|
||||||
|
|
||||||
|
const quote_account_emojiMap = makeEmojiMap(status.quote.account.emojis);
|
||||||
|
const displayName = normalStatus.quote.account.display_name.length === 0 ? normalStatus.quote.account.username : normalStatus.quote.account.display_name;
|
||||||
|
normalStatus.quote.account.display_name_html = emojify(escapeTextContentForBrowser(displayName), quote_account_emojiMap);
|
||||||
|
normalStatus.quote.search_index = domParser.parseFromString(quote_searchContent, 'text/html').documentElement.textContent;
|
||||||
|
let docElem = domParser.parseFromString(normalStatus.quote.content, 'text/html').documentElement;
|
||||||
|
Array.from(docElem.querySelectorAll('span.quote-inline'), span => span.remove());
|
||||||
|
Array.from(docElem.querySelectorAll('p,br'), line => {
|
||||||
|
let parentNode = line.parentNode;
|
||||||
|
if (line.nextSibling) {
|
||||||
|
parentNode.insertBefore(document.createTextNode(' '), line.nextSibling);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let _contentHtml = docElem.textContent;
|
||||||
|
normalStatus.quote.contentHtml = '<p>'+emojify(_contentHtml.substr(0, 150), quote_emojiMap) + (_contentHtml.substr(150) ? '...' : '')+'</p>';
|
||||||
|
normalStatus.quote.spoilerHtml = emojify(escapeTextContentForBrowser(quote_spoilerText), quote_emojiMap);
|
||||||
|
normalStatus.quote_hidden = (quote_spoilerText.length > 0 || normalStatus.quote.sensitive) && autoHideCW(settings, quote_spoilerText);
|
||||||
|
|
||||||
|
// delete the quote link!!!!
|
||||||
|
let parentDocElem = domParser.parseFromString(normalStatus.contentHtml, 'text/html').documentElement;
|
||||||
|
Array.from(parentDocElem.querySelectorAll('span.quote-inline'), span => span.remove());
|
||||||
|
normalStatus.contentHtml = parentDocElem.children[1].innerHTML;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalOldStatus) {
|
if (normalOldStatus) {
|
||||||
|
|
|
@ -85,6 +85,7 @@ class Status extends ImmutablePureComponent {
|
||||||
rootId: PropTypes.string,
|
rootId: PropTypes.string,
|
||||||
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,
|
||||||
onBookmark: PropTypes.func,
|
onBookmark: PropTypes.func,
|
||||||
|
@ -732,7 +733,7 @@ class Status extends ImmutablePureComponent {
|
||||||
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
|
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
|
||||||
background = attachments.getIn([0, 'preview_url']);
|
background = attachments.getIn([0, 'preview_url']);
|
||||||
}
|
}
|
||||||
} else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) {
|
} else if (!status.get('quote') && status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) {
|
||||||
media.push(
|
media.push(
|
||||||
<Card
|
<Card
|
||||||
onOpenMedia={this.handleOpenMedia}
|
onOpenMedia={this.handleOpenMedia}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
|
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
|
||||||
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
|
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
|
||||||
|
import FormatQuoteIcon from '@/material-icons/400-24px/format_quote-fill.svg?react';
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||||
|
@ -46,6 +47,7 @@ const messages = defineMessages({
|
||||||
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
||||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
||||||
|
quote: { id: 'status.quote', defaultMessage: 'Quote' },
|
||||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
|
||||||
|
@ -74,6 +76,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
onReply: PropTypes.func,
|
onReply: PropTypes.func,
|
||||||
onFavourite: PropTypes.func,
|
onFavourite: PropTypes.func,
|
||||||
onReblog: PropTypes.func,
|
onReblog: PropTypes.func,
|
||||||
|
onQuote: PropTypes.func,
|
||||||
onDelete: PropTypes.func,
|
onDelete: PropTypes.func,
|
||||||
onDirect: PropTypes.func,
|
onDirect: PropTypes.func,
|
||||||
onMention: PropTypes.func,
|
onMention: PropTypes.func,
|
||||||
|
@ -114,6 +117,17 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleQuoteClick = () => {
|
||||||
|
const { signedIn } = this.props.identity;
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
|
this.props.onQuote(this.props.status, this.props.history);
|
||||||
|
} else {
|
||||||
|
// TODO(ariadne): Add an interaction modal for quoting specifically.
|
||||||
|
this.props.onInteractionModal('reply', this.props.status);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleShareClick = () => {
|
handleShareClick = () => {
|
||||||
navigator.share({
|
navigator.share({
|
||||||
url: this.props.status.get('url'),
|
url: this.props.status.get('url'),
|
||||||
|
@ -217,6 +231,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
let reblogIcon = 'retweet';
|
let reblogIcon = 'retweet';
|
||||||
|
let quoteIcon = 'quote-right';
|
||||||
let replyIcon;
|
let replyIcon;
|
||||||
let replyIconComponent;
|
let replyIconComponent;
|
||||||
let replyTitle;
|
let replyTitle;
|
||||||
|
@ -299,6 +314,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
|
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
|
||||||
|
|
||||||
let reblogTitle, reblogIconComponent;
|
let reblogTitle, reblogIconComponent;
|
||||||
|
let quoteTitle, quoteIconComponent;
|
||||||
|
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
|
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
|
||||||
|
@ -314,6 +330,19 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
reblogIconComponent = RepeatDisabledIcon;
|
reblogIconComponent = RepeatDisabledIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// quotes
|
||||||
|
if (publicStatus) {
|
||||||
|
quoteTitle = intl.formatMessage(messages.quote);
|
||||||
|
quoteIconComponent = FormatQuoteIcon;
|
||||||
|
} else if (reblogPrivate) {
|
||||||
|
quoteTitle = intl.formatMessage(messages.reblog_private);
|
||||||
|
quoteIconComponent = FormatQuoteIcon;
|
||||||
|
} else {
|
||||||
|
quoteTitle = intl.formatMessage(messages.cannot_reblog);
|
||||||
|
quoteIconComponent = FormatQuoteIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const filterButton = this.props.onFilter && (
|
const filterButton = this.props.onFilter && (
|
||||||
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
|
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
|
||||||
);
|
);
|
||||||
|
@ -329,7 +358,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
counter={showReplyCount ? status.get('replies_count') : undefined}
|
counter={showReplyCount ? status.get('replies_count') : undefined}
|
||||||
obfuscateCount
|
obfuscateCount
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
||||||
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} /* active={status.get('reblogged')} */ title={quoteTitle} icon={quoteIcon} iconComponent={quoteIconComponent} onClick={this.handleQuoteClick} />
|
||||||
|
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||||
<IconButton className='status__action-bar-button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
|
<IconButton className='status__action-bar-button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
|
||||||
import LinkIcon from '@/material-icons/400-24px/link.svg?react';
|
import LinkIcon from '@/material-icons/400-24px/link.svg?react';
|
||||||
import MovieIcon from '@/material-icons/400-24px/movie.svg?react';
|
import MovieIcon from '@/material-icons/400-24px/movie.svg?react';
|
||||||
import MusicNoteIcon from '@/material-icons/400-24px/music_note.svg?react';
|
import MusicNoteIcon from '@/material-icons/400-24px/music_note.svg?react';
|
||||||
|
import QuoteIcon from '@/material-icons/400-24px/format_quote-fill.svg?react';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
||||||
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
|
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
|
||||||
|
@ -361,6 +362,37 @@ class StatusContent extends PureComponent {
|
||||||
<TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} />
|
<TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let quote = '';
|
||||||
|
|
||||||
|
if (status.get('quote', null) !== null) {
|
||||||
|
let quoteStatus = status.get('quote');
|
||||||
|
let quoteStatusContent = { __html: quoteStatus.get('contentHtml') };
|
||||||
|
let quoteStatusAccount = quoteStatus.get('account');
|
||||||
|
let quoteStatusDisplayName = { __html: quoteStatusAccount.get('display_name_html') };
|
||||||
|
|
||||||
|
quote = (
|
||||||
|
<div className='status__quote'>
|
||||||
|
<blockquote>
|
||||||
|
<bdi>
|
||||||
|
<span className='quote-display-name'>
|
||||||
|
<Icon
|
||||||
|
fixedWidth
|
||||||
|
aria-hidden='true'
|
||||||
|
key='icon-quote-right'
|
||||||
|
icon={QuoteIcon} />
|
||||||
|
<strong className='display-name__html'>
|
||||||
|
<a onClick={this.handleAccountClick} href={quoteStatus.getIn(['account', 'url'])} dangerouslySetInnerHTML={quoteStatusDisplayName} />
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
</bdi>
|
||||||
|
<div>
|
||||||
|
<a href={quoteStatus.get('url')} target='_blank' rel='noopener noreferrer' dangerouslySetInnerHTML={quoteStatusContent} />
|
||||||
|
</div>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (status.get('spoiler_text').length > 0) {
|
if (status.get('spoiler_text').length > 0) {
|
||||||
let mentionsPlaceholder = '';
|
let mentionsPlaceholder = '';
|
||||||
|
|
||||||
|
@ -435,6 +467,7 @@ class StatusContent extends PureComponent {
|
||||||
{mentionsPlaceholder}
|
{mentionsPlaceholder}
|
||||||
|
|
||||||
<div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
|
<div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
|
||||||
|
{quote}
|
||||||
<div
|
<div
|
||||||
ref={this.setContentsRef}
|
ref={this.setContentsRef}
|
||||||
key={`contents-${tagLinks}`}
|
key={`contents-${tagLinks}`}
|
||||||
|
@ -460,6 +493,7 @@ class StatusContent extends PureComponent {
|
||||||
onMouseUp={this.handleMouseUp}
|
onMouseUp={this.handleMouseUp}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
|
{quote}
|
||||||
<div
|
<div
|
||||||
ref={this.setContentsRef}
|
ref={this.setContentsRef}
|
||||||
key={`contents-${tagLinks}-${rewriteMentions}`}
|
key={`contents-${tagLinks}-${rewriteMentions}`}
|
||||||
|
@ -481,6 +515,7 @@ class StatusContent extends PureComponent {
|
||||||
className='status__content'
|
className='status__content'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
|
{quote}
|
||||||
<div
|
<div
|
||||||
ref={this.setContentsRef}
|
ref={this.setContentsRef}
|
||||||
key={`contents-${tagLinks}`}
|
key={`contents-${tagLinks}`}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||||
import {
|
import {
|
||||||
replyCompose,
|
replyCompose,
|
||||||
|
quoteCompose,
|
||||||
mentionCompose,
|
mentionCompose,
|
||||||
directCompose,
|
directCompose,
|
||||||
} from 'flavours/glitch/actions/compose';
|
} from 'flavours/glitch/actions/compose';
|
||||||
|
@ -51,6 +52,8 @@ const messages = defineMessages({
|
||||||
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?' },
|
||||||
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
|
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
|
||||||
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing 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?' },
|
||||||
unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' },
|
unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' },
|
||||||
author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
|
author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
|
||||||
matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' },
|
matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' },
|
||||||
|
@ -113,6 +116,26 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onQuote (status, router) {
|
||||||
|
dispatch((_, getState) => {
|
||||||
|
let state = getState();
|
||||||
|
|
||||||
|
if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalType: 'CONFIRM',
|
||||||
|
modalProps: {
|
||||||
|
message: intl.formatMessage(messages.quoteMessage),
|
||||||
|
confirm: intl.formatMessage(messages.quoteConfirm),
|
||||||
|
onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)),
|
||||||
|
onConfirm: () => dispatch(quoteCompose(status, router)),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
dispatch(quoteCompose(status, router));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onModalReblog (status, privacy) {
|
onModalReblog (status, privacy) {
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
dispatch(unreblog({ statusId: status.get('id') }));
|
dispatch(unreblog({ statusId: status.get('id') }));
|
||||||
|
|
|
@ -257,6 +257,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<form className='compose-form' onSubmit={this.handleSubmit}>
|
<form className='compose-form' onSubmit={this.handleSubmit}>
|
||||||
<ReplyIndicator />
|
<ReplyIndicator />
|
||||||
|
{/* <QuoteIndicatorContainer /> */}
|
||||||
{!withoutNavigation && <NavigationBar />}
|
{!withoutNavigation && <NavigationBar />}
|
||||||
<WarningContainer />
|
<WarningContainer />
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
// Package imports.
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
|
|
||||||
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
|
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||||
|
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
|
import { Avatar } from '../../../components/avatar';
|
||||||
|
import { DisplayName } from '../../../components/display_name';
|
||||||
|
import { Icon } from '../../../components/icon';
|
||||||
|
import { IconButton } from '../../../components/icon_button';
|
||||||
|
|
||||||
|
// Messages.
|
||||||
|
const messages = defineMessages({
|
||||||
|
cancel: {
|
||||||
|
defaultMessage: 'Cancel',
|
||||||
|
id: 'quote_indicator.cancel',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class QuoteIndicator extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
status: ImmutablePropTypes.map,
|
||||||
|
onCancel: PropTypes.func,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
...WithOptionalRouterPropTypes,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleAccountClick = (e) => {
|
||||||
|
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.history?.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rendering.
|
||||||
|
render () {
|
||||||
|
const { status, intl } = this.props;
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = { __html: status.get('contentHtml') };
|
||||||
|
|
||||||
|
// The result.
|
||||||
|
return (
|
||||||
|
<article className='quote-indicator'>
|
||||||
|
<header className='quote-indicator__header'>
|
||||||
|
<div className='quote-indicator__cancel'>
|
||||||
|
<IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={this.handleClick} inverted />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='quote-indicator__display-name' target='_blank' rel='noopener noreferrer'>
|
||||||
|
<div className='quote-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
|
||||||
|
<DisplayName account={status.get('account')} inline />
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className='quote-indicator__content translate' dangerouslySetInnerHTML={content} />
|
||||||
|
|
||||||
|
{status.get('media_attachments').size > 0 && (
|
||||||
|
<AttachmentList
|
||||||
|
compact
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withOptionalRouter(injectIntl(QuoteIndicator));
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { cancelQuoteCompose } from 'flavours/glitch/actions/compose';
|
||||||
|
import { makeGetStatus } from '../../../selectors';
|
||||||
|
import QuoteIndicator from '../components/quote_indicator';
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getStatus = makeGetStatus();
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const statusId = state.getIn(['compose', 'quote_id'], null);
|
||||||
|
const editing = false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: getStatus(state, { id: statusId }),
|
||||||
|
editing,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
|
onCancel () {
|
||||||
|
dispatch(cancelQuoteCompose());
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps, mapDispatchToProps)(QuoteIndicator);
|
|
@ -11,6 +11,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
|
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
|
||||||
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
|
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
|
import QuoteIcon from '@/material-icons/400-24px/format_quote-fill.svg?react';
|
||||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||||
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
|
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
|
||||||
|
@ -38,6 +39,7 @@ const messages = defineMessages({
|
||||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
||||||
|
quote: { id: 'status.quote', defaultMessage: 'Quote' },
|
||||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
|
||||||
|
@ -71,6 +73,7 @@ class ActionBar extends PureComponent {
|
||||||
onEdit: PropTypes.func.isRequired,
|
onEdit: PropTypes.func.isRequired,
|
||||||
onDirect: PropTypes.func.isRequired,
|
onDirect: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
|
onQuote: PropTypes.func.isRequired,
|
||||||
onMute: PropTypes.func,
|
onMute: PropTypes.func,
|
||||||
onBlock: PropTypes.func,
|
onBlock: PropTypes.func,
|
||||||
onMuteConversation: PropTypes.func,
|
onMuteConversation: PropTypes.func,
|
||||||
|
@ -97,6 +100,10 @@ class ActionBar extends PureComponent {
|
||||||
this.props.onBookmark(this.props.status, e);
|
this.props.onBookmark(this.props.status, e);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleQuoteClick = () => {
|
||||||
|
this.props.onQuote(this.props.status);
|
||||||
|
}
|
||||||
|
|
||||||
handleDeleteClick = () => {
|
handleDeleteClick = () => {
|
||||||
this.props.onDelete(this.props.status, this.props.history);
|
this.props.onDelete(this.props.status, this.props.history);
|
||||||
};
|
};
|
||||||
|
@ -250,6 +257,7 @@ class ActionBar extends 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={replyIcon} iconComponent={replyIconComponent} onClick={this.handleReplyClick} /></div>
|
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={replyIcon} iconComponent={replyIconComponent} onClick={this.handleReplyClick} /></div>
|
||||||
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} /></div>
|
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} /></div>
|
||||||
|
<div className='detailed-status__button'><IconButton className='quote-right-icon' disabled={!publicStatus} title={intl.formatMessage(messages.quote)} icon='quote-right' iconComponent={QuoteIcon} 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' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} 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' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div>
|
||||||
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div>
|
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div>
|
||||||
|
|
||||||
|
|
|
@ -221,7 +221,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
mediaIcons.push('picture-o');
|
mediaIcons.push('picture-o');
|
||||||
}
|
}
|
||||||
} else if (status.get('card')) {
|
} else if (!status.get('quote') && status.get('card')) {
|
||||||
media.push(<Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />);
|
media.push(<Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />);
|
||||||
mediaIcons.push('link');
|
mediaIcons.push('link');
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { showAlertForError } from '../../../actions/alerts';
|
||||||
import { initBlockModal } from '../../../actions/blocks';
|
import { initBlockModal } from '../../../actions/blocks';
|
||||||
import {
|
import {
|
||||||
replyCompose,
|
replyCompose,
|
||||||
|
quoteCompose,
|
||||||
mentionCompose,
|
mentionCompose,
|
||||||
directCompose,
|
directCompose,
|
||||||
} from '../../../actions/compose';
|
} from '../../../actions/compose';
|
||||||
|
@ -32,6 +33,8 @@ import DetailedStatus from '../components/detailed_status';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||||
|
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?' },
|
||||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
||||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
|
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
|
||||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||||
|
@ -70,6 +73,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, privacy) {
|
onModalReblog (status, privacy) {
|
||||||
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
|
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
|
||||||
},
|
},
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
import { initBlockModal } from '../../actions/blocks';
|
import { initBlockModal } from '../../actions/blocks';
|
||||||
import {
|
import {
|
||||||
replyCompose,
|
replyCompose,
|
||||||
|
quoteCompose,
|
||||||
mentionCompose,
|
mentionCompose,
|
||||||
directCompose,
|
directCompose,
|
||||||
} from '../../actions/compose';
|
} from '../../actions/compose';
|
||||||
|
@ -342,6 +343,21 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleQuoteClick = (status) => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const { signedIn } = this.props.identity;
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
|
dispatch(quoteCompose(status, this.props.history));
|
||||||
|
} else {
|
||||||
|
dispatch(openModal('INTERACTION', {
|
||||||
|
type: 'reply',
|
||||||
|
accountId: status.getIn(['account', 'id']),
|
||||||
|
url: status.get('url'),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleModalReblog = (status, privacy) => {
|
handleModalReblog = (status, privacy) => {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
|
@ -760,6 +776,7 @@ class Status extends ImmutablePureComponent {
|
||||||
onFavourite={this.handleFavouriteClick}
|
onFavourite={this.handleFavouriteClick}
|
||||||
onReblog={this.handleReblogClick}
|
onReblog={this.handleReblogClick}
|
||||||
onBookmark={this.handleBookmarkClick}
|
onBookmark={this.handleBookmarkClick}
|
||||||
|
onQuote={this.handleQuoteClick}
|
||||||
onDelete={this.handleDeleteClick}
|
onDelete={this.handleDeleteClick}
|
||||||
onEdit={this.handleEditClick}
|
onEdit={this.handleEditClick}
|
||||||
onDirect={this.handleDirectClick}
|
onDirect={this.handleDirectClick}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {
|
||||||
COMPOSE_REPLY,
|
COMPOSE_REPLY,
|
||||||
COMPOSE_REPLY_CANCEL,
|
COMPOSE_REPLY_CANCEL,
|
||||||
COMPOSE_DIRECT,
|
COMPOSE_DIRECT,
|
||||||
|
COMPOSE_QUOTE,
|
||||||
|
COMPOSE_QUOTE_CANCEL,
|
||||||
COMPOSE_MENTION,
|
COMPOSE_MENTION,
|
||||||
COMPOSE_SUBMIT_REQUEST,
|
COMPOSE_SUBMIT_REQUEST,
|
||||||
COMPOSE_SUBMIT_SUCCESS,
|
COMPOSE_SUBMIT_SUCCESS,
|
||||||
|
@ -80,6 +82,7 @@ const initialState = ImmutableMap({
|
||||||
caretPosition: null,
|
caretPosition: null,
|
||||||
preselectDate: null,
|
preselectDate: null,
|
||||||
in_reply_to: null,
|
in_reply_to: null,
|
||||||
|
quote_id: null,
|
||||||
is_composing: false,
|
is_composing: false,
|
||||||
is_submitting: false,
|
is_submitting: false,
|
||||||
is_changing_upload: false,
|
is_changing_upload: false,
|
||||||
|
@ -169,6 +172,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_id', null);
|
||||||
map.update(
|
map.update(
|
||||||
'advanced_options',
|
'advanced_options',
|
||||||
map => map.mergeWith(overwrite, state.get('default_advanced_options')),
|
map => map.mergeWith(overwrite, state.get('default_advanced_options')),
|
||||||
|
@ -358,6 +362,51 @@ const updateSuggestionTags = (state, token) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateWithReply = (state, action) => {
|
||||||
|
// doesn't support QT&reply
|
||||||
|
const isQuote = action.type === COMPOSE_QUOTE;
|
||||||
|
const parentStatusId = action.status.get('id');
|
||||||
|
|
||||||
|
return state.withMutations(map => {
|
||||||
|
map.set('id', null);
|
||||||
|
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||||
|
map.update(
|
||||||
|
'advanced_options',
|
||||||
|
map => map.merge(new ImmutableMap({ do_not_federate: !!action.status.get('local_only') })),
|
||||||
|
);
|
||||||
|
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) {
|
||||||
|
let spoilerText = action.status.get('spoiler_text');
|
||||||
|
if (action.prependCWRe && !spoilerText.match(/^(re|qt)[: ]/i)) {
|
||||||
|
spoilerText = isQuote ? `QT: ${spoilerText}` : `re: ${spoilerText}`;
|
||||||
|
}
|
||||||
|
map.set('spoiler', true);
|
||||||
|
map.set('spoiler_text', spoilerText);
|
||||||
|
} else {
|
||||||
|
map.set('spoiler', false);
|
||||||
|
map.set('spoiler_text', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isQuote) {
|
||||||
|
map.set('in_reply_to', null);
|
||||||
|
map.set('quote_id', parentStatusId);
|
||||||
|
map.set('text', '');
|
||||||
|
} else {
|
||||||
|
map.set('in_reply_to', parentStatusId);
|
||||||
|
map.set('quote_id', null);
|
||||||
|
map.set('text', statusToTextMentions(state, action.status));
|
||||||
|
|
||||||
|
if (action.status.get('language')) {
|
||||||
|
map.set('language', action.status.get('language'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const updatePoll = (state, index, value, maxOptions) => state.updateIn(['poll', 'options'], options => {
|
const updatePoll = (state, index, value, maxOptions) => state.updateIn(['poll', 'options'], options => {
|
||||||
const tmp = options.set(index, value).filterNot(x => x.trim().length === 0);
|
const tmp = options.set(index, value).filterNot(x => x.trim().length === 0);
|
||||||
|
|
||||||
|
@ -420,46 +469,17 @@ export default function compose(state = initialState, action) {
|
||||||
case COMPOSE_COMPOSING_CHANGE:
|
case COMPOSE_COMPOSING_CHANGE:
|
||||||
return state.set('is_composing', action.value);
|
return state.set('is_composing', action.value);
|
||||||
case COMPOSE_REPLY:
|
case COMPOSE_REPLY:
|
||||||
return state.withMutations(map => {
|
case COMPOSE_QUOTE:
|
||||||
map.set('id', null);
|
return updateWithReply(state, action);
|
||||||
map.set('in_reply_to', action.status.get('id'));
|
|
||||||
map.set('text', statusToTextMentions(state, action.status));
|
|
||||||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
|
||||||
map.update(
|
|
||||||
'advanced_options',
|
|
||||||
map => map.merge(new ImmutableMap({ do_not_federate: !!action.status.get('local_only') })),
|
|
||||||
);
|
|
||||||
map.set('focusDate', new Date());
|
|
||||||
map.set('caretPosition', null);
|
|
||||||
map.set('preselectDate', new Date());
|
|
||||||
map.set('idempotencyKey', uuid());
|
|
||||||
|
|
||||||
map.update('media_attachments', list => list.filter(media => media.get('unattached')));
|
|
||||||
|
|
||||||
if (action.status.get('language') && !action.status.has('translation')) {
|
|
||||||
map.set('language', action.status.get('language'));
|
|
||||||
} else {
|
|
||||||
map.set('language', state.get('default_language'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.status.get('spoiler_text').length > 0) {
|
|
||||||
let spoiler_text = action.status.get('spoiler_text');
|
|
||||||
if (action.prependCWRe && !spoiler_text.match(/^re[: ]/i)) {
|
|
||||||
spoiler_text = 're: '.concat(spoiler_text);
|
|
||||||
}
|
|
||||||
map.set('spoiler', true);
|
|
||||||
map.set('spoiler_text', spoiler_text);
|
|
||||||
} else {
|
|
||||||
map.set('spoiler', false);
|
|
||||||
map.set('spoiler_text', '');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
case COMPOSE_REPLY_CANCEL:
|
case COMPOSE_REPLY_CANCEL:
|
||||||
state = state.setIn(['advanced_options', 'threaded_mode'], false);
|
state = state.setIn(['advanced_options', 'threaded_mode'], false);
|
||||||
// eslint-disable-next-line no-fallthrough -- fall-through to `COMPOSE_RESET` is intended
|
// eslint-disable-next-line no-fallthrough -- fall-through to `COMPOSE_RESET` is intended
|
||||||
|
case COMPOSE_QUOTE_CANCEL:
|
||||||
|
// eslint-disable-next-line no-fallthrough -- fall-through to `COMPOSE_RESET` is intended
|
||||||
case COMPOSE_RESET:
|
case COMPOSE_RESET:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('in_reply_to', null);
|
map.set('in_reply_to', null);
|
||||||
|
map.set('quote_id', null);
|
||||||
if (defaultContentType) map.set('content_type', defaultContentType);
|
if (defaultContentType) map.set('content_type', defaultContentType);
|
||||||
map.set('text', '');
|
map.set('text', '');
|
||||||
map.set('spoiler', false);
|
map.set('spoiler', false);
|
||||||
|
|
|
@ -1009,6 +1009,50 @@ body > [data-popper-placement] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quote-indicator,
|
||||||
|
.reply-indicator {
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: $ui-primary-color;
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 23px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 0 2 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-indicator__header,
|
||||||
|
.reply-indicator__header {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-indicator__cancel,
|
||||||
|
.reply-indicator__cancel {
|
||||||
|
float: right;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-indicator__display-name,
|
||||||
|
.reply-indicator__display-name {
|
||||||
|
color: $inverted-text-color;
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
line-height: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
& > .display-name {
|
||||||
|
line-height: unset;
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-indicator__display-avatar,
|
||||||
|
.reply-indicator__display-avatar {
|
||||||
|
float: left;
|
||||||
|
margin-inline-end: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.status__content--with-action {
|
.status__content--with-action {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -1019,6 +1063,7 @@ body > [data-popper-placement] {
|
||||||
|
|
||||||
.status__content,
|
.status__content,
|
||||||
.edit-indicator__content,
|
.edit-indicator__content,
|
||||||
|
.quote-indicator__content,
|
||||||
.reply-indicator__content {
|
.reply-indicator__content {
|
||||||
position: relative;
|
position: relative;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
@ -1041,7 +1086,8 @@ body > [data-popper-placement] {
|
||||||
}
|
}
|
||||||
|
|
||||||
p,
|
p,
|
||||||
pre {
|
pre,
|
||||||
|
blockquote {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
unicode-bidi: plaintext;
|
unicode-bidi: plaintext;
|
||||||
|
@ -1419,6 +1465,7 @@ body > [data-popper-placement] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.focusable {
|
.focusable {
|
||||||
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
background: rgba($ui-highlight-color, 0.05);
|
background: rgba($ui-highlight-color, 0.05);
|
||||||
|
@ -2279,7 +2326,9 @@ a .account__avatar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__display-name,
|
a.status__display-name,
|
||||||
|
.quote-indicator__display-name,
|
||||||
|
.reply-indicator__display-name,
|
||||||
.detailed-status__display-name,
|
.detailed-status__display-name,
|
||||||
a.account__display-name {
|
a.account__display-name {
|
||||||
&:hover .display-name strong {
|
&:hover .display-name strong {
|
||||||
|
|
|
@ -1,7 +1,21 @@
|
||||||
|
.compose-form {
|
||||||
|
.compose-form__modifiers {
|
||||||
|
.compose-form__upload {
|
||||||
|
&-description {
|
||||||
|
input {
|
||||||
|
&::placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.status__content a,
|
.status__content a,
|
||||||
.reply-indicator__content a,
|
|
||||||
.edit-indicator__content a,
|
|
||||||
.link-footer a,
|
.link-footer a,
|
||||||
|
.quote-indicator__content a,
|
||||||
|
.reply-indicator__content a,
|
||||||
.status__content__read-more-button,
|
.status__content__read-more-button,
|
||||||
.status__content__translate-button {
|
.status__content__translate-button {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
@ -29,9 +43,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__content a,
|
.status__content a {
|
||||||
.reply-indicator__content a,
|
|
||||||
.edit-indicator__content a {
|
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,10 +51,24 @@
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-dialog-modal__textarea::placeholder {
|
.compose-form__poll-wrapper .button.button-secondary,
|
||||||
|
.compose-form .autosuggest-textarea__textarea::placeholder,
|
||||||
|
.compose-form .spoiler-input__input::placeholder,
|
||||||
|
.report-dialog-modal__textarea::placeholder,
|
||||||
|
.language-dropdown__dropdown__results__item__common-name,
|
||||||
|
.compose-form .icon-button {
|
||||||
color: $inverted-text-color;
|
color: $inverted-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-icon-button.active {
|
||||||
|
color: $ui-highlight-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-dropdown__dropdown__results__item.active {
|
||||||
|
background: $ui-highlight-color;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.link-button:disabled {
|
.link-button:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,25 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change default background colors of columns
|
// Change default background colors of columns
|
||||||
|
.column > .scrollable,
|
||||||
|
.getting-started,
|
||||||
|
.column-inline-form,
|
||||||
|
.regeneration-indicator {
|
||||||
|
background: $white;
|
||||||
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-column {
|
||||||
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column > .scrollable.about {
|
||||||
|
border-top: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about__meta,
|
||||||
|
.about__section__title,
|
||||||
.interaction-modal {
|
.interaction-modal {
|
||||||
background: $white;
|
background: $white;
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
@ -34,6 +53,29 @@ html {
|
||||||
background: lighten($ui-base-color, 12%);
|
background: lighten($ui-base-color, 12%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
background: $white;
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-back-button,
|
||||||
|
.column-header {
|
||||||
|
background: $white;
|
||||||
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
|
||||||
|
@media screen and (max-width: $no-gap-breakpoint) {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--slim-button {
|
||||||
|
top: -50px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header__back-button,
|
||||||
|
.column-header__button,
|
||||||
|
.column-header__button.active,
|
||||||
.account__header {
|
.account__header {
|
||||||
background: $white;
|
background: $white;
|
||||||
}
|
}
|
||||||
|
@ -45,6 +87,7 @@ html {
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: $ui-highlight-color;
|
color: $ui-highlight-color;
|
||||||
|
background: $white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +113,25 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-subheading {
|
||||||
|
background: darken($ui-base-color, 4%);
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.getting-started,
|
||||||
|
.scrollable {
|
||||||
|
.column-link {
|
||||||
|
background: $white;
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
background: $ui-base-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.getting-started .navigation-bar {
|
.getting-started .navigation-bar {
|
||||||
border-top: 1px solid lighten($ui-base-color, 8%);
|
border-top: 1px solid lighten($ui-base-color, 8%);
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
@ -79,8 +141,11 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compose-form__autosuggest-wrapper,
|
||||||
|
.poll__option input[type='text'],
|
||||||
|
.compose-form .spoiler-input__input,
|
||||||
|
.compose-form__poll-wrapper select,
|
||||||
.search__input,
|
.search__input,
|
||||||
.search__popout,
|
|
||||||
.setting-text,
|
.setting-text,
|
||||||
.report-dialog-modal__textarea,
|
.report-dialog-modal__textarea,
|
||||||
.audio-player {
|
.audio-player {
|
||||||
|
@ -103,6 +168,86 @@ html {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compose-form__poll-wrapper select {
|
||||||
|
background: $simple-background-color
|
||||||
|
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>")
|
||||||
|
no-repeat right 8px center / auto 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-form__poll-wrapper,
|
||||||
|
.compose-form__poll-wrapper .poll__footer {
|
||||||
|
border-top-color: lighten($ui-base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__filter-bar {
|
||||||
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-form .compose-form__buttons-wrapper {
|
||||||
|
background: $ui-base-color;
|
||||||
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer__header,
|
||||||
|
.drawer__inner {
|
||||||
|
background: $white;
|
||||||
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer__inner__mastodon {
|
||||||
|
background: $white
|
||||||
|
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>')
|
||||||
|
no-repeat bottom / 100% auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the colors used in compose-form
|
||||||
|
.compose-form {
|
||||||
|
.compose-form__modifiers {
|
||||||
|
.compose-form__upload__actions .icon-button,
|
||||||
|
.compose-form__upload__warning .icon-button {
|
||||||
|
color: lighten($white, 7%);
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-form__buttons-wrapper {
|
||||||
|
background: darken($ui-base-color, 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.autosuggest-textarea__suggestions {
|
||||||
|
background: darken($ui-base-color, 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.autosuggest-textarea__suggestions__item {
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active,
|
||||||
|
&.selected {
|
||||||
|
background: lighten($ui-base-color, 4%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-mart-bar {
|
||||||
|
border-color: lighten($ui-base-color, 4%);
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
background: darken($ui-base-color, 6%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-mart-search input {
|
||||||
|
background: rgba($ui-base-color, 0.3);
|
||||||
|
border-color: $ui-base-color;
|
||||||
|
}
|
||||||
|
|
||||||
.upload-progress__backdrop {
|
.upload-progress__backdrop {
|
||||||
background: $ui-base-color;
|
background: $ui-base-color;
|
||||||
}
|
}
|
||||||
|
@ -112,7 +257,13 @@ html {
|
||||||
background: lighten($white, 4%);
|
background: lighten($white, 4%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detailed-status,
|
||||||
|
.detailed-status__action-bar {
|
||||||
|
background: $white;
|
||||||
|
}
|
||||||
|
|
||||||
// Change the background colors of status__content__spoiler-link
|
// Change the background colors of status__content__spoiler-link
|
||||||
|
.quote-indicator__content .status__content__spoiler-link,
|
||||||
.reply-indicator__content .status__content__spoiler-link,
|
.reply-indicator__content .status__content__spoiler-link,
|
||||||
.status__content .status__content__spoiler-link {
|
.status__content .status__content__spoiler-link {
|
||||||
background: $ui-base-color;
|
background: $ui-base-color;
|
||||||
|
@ -123,11 +274,52 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Change the background colors of media and video spoilers
|
||||||
|
.media-spoiler,
|
||||||
|
.video-player__spoiler {
|
||||||
|
background: $ui-base-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-dropdown.active .privacy-dropdown__value.active .icon-button {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
.account-gallery__item a {
|
.account-gallery__item a {
|
||||||
background-color: $ui-base-color;
|
background-color: $ui-base-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Change the colors used in the dropdown menu
|
||||||
|
.dropdown-menu {
|
||||||
|
background: $white;
|
||||||
|
|
||||||
|
&__arrow::before {
|
||||||
|
background-color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
color: $darker-text-color;
|
||||||
|
|
||||||
|
&--dangerous {
|
||||||
|
color: $error-value-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
button {
|
||||||
|
background: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Change the text colors on inverted background
|
// Change the text colors on inverted background
|
||||||
|
.privacy-dropdown__option.active,
|
||||||
|
.privacy-dropdown__option:hover,
|
||||||
|
.privacy-dropdown__option.active .privacy-dropdown__option__content,
|
||||||
|
.privacy-dropdown__option.active .privacy-dropdown__option__content strong,
|
||||||
|
.privacy-dropdown__option:hover .privacy-dropdown__option__content,
|
||||||
|
.privacy-dropdown__option:hover .privacy-dropdown__option__content strong,
|
||||||
|
.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:active,
|
||||||
|
.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:focus,
|
||||||
|
.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:hover,
|
||||||
.actions-modal ul li:not(:empty) a.active,
|
.actions-modal ul li:not(:empty) a.active,
|
||||||
.actions-modal ul li:not(:empty) a.active button,
|
.actions-modal ul li:not(:empty) a.active button,
|
||||||
.actions-modal ul li:not(:empty) a:active,
|
.actions-modal ul li:not(:empty) a:active,
|
||||||
|
@ -136,6 +328,7 @@ html {
|
||||||
.actions-modal ul li:not(:empty) a:focus button,
|
.actions-modal ul li:not(:empty) a:focus button,
|
||||||
.actions-modal ul li:not(:empty) a:hover,
|
.actions-modal ul li:not(:empty) a:hover,
|
||||||
.actions-modal ul li:not(:empty) a:hover button,
|
.actions-modal ul li:not(:empty) a:hover button,
|
||||||
|
.language-dropdown__dropdown__results__item.active,
|
||||||
.admin-wrapper .sidebar ul .simple-navigation-active-leaf a,
|
.admin-wrapper .sidebar ul .simple-navigation-active-leaf a,
|
||||||
.simple_form .block-button,
|
.simple_form .block-button,
|
||||||
.simple_form .button,
|
.simple_form .button,
|
||||||
|
@ -143,6 +336,19 @@ html {
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.language-dropdown__dropdown__results__item
|
||||||
|
.language-dropdown__dropdown__results__item__common-name {
|
||||||
|
color: lighten($ui-base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-dropdown__dropdown__results__item.active
|
||||||
|
.language-dropdown__dropdown__results__item__common-name {
|
||||||
|
color: darken($ui-base-color, 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu__separator,
|
||||||
|
.dropdown-menu__item.edited-timestamp__history__item,
|
||||||
|
.dropdown-menu__container__header,
|
||||||
.compare-history-modal .report-modal__target,
|
.compare-history-modal .report-modal__target,
|
||||||
.report-dialog-modal .poll__option.dialog-option {
|
.report-dialog-modal .poll__option.dialog-option {
|
||||||
border-bottom-color: lighten($ui-base-color, 4%);
|
border-bottom-color: lighten($ui-base-color, 4%);
|
||||||
|
@ -176,7 +382,10 @@ html {
|
||||||
|
|
||||||
.reactions-bar__item:hover,
|
.reactions-bar__item:hover,
|
||||||
.reactions-bar__item:focus,
|
.reactions-bar__item:focus,
|
||||||
.reactions-bar__item:active {
|
.reactions-bar__item:active,
|
||||||
|
.language-dropdown__dropdown__results__item:hover,
|
||||||
|
.language-dropdown__dropdown__results__item:focus,
|
||||||
|
.language-dropdown__dropdown__results__item:active {
|
||||||
background-color: $ui-base-color;
|
background-color: $ui-base-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,7 +423,7 @@ html {
|
||||||
.column-header__collapsible-inner {
|
.column-header__collapsible-inner {
|
||||||
background: darken($ui-base-color, 4%);
|
background: darken($ui-base-color, 4%);
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
border-bottom: 0;
|
border-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-settings__hashtags .column-select__option {
|
.column-settings__hashtags .column-select__option {
|
||||||
|
@ -264,11 +473,11 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle-track {
|
.react-toggle-track {
|
||||||
background: $ui-primary-color;
|
background: $ui-secondary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
|
.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
|
||||||
background: lighten($ui-primary-color, 10%);
|
background: darken($ui-secondary-color, 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled)
|
.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled)
|
||||||
|
@ -419,7 +628,14 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quote-indicator,
|
||||||
|
.reply-indicator {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
.status__content,
|
.status__content,
|
||||||
|
.quote-indicator__content,
|
||||||
.reply-indicator__content {
|
.reply-indicator__content {
|
||||||
a {
|
a {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
|
@ -440,8 +656,7 @@ html {
|
||||||
.directory__tag > div,
|
.directory__tag > div,
|
||||||
.card > a,
|
.card > a,
|
||||||
.page-header,
|
.page-header,
|
||||||
.compose-form,
|
.compose-form .compose-form__warning {
|
||||||
.compose-form__warning {
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -513,6 +728,13 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status.collapsed .status__content::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
rgba(darken($ui-base-color, 13%), 0),
|
||||||
|
rgba(darken($ui-base-color, 13%), 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.drawer__inner__mastodon {
|
.drawer__inner__mastodon {
|
||||||
background: $white
|
background: $white
|
||||||
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>')
|
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>')
|
||||||
|
@ -522,47 +744,3 @@ html {
|
||||||
filter: contrast(75%) brightness(75%) !important;
|
filter: contrast(75%) brightness(75%) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.compose-form__actions .icon-button.active,
|
|
||||||
.dropdown-button.active,
|
|
||||||
.privacy-dropdown__option.active,
|
|
||||||
.privacy-dropdown__option:focus,
|
|
||||||
.language-dropdown__dropdown__results__item:focus,
|
|
||||||
.language-dropdown__dropdown__results__item.active,
|
|
||||||
.privacy-dropdown__option:focus .privacy-dropdown__option__content,
|
|
||||||
.privacy-dropdown__option:focus .privacy-dropdown__option__content strong,
|
|
||||||
.privacy-dropdown__option.active .privacy-dropdown__option__content,
|
|
||||||
.privacy-dropdown__option.active .privacy-dropdown__option__content strong,
|
|
||||||
.language-dropdown__dropdown__results__item:focus
|
|
||||||
.language-dropdown__dropdown__results__item__common-name,
|
|
||||||
.language-dropdown__dropdown__results__item.active
|
|
||||||
.language-dropdown__dropdown__results__item__common-name {
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compose-form .spoiler-input__input {
|
|
||||||
color: lighten($ui-highlight-color, 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.compose-form .autosuggest-textarea__textarea,
|
|
||||||
.compose-form__highlightable,
|
|
||||||
.search__input,
|
|
||||||
.search__popout,
|
|
||||||
.emoji-mart-search input,
|
|
||||||
.language-dropdown__dropdown .emoji-mart-search input,
|
|
||||||
.poll__option input[type='text'] {
|
|
||||||
background: darken($ui-base-color, 10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-follow-suggestions {
|
|
||||||
background-color: rgba($ui-highlight-color, 0.1);
|
|
||||||
border-bottom-color: rgba($ui-highlight-color, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-follow-suggestions__body__scrollable__card {
|
|
||||||
background: $white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-follow-suggestions__body__scroll-button__icon {
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
.status__quote,
|
||||||
.status__content__text,
|
.status__content__text,
|
||||||
.e-content,
|
.e-content,
|
||||||
.edit-indicator__content,
|
.edit-indicator__content,
|
||||||
|
.quote-indicator__content,
|
||||||
.reply-indicator__content {
|
.reply-indicator__content {
|
||||||
pre,
|
pre,
|
||||||
blockquote {
|
blockquote {
|
||||||
|
@ -91,3 +93,11 @@
|
||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quote-indicator__content,
|
||||||
|
.reply-indicator__content {
|
||||||
|
blockquote {
|
||||||
|
border-inline-start-color: $inverted-text-color;
|
||||||
|
color: $inverted-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m228-240 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T458-480L320-240h-92Zm360 0 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T818-480L680-240h-92Z"/></svg>
|
After Width: | Height: | Size: 322 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m228-240 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T458-480L320-240h-92Zm360 0 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T818-480L680-240h-92ZM320-500q25 0 42.5-17.5T380-560q0-25-17.5-42.5T320-620q-25 0-42.5 17.5T260-560q0 25 17.5 42.5T320-500Zm360 0q25 0 42.5-17.5T740-560q0-25-17.5-42.5T680-620q-25 0-42.5 17.5T620-560q0 25 17.5 42.5T680-500Zm0-60Zm-360 0Z"/></svg>
|
After Width: | Height: | Size: 538 B |
|
@ -130,6 +130,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
media_attachment_ids: attachment_ids,
|
media_attachment_ids: attachment_ids,
|
||||||
ordered_media_attachment_ids: attachment_ids,
|
ordered_media_attachment_ids: attachment_ids,
|
||||||
poll: process_poll,
|
poll: process_poll,
|
||||||
|
quote: process_quote,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -430,4 +431,28 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
poll.reload
|
poll.reload
|
||||||
retry
|
retry
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def guess_quote_url
|
||||||
|
if @object["quoteUri"] && !@object["quoteUri"].empty?
|
||||||
|
@object["quoteUri"]
|
||||||
|
elsif @object["quoteUrl"] && !@object["quoteUrl"].empty?
|
||||||
|
@object["quoteUrl"]
|
||||||
|
elsif @object["quoteURL"] && !@object["quoteURL"].empty?
|
||||||
|
@object["quoteURL"]
|
||||||
|
elsif @object["_misskey_quote"] && !@object["_misskey_quote"].empty?
|
||||||
|
@object["_misskey_quote"]
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_quote
|
||||||
|
url = guess_quote_url
|
||||||
|
return nil if url.nil?
|
||||||
|
|
||||||
|
quote = ResolveURLService.new.call(url)
|
||||||
|
status_from_uri(quote.uri) if quote
|
||||||
|
rescue
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -83,11 +83,15 @@ class ActivityPub::TagManager
|
||||||
# Unlisted and private statuses go out primarily to the followers collection
|
# Unlisted and private statuses go out primarily to the followers collection
|
||||||
# Others go out only to the people they mention
|
# Others go out only to the people they mention
|
||||||
def to(status)
|
def to(status)
|
||||||
|
to = []
|
||||||
|
|
||||||
|
to << uri_for(status.quote.account) if status.quote?
|
||||||
|
|
||||||
case status.visibility
|
case status.visibility
|
||||||
when 'public'
|
when 'public'
|
||||||
[COLLECTIONS[:public]]
|
to << COLLECTIONS[:public]
|
||||||
when 'unlisted', 'private'
|
when 'unlisted', 'private'
|
||||||
[account_followers_url(status.account)]
|
to << account_followers_url(status.account)
|
||||||
when 'direct', 'limited'
|
when 'direct', 'limited'
|
||||||
if status.account.silenced?
|
if status.account.silenced?
|
||||||
# Only notify followers if the account is locally silenced
|
# Only notify followers if the account is locally silenced
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
# edited_at :datetime
|
# edited_at :datetime
|
||||||
# trendable :boolean
|
# trendable :boolean
|
||||||
# ordered_media_attachment_ids :bigint(8) is an Array
|
# ordered_media_attachment_ids :bigint(8) is an Array
|
||||||
|
# quote_id :bigint(8)
|
||||||
#
|
#
|
||||||
|
|
||||||
class Status < ApplicationRecord
|
class Status < ApplicationRecord
|
||||||
|
@ -66,6 +67,7 @@ class Status < ApplicationRecord
|
||||||
with_options class_name: 'Status', optional: true do
|
with_options class_name: 'Status', optional: true do
|
||||||
belongs_to :thread, foreign_key: 'in_reply_to_id', inverse_of: :replies
|
belongs_to :thread, foreign_key: 'in_reply_to_id', inverse_of: :replies
|
||||||
belongs_to :reblog, foreign_key: 'reblog_of_id', inverse_of: :reblogs
|
belongs_to :reblog, foreign_key: 'reblog_of_id', inverse_of: :reblogs
|
||||||
|
belongs_to :quote, foreign_key: 'quote_id', inverse_of: :quoted
|
||||||
end
|
end
|
||||||
|
|
||||||
has_many :favourites, inverse_of: :status, dependent: :destroy
|
has_many :favourites, inverse_of: :status, dependent: :destroy
|
||||||
|
@ -76,6 +78,7 @@ class Status < ApplicationRecord
|
||||||
has_many :mentions, dependent: :destroy, inverse_of: :status
|
has_many :mentions, dependent: :destroy, inverse_of: :status
|
||||||
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
|
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
|
||||||
has_many :media_attachments, dependent: :nullify
|
has_many :media_attachments, dependent: :nullify
|
||||||
|
has_many :quoted, foreign_key: 'quote_id', class_name: 'Status', inverse_of: :quote, dependent: :nullify
|
||||||
|
|
||||||
# The `dependent` option is enabled by the initial `mentions` association declaration
|
# The `dependent` option is enabled by the initial `mentions` association declaration
|
||||||
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status # rubocop:disable Rails/HasManyOrHasOneDependent
|
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status # rubocop:disable Rails/HasManyOrHasOneDependent
|
||||||
|
@ -102,6 +105,7 @@ class Status < ApplicationRecord
|
||||||
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
|
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
|
||||||
validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
|
validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
|
||||||
validates :content_type, inclusion: { in: %w(text/plain text/markdown text/html) }, allow_nil: true
|
validates :content_type, inclusion: { in: %w(text/plain text/markdown text/html) }, allow_nil: true
|
||||||
|
validates :quote_visibility, inclusion: { in: %w(public unlisted) }, if: :quote?
|
||||||
|
|
||||||
accepts_nested_attributes_for :poll
|
accepts_nested_attributes_for :poll
|
||||||
|
|
||||||
|
@ -178,6 +182,17 @@ class Status < ApplicationRecord
|
||||||
account: [:account_stat, user: :role],
|
account: [:account_stat, user: :role],
|
||||||
active_mentions: :account,
|
active_mentions: :account,
|
||||||
],
|
],
|
||||||
|
quote: [
|
||||||
|
:application,
|
||||||
|
:tags,
|
||||||
|
:media_attachments,
|
||||||
|
:conversation,
|
||||||
|
:status_stat,
|
||||||
|
:preloadable_poll,
|
||||||
|
preview_cards_status: [:preview_card],
|
||||||
|
account: [:account_stat, :user],
|
||||||
|
active_mentions: { account: :account_stat },
|
||||||
|
],
|
||||||
thread: :account
|
thread: :account
|
||||||
|
|
||||||
delegate :domain, to: :account, prefix: true
|
delegate :domain, to: :account, prefix: true
|
||||||
|
@ -212,6 +227,14 @@ class Status < ApplicationRecord
|
||||||
!reblog_of_id.nil?
|
!reblog_of_id.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def quote?
|
||||||
|
!quote_id.nil? && quote
|
||||||
|
end
|
||||||
|
|
||||||
|
def quote_visibility
|
||||||
|
quote&.visibility
|
||||||
|
end
|
||||||
|
|
||||||
def within_realtime_window?
|
def within_realtime_window?
|
||||||
created_at >= REAL_TIME_WINDOW.ago
|
created_at >= REAL_TIME_WINDOW.ago
|
||||||
end
|
end
|
||||||
|
@ -284,7 +307,7 @@ class Status < ApplicationRecord
|
||||||
fields = [spoiler_text, text]
|
fields = [spoiler_text, text]
|
||||||
fields += preloadable_poll.options unless preloadable_poll.nil?
|
fields += preloadable_poll.options unless preloadable_poll.nil?
|
||||||
|
|
||||||
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain)
|
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain) + (quote? ? CustomEmoji.from_text([quote.spoiler_text, quote.text].join(' '), quote.account.domain) : [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def ordered_media_attachments
|
def ordered_media_attachments
|
||||||
|
|
|
@ -64,6 +64,10 @@ class StatusEdit < ApplicationRecord
|
||||||
end.take(Status::MEDIA_ATTACHMENTS_LIMIT)
|
end.take(Status::MEDIA_ATTACHMENTS_LIMIT)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def quote?
|
||||||
|
status.quote?
|
||||||
|
end
|
||||||
|
|
||||||
def proper
|
def proper
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
include FormattingHelper
|
include FormattingHelper
|
||||||
|
|
||||||
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :direct_message
|
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :direct_message, :quote_uri
|
||||||
|
|
||||||
attributes :id, :type, :summary,
|
attributes :id, :type, :summary,
|
||||||
:in_reply_to, :published, :url,
|
:in_reply_to, :published, :url,
|
||||||
|
@ -11,6 +11,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
:atom_uri, :in_reply_to_atom_uri,
|
:atom_uri, :in_reply_to_atom_uri,
|
||||||
:conversation
|
:conversation
|
||||||
|
|
||||||
|
attribute :quote_uri, if: -> { object.quote? }
|
||||||
|
|
||||||
attribute :content
|
attribute :content
|
||||||
attribute :content_map, if: :language?
|
attribute :content_map, if: :language?
|
||||||
attribute :updated, if: :edited?
|
attribute :updated, if: :edited?
|
||||||
|
@ -150,6 +152,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def quote_uri
|
||||||
|
ActivityPub::TagManager.instance.uri_for(object.quote) if object.quote?
|
||||||
|
end
|
||||||
|
|
||||||
def local?
|
def local?
|
||||||
object.account.local?
|
object.account.local?
|
||||||
end
|
end
|
||||||
|
|
|
@ -202,3 +202,13 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class REST::QuoteStatusSerializer < REST::StatusSerializer
|
||||||
|
attribute :quote do
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class REST::StatusSerializer < ActiveModel::Serializer
|
||||||
|
belongs_to :quote, serializer: REST::QuoteStatusSerializer
|
||||||
|
end
|
||||||
|
|
|
@ -75,7 +75,7 @@ class FetchLinkCardService < BaseService
|
||||||
@status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
|
@status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
|
||||||
else
|
else
|
||||||
document = Nokogiri::HTML(@status.text)
|
document = Nokogiri::HTML(@status.text)
|
||||||
links = document.css('a')
|
links = document.css(':not(.quote-inline) > a')
|
||||||
|
|
||||||
links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize)
|
links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize)
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,6 +31,7 @@ class PostStatusService < BaseService
|
||||||
# @option [String] :idempotency Optional idempotency key
|
# @option [String] :idempotency Optional idempotency key
|
||||||
# @option [Boolean] :with_rate_limit
|
# @option [Boolean] :with_rate_limit
|
||||||
# @option [Enumerable] :allowed_mentions Optional array of expected mentioned account IDs, raises `UnexpectedMentionsError` if unexpected accounts end up in mentions
|
# @option [Enumerable] :allowed_mentions Optional array of expected mentioned account IDs, raises `UnexpectedMentionsError` if unexpected accounts end up in mentions
|
||||||
|
# @option [String] :quote_id
|
||||||
# @return [Status]
|
# @return [Status]
|
||||||
def call(account, options = {})
|
def call(account, options = {})
|
||||||
@account = account
|
@account = account
|
||||||
|
@ -210,6 +211,7 @@ class PostStatusService < BaseService
|
||||||
application: @options[:application],
|
application: @options[:application],
|
||||||
content_type: @options[:content_type] || @account.user&.setting_default_content_type,
|
content_type: @options[:content_type] || @account.user&.setting_default_content_type,
|
||||||
rate_limit: @options[:with_rate_limit],
|
rate_limit: @options[:with_rate_limit],
|
||||||
|
quote_id: @options[:quote_id],
|
||||||
}.compact
|
}.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,11 @@
|
||||||
|
|
||||||
= account_action_button(status.account)
|
= account_action_button(status.account)
|
||||||
|
|
||||||
|
- if status.quote?
|
||||||
|
= render partial: "statuses/quote_status", locals: {status: status.quote}
|
||||||
|
|
||||||
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
|
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
|
||||||
|
|
||||||
- if status.spoiler_text?
|
- if status.spoiler_text?
|
||||||
%p<
|
%p<
|
||||||
%span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}
|
%span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
.status.quote-status{ dataurl: ActivityPub::TagManager.instance.url_for(status) }
|
||||||
|
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||||
|
.status__avatar
|
||||||
|
%div
|
||||||
|
- if prefers_autoplay?
|
||||||
|
= image_tag status.account.avatar_original_url, alt: '', class: 'u-photo account__avatar'
|
||||||
|
- else
|
||||||
|
= image_tag status.account.avatar_static_url, alt: '', class: 'u-photo account__avatar'
|
||||||
|
%span.display-name
|
||||||
|
%bdi
|
||||||
|
%strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: prefers_autoplay?)
|
||||||
|
|
||||||
|
%span.display-name__account
|
||||||
|
= acct(status.account)
|
||||||
|
= fa_icon('lock') if status.account.locked?
|
||||||
|
|
||||||
|
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
|
||||||
|
|
||||||
|
- if status.spoiler_text?
|
||||||
|
%p<
|
||||||
|
%span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}
|
||||||
|
%button.status__content__spoiler-link= t('statuses.show_more')
|
||||||
|
.e-content{ lang: status.language }<
|
||||||
|
= prerender_custom_emojis(status_content_format(status), status.emojis)
|
||||||
|
|
||||||
|
- if status.preloadable_poll
|
||||||
|
= render_poll_component(status)
|
||||||
|
|
||||||
|
- if !status.ordered_media_attachments.empty?
|
||||||
|
- if status.ordered_media_attachments.first.video?
|
||||||
|
= render_video_component(status, width: 610, height: 343)
|
||||||
|
- elsif status.ordered_media_attachments.first.audio?
|
||||||
|
= render_audio_component(status, width: 610, height: 343)
|
||||||
|
- else
|
||||||
|
= render_media_gallery_component(status, height: 343)
|
||||||
|
- elsif status.preview_card
|
||||||
|
= render_card_component(status)
|
||||||
|
|
|
@ -27,7 +27,12 @@
|
||||||
%span.display-name__account
|
%span.display-name__account
|
||||||
= acct(status.account)
|
= acct(status.account)
|
||||||
= fa_icon('lock') if status.account.locked?
|
= fa_icon('lock') if status.account.locked?
|
||||||
|
|
||||||
|
- if status.quote?
|
||||||
|
= render partial: "statuses/quote_status", locals: {status: status.quote}
|
||||||
|
|
||||||
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
|
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
|
||||||
|
|
||||||
- if status.spoiler_text?
|
- if status.spoiler_text?
|
||||||
%p<
|
%p<
|
||||||
%span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}
|
%span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddQuoteIdToStatuses < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :statuses, :quote_id, :bigint, null: true, default: nil
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,7 @@
|
||||||
|
class AddIndexToStatusesQuoteId < ActiveRecord::Migration[6.1]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def change
|
||||||
|
add_index :statuses, :quote_id, algorithm: :concurrently
|
||||||
|
end
|
||||||
|
end
|
|
@ -1087,6 +1087,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_07_094856) do
|
||||||
t.datetime "edited_at", precision: nil
|
t.datetime "edited_at", precision: nil
|
||||||
t.boolean "trendable"
|
t.boolean "trendable"
|
||||||
t.bigint "ordered_media_attachment_ids", array: true
|
t.bigint "ordered_media_attachment_ids", array: true
|
||||||
|
t.bigint "quote_id"
|
||||||
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
|
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
|
||||||
t.index ["account_id"], name: "index_statuses_on_account_id"
|
t.index ["account_id"], name: "index_statuses_on_account_id"
|
||||||
t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
|
t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
|
||||||
|
@ -1094,6 +1095,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_07_094856) do
|
||||||
t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
|
t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
|
||||||
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id", where: "(in_reply_to_account_id IS NOT NULL)"
|
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id", where: "(in_reply_to_account_id IS NOT NULL)"
|
||||||
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", where: "(in_reply_to_id IS NOT NULL)"
|
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", where: "(in_reply_to_id IS NOT NULL)"
|
||||||
|
t.index ["quote_id"], name: "index_statuses_on_quote_id"
|
||||||
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_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, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)"
|
t.index ["uri"], name: "index_statuses_on_uri", unique: true, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)"
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,6 +31,7 @@ class Sanitize
|
||||||
next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes
|
next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes
|
||||||
next true if /^(mention|hashtag)$/.match?(e) # semantic classes
|
next true if /^(mention|hashtag)$/.match?(e) # semantic classes
|
||||||
next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes
|
next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes
|
||||||
|
next true if /^quote-inline$/.match?(e) # quote inline classes
|
||||||
end
|
end
|
||||||
|
|
||||||
node['class'] = class_list.join(' ')
|
node['class'] = class_list.join(' ')
|
||||||
|
|
Loading…
Reference in New Issue