Add: Webでの引用表示 (#50)
* Add compacted component * 引用表示の間にコンテナをはさみ、不要なコードを削除 * 引用APIを作成、ついでにブロック状況を引用APIに反映 * テスト修正など * 引用をキャッシュに登録 * `quote_id`が`quote_of_id`になったのをSerializerに反映 * Fix test * 引用をフィルターの対象に含める設定+エラー修正 * ストリーミングの存在しないプロパティ削除によるエラーを修正 * Fix lint * 他のサーバーから来た引用付き投稿を処理 * Fix test * フィルター設定時エラーの調整 * 画像つき投稿のスタイルを調整 * 画像つき投稿の最大高さを調整 * 引用禁止・非表示の設定を追加 * ブロック対応 * マイグレーションコード調整 * 引用設定の翻訳を作成 * Lint修正 * 参照1つの場合は引用に変換する設定を削除 * 不要になったテストを削除 * ブロック設定追加、バグ修正 * 他サーバーへ引用送信・受け入れ
This commit is contained in:
parent
3c649aa74d
commit
44b739a39a
53 changed files with 1362 additions and 120 deletions
|
@ -52,11 +52,11 @@ class Api::V1::FiltersController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.permit(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :whole_word, context: [])
|
params.permit(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :whole_word, context: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
resource_params.slice(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :context)
|
resource_params.slice(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :context)
|
||||||
end
|
end
|
||||||
|
|
||||||
def keyword_params
|
def keyword_params
|
||||||
|
|
|
@ -31,7 +31,7 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(
|
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(
|
||||||
[@status], current_account.id, emoji_reactions_map: { @status.id => false }
|
[@status], current_account.id
|
||||||
)
|
)
|
||||||
rescue Mastodon::NotPermittedError
|
rescue Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
|
|
|
@ -43,6 +43,6 @@ class Api::V2::FiltersController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
|
params.permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :with_quote, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -49,7 +49,7 @@ class FiltersController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:custom_filter).permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
|
params.require(:custom_filter).permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :with_quote, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
|
|
|
@ -80,6 +80,10 @@ export function importFetchedStatuses(statuses) {
|
||||||
processStatus(status.reblog);
|
processStatus(status.reblog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.quote && status.quote.id) {
|
||||||
|
processStatus(status.quote);
|
||||||
|
}
|
||||||
|
|
||||||
if (status.poll && status.poll.id) {
|
if (status.poll && status.poll.id) {
|
||||||
pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
|
pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,11 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||||
normalStatus.spoiler_text = normalOldStatus.get('spoiler_text');
|
normalStatus.spoiler_text = normalOldStatus.get('spoiler_text');
|
||||||
normalStatus.hidden = normalOldStatus.get('hidden');
|
normalStatus.hidden = normalOldStatus.get('hidden');
|
||||||
|
|
||||||
|
// for quoted post
|
||||||
|
if (!normalStatus.filtered && normalOldStatus.get('filtered')) {
|
||||||
|
normalStatus.filtered = normalOldStatus.get('filtered');
|
||||||
|
}
|
||||||
|
|
||||||
if (normalOldStatus.get('translation')) {
|
if (normalOldStatus.get('translation')) {
|
||||||
normalStatus.translation = normalOldStatus.get('translation');
|
normalStatus.translation = normalOldStatus.get('translation');
|
||||||
}
|
}
|
||||||
|
|
503
app/javascript/mastodon/components/compacted_status.jsx
Normal file
503
app/javascript/mastodon/components/compacted_status.jsx
Normal file
|
@ -0,0 +1,503 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
|
import { HotKeys } from 'react-hotkeys';
|
||||||
|
|
||||||
|
import AttachmentList from 'mastodon/components/attachment_list';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
|
||||||
|
import Card from '../features/status/components/card';
|
||||||
|
// We use the component (and not the container) since we do not want
|
||||||
|
// to use the progress bar to show download progress
|
||||||
|
import Bundle from '../features/ui/components/bundle';
|
||||||
|
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
||||||
|
import { displayMedia } from '../initial_state';
|
||||||
|
|
||||||
|
import { Avatar } from './avatar';
|
||||||
|
import { DisplayName } from './display_name';
|
||||||
|
import { getHashtagBarForStatus } from './hashtag_bar';
|
||||||
|
import { RelativeTimestamp } from './relative_timestamp';
|
||||||
|
import StatusContent from './status_content';
|
||||||
|
|
||||||
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
|
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
|
||||||
|
const displayName = status.getIn(['account', 'display_name']);
|
||||||
|
|
||||||
|
const spoilerText = status.getIn(['translation', 'spoiler_text']) || status.get('spoiler_text');
|
||||||
|
const contentHtml = status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
|
||||||
|
const contentText = domParser.parseFromString(contentHtml, 'text/html').documentElement.textContent;
|
||||||
|
|
||||||
|
const values = [
|
||||||
|
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
|
||||||
|
spoilerText && status.get('hidden') ? spoilerText : contentText,
|
||||||
|
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
|
||||||
|
status.getIn(['account', 'acct']),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (rebloggedByText) {
|
||||||
|
values.push(rebloggedByText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.join(', ');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultMediaVisibility = (status) => {
|
||||||
|
if (!status) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
||||||
|
status = status.get('reblog');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
|
||||||
|
};
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||||
|
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||||
|
public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' },
|
||||||
|
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' },
|
||||||
|
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
|
||||||
|
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
|
||||||
|
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
|
||||||
|
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle members only' },
|
||||||
|
personal_short: { id: 'privacy.personal.short', defaultMessage: 'Yourself only' },
|
||||||
|
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
|
||||||
|
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
||||||
|
});
|
||||||
|
|
||||||
|
class CompactedStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
status: ImmutablePropTypes.map,
|
||||||
|
previousId: PropTypes.string,
|
||||||
|
nextInReplyToId: PropTypes.string,
|
||||||
|
rootId: PropTypes.string,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
onOpenMedia: PropTypes.func,
|
||||||
|
onOpenVideo: PropTypes.func,
|
||||||
|
onHeightChange: PropTypes.func,
|
||||||
|
onToggleHidden: PropTypes.func,
|
||||||
|
onToggleCollapsed: PropTypes.func,
|
||||||
|
onTranslate: PropTypes.func,
|
||||||
|
onInteractionModal: PropTypes.func,
|
||||||
|
muted: PropTypes.bool,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
|
unread: PropTypes.bool,
|
||||||
|
onMoveUp: PropTypes.func,
|
||||||
|
onMoveDown: PropTypes.func,
|
||||||
|
showThread: PropTypes.bool,
|
||||||
|
getScrollPosition: PropTypes.func,
|
||||||
|
updateScrollBottom: PropTypes.func,
|
||||||
|
cacheMediaWidth: PropTypes.func,
|
||||||
|
cachedMediaWidth: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Avoid checking props that are functions (and whose equality will always
|
||||||
|
// evaluate to false. See react-immutable-pure-component for usage.
|
||||||
|
updateOnProps = [
|
||||||
|
'status',
|
||||||
|
'muted',
|
||||||
|
'hidden',
|
||||||
|
'unread',
|
||||||
|
];
|
||||||
|
|
||||||
|
state = {
|
||||||
|
showMedia: defaultMediaVisibility(this.props.status),
|
||||||
|
statusId: undefined,
|
||||||
|
forceFilter: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(nextProps, prevState) {
|
||||||
|
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
|
||||||
|
return {
|
||||||
|
showMedia: defaultMediaVisibility(nextProps.status),
|
||||||
|
statusId: nextProps.status.get('id'),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggleMediaVisibility = () => {
|
||||||
|
this.setState({ showMedia: !this.state.showMedia });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = e => {
|
||||||
|
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleHotkeyOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
handlePrependAccountClick = e => {
|
||||||
|
this.handleAccountClick(e, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleAccountClick = (e, proper = true) => {
|
||||||
|
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._openProfile(proper);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleExpandedToggle = () => {
|
||||||
|
this.props.onToggleHidden(this._properStatus());
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCollapsedToggle = isCollapsed => {
|
||||||
|
this.props.onToggleCollapsed(this._properStatus(), isCollapsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleTranslate = () => {
|
||||||
|
this.props.onTranslate(this._properStatus());
|
||||||
|
};
|
||||||
|
|
||||||
|
getAttachmentAspectRatio () {
|
||||||
|
const attachments = this._properStatus().get('media_attachments');
|
||||||
|
|
||||||
|
if (attachments.getIn([0, 'type']) === 'video') {
|
||||||
|
return `${attachments.getIn([0, 'meta', 'original', 'width'])} / ${attachments.getIn([0, 'meta', 'original', 'height'])}`;
|
||||||
|
} else if (attachments.getIn([0, 'type']) === 'audio') {
|
||||||
|
return '16 / 9';
|
||||||
|
} else {
|
||||||
|
return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLoadingMediaGallery = () => {
|
||||||
|
return (
|
||||||
|
<div className='media-gallery' style={{ aspectRatio: this.getAttachmentAspectRatio() }} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderLoadingVideoPlayer = () => {
|
||||||
|
return (
|
||||||
|
<div className='video-player' style={{ aspectRatio: this.getAttachmentAspectRatio() }} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderLoadingAudioPlayer = () => {
|
||||||
|
return (
|
||||||
|
<div className='audio-player' style={{ aspectRatio: this.getAttachmentAspectRatio() }} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleOpenVideo = (options) => {
|
||||||
|
const status = this._properStatus();
|
||||||
|
const lang = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleOpenMedia = (media, index) => {
|
||||||
|
const status = this._properStatus();
|
||||||
|
const lang = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
this.props.onOpenMedia(status.get('id'), media, index, lang);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleHotkeyOpenMedia = e => {
|
||||||
|
const { onOpenMedia, onOpenVideo } = this.props;
|
||||||
|
const status = this._properStatus();
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (status.get('media_attachments').size > 0) {
|
||||||
|
const lang = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
|
onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, { startTime: 0 });
|
||||||
|
} else {
|
||||||
|
onOpenMedia(status.get('id'), status.get('media_attachments'), 0, lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleHotkeyOpen = () => {
|
||||||
|
if (this.props.onClick) {
|
||||||
|
this.props.onClick();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { router } = this.context;
|
||||||
|
const status = this._properStatus();
|
||||||
|
|
||||||
|
if (!router) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleHotkeyOpenProfile = () => {
|
||||||
|
this._openProfile();
|
||||||
|
};
|
||||||
|
|
||||||
|
_openProfile = (proper = true) => {
|
||||||
|
const { router } = this.context;
|
||||||
|
const status = proper ? this._properStatus() : this.props.status;
|
||||||
|
|
||||||
|
if (!router) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.history.push(`/@${status.getIn(['account', 'acct'])}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleHotkeyMoveUp = e => {
|
||||||
|
this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
|
||||||
|
};
|
||||||
|
|
||||||
|
handleHotkeyMoveDown = e => {
|
||||||
|
this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
|
||||||
|
};
|
||||||
|
|
||||||
|
handleHotkeyToggleHidden = () => {
|
||||||
|
this.props.onToggleHidden(this._properStatus());
|
||||||
|
};
|
||||||
|
|
||||||
|
handleHotkeyToggleSensitive = () => {
|
||||||
|
this.handleToggleMediaVisibility();
|
||||||
|
};
|
||||||
|
|
||||||
|
_properStatus () {
|
||||||
|
const { status } = this.props;
|
||||||
|
|
||||||
|
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
||||||
|
return status.get('reblog');
|
||||||
|
} else {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRef = c => {
|
||||||
|
this.node = c;
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, hidden, featured, unread, showThread, previousId, nextInReplyToId, rootId } = this.props;
|
||||||
|
|
||||||
|
let { status } = this.props;
|
||||||
|
|
||||||
|
if (status === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = this.props.muted ? {} : {
|
||||||
|
open: this.handleHotkeyOpen,
|
||||||
|
openProfile: this.handleHotkeyOpenProfile,
|
||||||
|
moveUp: this.handleHotkeyMoveUp,
|
||||||
|
moveDown: this.handleHotkeyMoveDown,
|
||||||
|
toggleHidden: this.handleHotkeyToggleHidden,
|
||||||
|
toggleSensitive: this.handleHotkeyToggleSensitive,
|
||||||
|
openMedia: this.handleHotkeyOpenMedia,
|
||||||
|
};
|
||||||
|
|
||||||
|
let media, isCardMediaWithSensitive, prepend, rebloggedByText;
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={handlers}>
|
||||||
|
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={0}>
|
||||||
|
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
|
||||||
|
<span>{status.get('content')}</span>
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectUp = previousId && previousId === status.get('in_reply_to_id');
|
||||||
|
const connectToRoot = rootId && rootId === status.get('in_reply_to_id');
|
||||||
|
const connectReply = nextInReplyToId && nextInReplyToId === status.get('id');
|
||||||
|
|
||||||
|
if (showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id'])) {
|
||||||
|
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
|
||||||
|
|
||||||
|
prepend = (
|
||||||
|
<div className='status__prepend'>
|
||||||
|
<div className='status__prepend-icon-wrapper'><Icon id='reply' className='status__prepend-icon' fixedWidth /></div>
|
||||||
|
<FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.get('quote_muted')) {
|
||||||
|
const minHandlers = {
|
||||||
|
moveUp: this.handleHotkeyMoveUp,
|
||||||
|
moveDown: this.handleHotkeyMoveDown,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={minHandlers}>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||||
|
<div className='status__wrapper status__wrapper__compact status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef} onClick={this.handleClick}>
|
||||||
|
<FormattedMessage id='status.quote_filtered' defaultMessage='This quote is filtered because of muting, blocking or domain blocking' />
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isCardMediaWithSensitive = false;
|
||||||
|
|
||||||
|
if (status.get('media_attachments').size > 0) {
|
||||||
|
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
|
||||||
|
if (this.props.muted) {
|
||||||
|
media = (
|
||||||
|
<AttachmentList
|
||||||
|
compact
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||||
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||||
|
|
||||||
|
media = (
|
||||||
|
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||||
|
{Component => (
|
||||||
|
<Component
|
||||||
|
src={attachment.get('url')}
|
||||||
|
alt={description}
|
||||||
|
lang={language}
|
||||||
|
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||||
|
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||||
|
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
||||||
|
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
|
||||||
|
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||||
|
width={this.props.cachedMediaWidth}
|
||||||
|
height={110}
|
||||||
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
blurhash={attachment.get('blurhash')}
|
||||||
|
visible={this.state.showMedia}
|
||||||
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Bundle>
|
||||||
|
);
|
||||||
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||||
|
|
||||||
|
media = (
|
||||||
|
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||||
|
{Component => (
|
||||||
|
<Component
|
||||||
|
preview={attachment.get('preview_url')}
|
||||||
|
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||||
|
aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`}
|
||||||
|
blurhash={attachment.get('blurhash')}
|
||||||
|
src={attachment.get('url')}
|
||||||
|
alt={description}
|
||||||
|
lang={language}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
onOpenVideo={this.handleOpenVideo}
|
||||||
|
visible={this.state.showMedia}
|
||||||
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Bundle>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
media = (
|
||||||
|
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||||
|
{Component => (
|
||||||
|
<Component
|
||||||
|
compact
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
lang={language}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
height={110}
|
||||||
|
onOpenMedia={this.handleOpenMedia}
|
||||||
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
|
defaultWidth={this.props.cachedMediaWidth}
|
||||||
|
visible={this.state.showMedia}
|
||||||
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Bundle>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (status.get('card') && !this.props.muted) {
|
||||||
|
media = (
|
||||||
|
<Card
|
||||||
|
onOpenMedia={this.handleOpenMedia}
|
||||||
|
card={status.get('card')}
|
||||||
|
compact
|
||||||
|
sensitive={status.get('sensitive') && !status.get('spoiler_text')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
isCardMediaWithSensitive = status.get('spoiler_text').length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||||
|
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={handlers}>
|
||||||
|
<div className={classNames('status__wrapper', 'status__wrapper__compact', `status__wrapper-${status.get('visibility_ex')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
|
||||||
|
{prepend}
|
||||||
|
|
||||||
|
<div className={classNames('status', `status-${status.get('visibility_ex')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
|
||||||
|
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
|
||||||
|
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||||
|
<div onClick={this.handleClick} className='status__info'>
|
||||||
|
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||||
|
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
||||||
|
<div className='status__avatar status__avatar__compact'>
|
||||||
|
<Avatar account={status.get('account')} size={24} inline />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DisplayName account={status.get('account')} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusContent
|
||||||
|
status={status}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
expanded={expanded}
|
||||||
|
onExpandedToggle={this.handleExpandedToggle}
|
||||||
|
onTranslate={this.handleTranslate}
|
||||||
|
collapsible
|
||||||
|
onCollapsedToggle={this.handleCollapsedToggle}
|
||||||
|
{...statusContentProps}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(!isCardMediaWithSensitive || !status.get('hidden')) && media}
|
||||||
|
|
||||||
|
{(!status.get('spoiler_text') || expanded) && hashtagBar}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(CompactedStatus);
|
|
@ -236,6 +236,7 @@ class MediaGallery extends PureComponent {
|
||||||
visible: PropTypes.bool,
|
visible: PropTypes.bool,
|
||||||
autoplay: PropTypes.bool,
|
autoplay: PropTypes.bool,
|
||||||
onToggleVisibility: PropTypes.func,
|
onToggleVisibility: PropTypes.func,
|
||||||
|
compact: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -306,7 +307,7 @@ class MediaGallery extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { media, lang, intl, sensitive, defaultWidth, autoplay } = this.props;
|
const { media, lang, intl, sensitive, defaultWidth, autoplay, compact } = this.props;
|
||||||
const { visible } = this.state;
|
const { visible } = this.state;
|
||||||
const width = this.state.width || defaultWidth;
|
const width = this.state.width || defaultWidth;
|
||||||
|
|
||||||
|
@ -359,9 +360,10 @@ class MediaGallery extends PureComponent {
|
||||||
const columnClass = (size === 9) ? 'media-gallery--column3' :
|
const columnClass = (size === 9) ? 'media-gallery--column3' :
|
||||||
(size === 10 || size === 11 || size === 12 || size === 13 || size === 14 || size === 15 || size === 16) ? 'media-gallery--column4' :
|
(size === 10 || size === 11 || size === 12 || size === 13 || size === 14 || size === 15 || size === 16) ? 'media-gallery--column4' :
|
||||||
'media-gallery--column2';
|
'media-gallery--column2';
|
||||||
|
const compactClass = compact ? 'media-gallery__compact' : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('media-gallery', rowClass, columnClass)} style={style} ref={this.handleRef}>
|
<div className={classNames('media-gallery', rowClass, columnClass, compactClass)} style={style} ref={this.handleRef}>
|
||||||
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}>
|
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,12 +13,13 @@ import AttachmentList from 'mastodon/components/attachment_list';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
|
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
|
||||||
|
|
||||||
|
import CompactedStatusContainer from '../containers/compacted_status_container'
|
||||||
import Card from '../features/status/components/card';
|
import Card from '../features/status/components/card';
|
||||||
// We use the component (and not the container) since we do not want
|
// We use the component (and not the container) since we do not want
|
||||||
// to use the progress bar to show download progress
|
// to use the progress bar to show download progress
|
||||||
import Bundle from '../features/ui/components/bundle';
|
import Bundle from '../features/ui/components/bundle';
|
||||||
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
||||||
import { displayMedia, enableEmojiReaction, showEmojiReactionOnTimeline } from '../initial_state';
|
import { displayMedia, enableEmojiReaction, showEmojiReactionOnTimeline, showQuoteInHome, showQuoteInPublic } from '../initial_state';
|
||||||
|
|
||||||
import { Avatar } from './avatar';
|
import { Avatar } from './avatar';
|
||||||
import { AvatarOverlay } from './avatar_overlay';
|
import { AvatarOverlay } from './avatar_overlay';
|
||||||
|
@ -87,6 +88,7 @@ class Status extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
status: ImmutablePropTypes.map,
|
status: ImmutablePropTypes.map,
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
|
contextType: PropTypes.string,
|
||||||
previousId: PropTypes.string,
|
previousId: PropTypes.string,
|
||||||
nextInReplyToId: PropTypes.string,
|
nextInReplyToId: PropTypes.string,
|
||||||
rootId: PropTypes.string,
|
rootId: PropTypes.string,
|
||||||
|
@ -357,15 +359,17 @@ class Status extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId } = this.props;
|
const { intl, hidden, featured, unread, muted, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId } = this.props;
|
||||||
|
|
||||||
let { status, account, ...other } = this.props;
|
let { status, account, ...other } = this.props;
|
||||||
|
|
||||||
|
const contextType = (this.props.contextType || '').split(':')[0];
|
||||||
|
|
||||||
if (status === null) {
|
if (status === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlers = this.props.muted ? {} : {
|
const handlers = muted ? {} : {
|
||||||
reply: this.handleHotkeyReply,
|
reply: this.handleHotkeyReply,
|
||||||
favourite: this.handleHotkeyFavourite,
|
favourite: this.handleHotkeyFavourite,
|
||||||
boost: this.handleHotkeyBoost,
|
boost: this.handleHotkeyBoost,
|
||||||
|
@ -384,7 +388,7 @@ class Status extends ImmutablePureComponent {
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers}>
|
<HotKeys handlers={handlers}>
|
||||||
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={0}>
|
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !muted })} tabIndex={0}>
|
||||||
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
|
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
|
||||||
<span>{status.get('content')}</span>
|
<span>{status.get('content')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -412,12 +416,53 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
let visibilityIcon = visibilityIconInfo[status.get('limited_scope') || status.get('visibility_ex')] || visibilityIconInfo[status.get('visibility')];
|
let visibilityIcon = visibilityIconInfo[status.get('limited_scope') || status.get('visibility_ex')] || visibilityIconInfo[status.get('visibility')];
|
||||||
|
|
||||||
|
if (account === undefined || account === null) {
|
||||||
|
statusAvatar = <Avatar account={status.get('account')} size={46} />;
|
||||||
|
} else {
|
||||||
|
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
|
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
|
||||||
const minHandlers = this.props.muted ? {} : {
|
const minHandlers = muted ? {} : {
|
||||||
moveUp: this.handleHotkeyMoveUp,
|
moveUp: this.handleHotkeyMoveUp,
|
||||||
moveDown: this.handleHotkeyMoveDown,
|
moveDown: this.handleHotkeyMoveDown,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (status.get('filter_action') === 'half_warn') {
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={minHandlers}>
|
||||||
|
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef}>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||||
|
<div onClick={this.handleClick} className='status__info'>
|
||||||
|
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||||
|
{withReference}
|
||||||
|
{withExpiration}
|
||||||
|
{withLimited}
|
||||||
|
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
|
||||||
|
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
||||||
|
<div className='status__avatar'>
|
||||||
|
{statusAvatar}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DisplayName account={status.get('account')} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div >
|
||||||
|
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
|
||||||
|
{' '}
|
||||||
|
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
|
||||||
|
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={minHandlers}>
|
<HotKeys handlers={minHandlers}>
|
||||||
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef}>
|
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef}>
|
||||||
|
@ -478,7 +523,7 @@ class Status extends ImmutablePureComponent {
|
||||||
} else if (status.get('media_attachments').size > 0) {
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
|
||||||
if (this.props.muted) {
|
if (muted) {
|
||||||
media = (
|
media = (
|
||||||
<AttachmentList
|
<AttachmentList
|
||||||
compact
|
compact
|
||||||
|
@ -556,7 +601,7 @@ class Status extends ImmutablePureComponent {
|
||||||
</Bundle>
|
</Bundle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (status.get('card') && !this.props.muted) {
|
} else if (status.get('card') && !muted) {
|
||||||
media = (
|
media = (
|
||||||
<Card
|
<Card
|
||||||
onOpenMedia={this.handleOpenMedia}
|
onOpenMedia={this.handleOpenMedia}
|
||||||
|
@ -568,12 +613,6 @@ class Status extends ImmutablePureComponent {
|
||||||
isCardMediaWithSensitive = status.get('spoiler_text').length > 0;
|
isCardMediaWithSensitive = status.get('spoiler_text').length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account === undefined || account === null) {
|
|
||||||
statusAvatar = <Avatar account={status.get('account')} size={46} />;
|
|
||||||
} else {
|
|
||||||
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
visibilityIcon = visibilityIconInfo[status.get('limited_scope') || status.get('visibility_ex')] || visibilityIconInfo[status.get('visibility')];
|
visibilityIcon = visibilityIconInfo[status.get('limited_scope') || status.get('visibility_ex')] || visibilityIconInfo[status.get('visibility')];
|
||||||
|
|
||||||
let emojiReactionsBar = null;
|
let emojiReactionsBar = null;
|
||||||
|
@ -588,20 +627,24 @@ class Status extends ImmutablePureComponent {
|
||||||
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
||||||
|
|
||||||
const withLimited = status.get('visibility_ex') === 'limited' && status.get('limited_scope') ? <span className='status__visibility-icon'><Icon id='get-pocket' title='Limited' /></span> : null;
|
const withLimited = status.get('visibility_ex') === 'limited' && status.get('limited_scope') ? <span className='status__visibility-icon'><Icon id='get-pocket' title='Limited' /></span> : null;
|
||||||
const withReference = status.get('status_references_count') > 0 ? <span className='status__visibility-icon'><Icon id='link' title='Reference' /></span> : null;
|
const withQuote = status.get('quote_id') ? <span className='status__visibility-icon'><Icon id='quote-right' title='Quote' /></span> : null;
|
||||||
|
const withReference = (!withQuote && status.get('status_references_count') > 0) ? <span className='status__visibility-icon'><Icon id='link' title='Reference' /></span> : null;
|
||||||
const withExpiration = status.get('expires_at') ? <span className='status__visibility-icon'><Icon id='clock-o' title='Expiration' /></span> : null;
|
const withExpiration = status.get('expires_at') ? <span className='status__visibility-icon'><Icon id='clock-o' title='Expiration' /></span> : null;
|
||||||
|
|
||||||
|
const quote = !muted && status.get('quote_id') && (['public', 'community'].includes(contextType) ? showQuoteInPublic : showQuoteInHome) && <CompactedStatusContainer id={status.get('quote_id')} />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers}>
|
<HotKeys handlers={handlers}>
|
||||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility_ex')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
|
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility_ex')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !muted })} tabIndex={muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
|
||||||
{prepend}
|
{prepend}
|
||||||
|
|
||||||
<div className={classNames('status', `status-${status.get('visibility_ex')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
|
<div className={classNames('status', `status-${status.get('visibility_ex')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: muted })} data-id={status.get('id')}>
|
||||||
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
|
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
|
||||||
|
|
||||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||||
<div onClick={this.handleClick} className='status__info'>
|
<div onClick={this.handleClick} className='status__info'>
|
||||||
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||||
|
{withQuote}
|
||||||
{withReference}
|
{withReference}
|
||||||
{withExpiration}
|
{withExpiration}
|
||||||
{withLimited}
|
{withLimited}
|
||||||
|
@ -629,6 +672,8 @@ class Status extends ImmutablePureComponent {
|
||||||
{...statusContentProps}
|
{...statusContentProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{(!status.get('spoiler_text') || expanded) && quote}
|
||||||
|
|
||||||
{(!isCardMediaWithSensitive || !status.get('hidden')) && media}
|
{(!isCardMediaWithSensitive || !status.get('hidden')) && media}
|
||||||
|
|
||||||
{(!status.get('spoiler_text') || expanded) && hashtagBar}
|
{(!status.get('spoiler_text') || expanded) && hashtagBar}
|
||||||
|
|
|
@ -298,6 +298,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
const writtenByMe = status.getIn(['account', 'id']) === me;
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
|
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
|
||||||
|
const allowQuote = status.getIn(['account', 'other_settings', 'allow_quote']);
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
|
@ -332,7 +333,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
if (publicStatus) {
|
if (publicStatus) {
|
||||||
menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
|
menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
|
||||||
menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote });
|
|
||||||
|
if (allowQuote) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClickOriginal });
|
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClickOriginal });
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { openModal } from '../actions/modal';
|
||||||
|
import {
|
||||||
|
hideStatus,
|
||||||
|
revealStatus,
|
||||||
|
toggleStatusCollapse,
|
||||||
|
translateStatus,
|
||||||
|
undoStatusTranslation,
|
||||||
|
} from '../actions/statuses';
|
||||||
|
import CompactedStatus from '../components/compacted_status';
|
||||||
|
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getStatus = makeGetStatus();
|
||||||
|
const getPictureInPicture = makeGetPictureInPicture();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
status: getStatus(state, props),
|
||||||
|
nextInReplyToId: props.nextId ? state.getIn(['statuses', props.nextId, 'in_reply_to_id']) : null,
|
||||||
|
pictureInPicture: getPictureInPicture(state, props),
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
|
||||||
|
onTranslate (status) {
|
||||||
|
if (status.get('translation')) {
|
||||||
|
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
|
||||||
|
} else {
|
||||||
|
dispatch(translateStatus(status.get('id')));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onOpenMedia (statusId, media, index, lang) {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalType: 'MEDIA',
|
||||||
|
modalProps: { statusId, media, index, lang },
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
onOpenVideo (statusId, media, lang, options) {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalType: 'VIDEO',
|
||||||
|
modalProps: { statusId, media, lang, options },
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
onToggleHidden (status) {
|
||||||
|
if (status.get('hidden')) {
|
||||||
|
dispatch(revealStatus(status.get('id')));
|
||||||
|
} else {
|
||||||
|
dispatch(hideStatus(status.get('id')));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onToggleCollapsed (status, isCollapsed) {
|
||||||
|
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
|
||||||
|
},
|
||||||
|
|
||||||
|
onInteractionModal (type, status) {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalType: 'INTERACTION',
|
||||||
|
modalProps: {
|
||||||
|
type,
|
||||||
|
accountId: status.getIn(['account', 'id']),
|
||||||
|
url: status.get('uri'),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(CompactedStatus));
|
|
@ -80,6 +80,8 @@ const makeMapStateToProps = () => {
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
|
|
||||||
|
contextType,
|
||||||
|
|
||||||
onReply (status, router) {
|
onReply (status, router) {
|
||||||
dispatch((_, getState) => {
|
dispatch((_, getState) => {
|
||||||
let state = getState();
|
let state = getState();
|
||||||
|
|
|
@ -236,6 +236,7 @@ class ActionBar extends PureComponent {
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
const writtenByMe = status.getIn(['account', 'id']) === me;
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
|
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
|
||||||
|
const allowQuote = status.getIn(['account', 'other_settings', 'allow_quote']);
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
|
@ -259,7 +260,10 @@ class ActionBar extends PureComponent {
|
||||||
|
|
||||||
if (publicStatus) {
|
if (publicStatus) {
|
||||||
menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
|
menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
|
||||||
menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote });
|
|
||||||
|
if (allowQuote) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
menu.push({ text: intl.formatMessage(messages.bookmark_category), action: this.handleBookmarkCategoryAdderClick });
|
menu.push({ text: intl.formatMessage(messages.bookmark_category), action: this.handleBookmarkCategoryAdderClick });
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
* @property {boolean} enable_local_privacy
|
* @property {boolean} enable_local_privacy
|
||||||
* @property {boolean} enable_dtl_menu
|
* @property {boolean} enable_dtl_menu
|
||||||
* @property {boolean=} expand_spoilers
|
* @property {boolean=} expand_spoilers
|
||||||
|
* @property {boolean} hide_blocking_quote
|
||||||
* @property {boolean} hide_recent_emojis
|
* @property {boolean} hide_recent_emojis
|
||||||
* @property {boolean} limited_federation_mode
|
* @property {boolean} limited_federation_mode
|
||||||
* @property {string} locale
|
* @property {string} locale
|
||||||
|
@ -78,6 +79,8 @@
|
||||||
* @property {boolean} search_enabled
|
* @property {boolean} search_enabled
|
||||||
* @property {boolean} trends_enabled
|
* @property {boolean} trends_enabled
|
||||||
* @property {boolean} show_emoji_reaction_on_timeline
|
* @property {boolean} show_emoji_reaction_on_timeline
|
||||||
|
* @property {boolean} show_quote_in_home
|
||||||
|
* @property {boolean} show_quote_in_public
|
||||||
* @property {string} simple_timeline_menu
|
* @property {string} simple_timeline_menu
|
||||||
* @property {boolean} single_user_mode
|
* @property {boolean} single_user_mode
|
||||||
* @property {string} source_url
|
* @property {string} source_url
|
||||||
|
@ -136,6 +139,7 @@ export const enableLoginPrivacy = getMeta('enable_login_privacy');
|
||||||
export const enableDtlMenu = getMeta('enable_dtl_menu');
|
export const enableDtlMenu = getMeta('enable_dtl_menu');
|
||||||
export const expandSpoilers = getMeta('expand_spoilers');
|
export const expandSpoilers = getMeta('expand_spoilers');
|
||||||
export const forceSingleColumn = !getMeta('advanced_layout');
|
export const forceSingleColumn = !getMeta('advanced_layout');
|
||||||
|
export const hideBlockingQuote = getMeta('hide_blocking_quote');
|
||||||
export const hideRecentEmojis = getMeta('hide_recent_emojis');
|
export const hideRecentEmojis = getMeta('hide_recent_emojis');
|
||||||
export const limitedFederationMode = getMeta('limited_federation_mode');
|
export const limitedFederationMode = getMeta('limited_federation_mode');
|
||||||
export const mascot = getMeta('mascot');
|
export const mascot = getMeta('mascot');
|
||||||
|
@ -149,6 +153,8 @@ export const repository = getMeta('repository');
|
||||||
export const searchEnabled = getMeta('search_enabled');
|
export const searchEnabled = getMeta('search_enabled');
|
||||||
export const trendsEnabled = getMeta('trends_enabled');
|
export const trendsEnabled = getMeta('trends_enabled');
|
||||||
export const showEmojiReactionOnTimeline = getMeta('show_emoji_reaction_on_timeline');
|
export const showEmojiReactionOnTimeline = getMeta('show_emoji_reaction_on_timeline');
|
||||||
|
export const showQuoteInHome = getMeta('show_quote_in_home');
|
||||||
|
export const showQuoteInPublic = getMeta('show_quote_in_public');
|
||||||
export const showTrends = getMeta('show_trends');
|
export const showTrends = getMeta('show_trends');
|
||||||
export const simpleTimelineMenu = getMeta('simple_timeline_menu');
|
export const simpleTimelineMenu = getMeta('simple_timeline_menu');
|
||||||
export const singleUserMode = getMeta('single_user_mode');
|
export const singleUserMode = getMeta('single_user_mode');
|
||||||
|
|
|
@ -692,7 +692,7 @@
|
||||||
"status.open": "Expand this post",
|
"status.open": "Expand this post",
|
||||||
"status.pin": "Pin on profile",
|
"status.pin": "Pin on profile",
|
||||||
"status.pinned": "Pinned post",
|
"status.pinned": "Pinned post",
|
||||||
"status.quote": "Ref (quote in other servers)",
|
"status.quote": "Quote",
|
||||||
"status.read_more": "Read more",
|
"status.read_more": "Read more",
|
||||||
"status.reblog": "Boost",
|
"status.reblog": "Boost",
|
||||||
"status.reblog_private": "Boost with original visibility",
|
"status.reblog_private": "Boost with original visibility",
|
||||||
|
|
|
@ -778,7 +778,7 @@
|
||||||
"status.open": "詳細を表示",
|
"status.open": "詳細を表示",
|
||||||
"status.pin": "プロフィールに固定表示",
|
"status.pin": "プロフィールに固定表示",
|
||||||
"status.pinned": "固定された投稿",
|
"status.pinned": "固定された投稿",
|
||||||
"status.quote": "参照 (他サーバーで引用扱い)",
|
"status.quote": "引用",
|
||||||
"status.read_more": "もっと見る",
|
"status.read_more": "もっと見る",
|
||||||
"status.reblog": "ブースト",
|
"status.reblog": "ブースト",
|
||||||
"status.reblog_private": "ブースト",
|
"status.reblog_private": "ブースト",
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import { toServerSideType } from 'mastodon/utils/filters';
|
import { toServerSideType } from 'mastodon/utils/filters';
|
||||||
|
|
||||||
import { me } from '../initial_state';
|
import { me, hideBlockingQuote } from '../initial_state';
|
||||||
|
|
||||||
const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
|
const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
|
||||||
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
|
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
|
||||||
|
@ -53,7 +53,12 @@ export const makeGetStatus = () => {
|
||||||
statusReblog = null;
|
statusReblog = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hideBlockingQuote && statusBase.getIn(['quote', 'quote_muted'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let filtered = false;
|
let filtered = false;
|
||||||
|
let filterAction = 'warn';
|
||||||
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
||||||
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
||||||
if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
||||||
|
@ -62,6 +67,7 @@ export const makeGetStatus = () => {
|
||||||
filterResults = filterResults.filter(result => filters.has(result.get('filter')));
|
filterResults = filterResults.filter(result => filters.has(result.get('filter')));
|
||||||
if (!filterResults.isEmpty()) {
|
if (!filterResults.isEmpty()) {
|
||||||
filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
|
filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
|
||||||
|
filterAction = filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'warn') ? 'warn' : 'half_warn';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +75,7 @@ export const makeGetStatus = () => {
|
||||||
map.set('reblog', statusReblog);
|
map.set('reblog', statusReblog);
|
||||||
map.set('account', accountBase);
|
map.set('account', accountBase);
|
||||||
map.set('matched_filters', filtered);
|
map.set('matched_filters', filtered);
|
||||||
|
map.set('filter_action', filterAction);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1733,6 +1733,11 @@ a.account__display-name {
|
||||||
.status__avatar {
|
.status__avatar {
|
||||||
width: 46px;
|
width: 46px;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
|
|
||||||
|
&.status__avatar__compact {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted {
|
.muted {
|
||||||
|
@ -6519,7 +6524,7 @@ a.status-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 64px;
|
min-height: 64px;
|
||||||
max-height: 70vh;
|
max-height: 60vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 50% 50%;
|
grid-template-columns: 50% 50%;
|
||||||
grid-template-rows: 50% 50%;
|
grid-template-rows: 50% 50%;
|
||||||
|
@ -6540,6 +6545,10 @@ a.status-card {
|
||||||
&--column4 {
|
&--column4 {
|
||||||
grid-template-columns: 25% 25% 25% 25%;
|
grid-template-columns: 25% 25% 25% 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__compact {
|
||||||
|
max-height: 24vh;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-gallery__item {
|
.media-gallery__item {
|
||||||
|
@ -8479,6 +8488,13 @@ noscript {
|
||||||
.status__wrapper {
|
.status__wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&.status__wrapper__compact {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid $ui-primary-color;
|
||||||
|
margin-block-start: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
&.unread {
|
&.unread {
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
|
|
@ -444,7 +444,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
|
|
||||||
def related_to_local_activity?
|
def related_to_local_activity?
|
||||||
fetch? || followed_by_local_accounts? || requested_through_relay? ||
|
fetch? || followed_by_local_accounts? || requested_through_relay? ||
|
||||||
responds_to_followed_account? || addresses_local_accounts?
|
responds_to_followed_account? || addresses_local_accounts? || quote_local?
|
||||||
end
|
end
|
||||||
|
|
||||||
def responds_to_followed_account?
|
def responds_to_followed_account?
|
||||||
|
@ -485,10 +485,22 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
|
|
||||||
def process_references!
|
def process_references!
|
||||||
references = @object['references'].nil? ? [] : ActivityPub::FetchReferencesService.new.call(@status, @object['references'])
|
references = @object['references'].nil? ? [] : ActivityPub::FetchReferencesService.new.call(@status, @object['references'])
|
||||||
quote = @object['quote'] || @object['quoteUrl'] || @object['quoteURL'] || @object['_misskey_quote']
|
|
||||||
references << quote if quote
|
|
||||||
|
|
||||||
ProcessReferencesService.perform_worker_async(@status, [], references)
|
ProcessReferencesService.perform_worker_async(@status, [], references, [quote].compact)
|
||||||
|
end
|
||||||
|
|
||||||
|
def quote_local?
|
||||||
|
url = quote
|
||||||
|
|
||||||
|
if url.present?
|
||||||
|
ResolveURLService.new.call(url, on_behalf_of: @account, local_only: true).present?
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def quote
|
||||||
|
@quote ||= @object['quote'] || @object['quoteUrl'] || @object['quoteURL'] || @object['_misskey_quote']
|
||||||
end
|
end
|
||||||
|
|
||||||
def join_group!
|
def join_group!
|
||||||
|
|
|
@ -54,6 +54,7 @@ class StatusReachFinder
|
||||||
reblogs_account_ids,
|
reblogs_account_ids,
|
||||||
favourites_account_ids,
|
favourites_account_ids,
|
||||||
replies_account_ids,
|
replies_account_ids,
|
||||||
|
quoted_account_id,
|
||||||
].tap do |arr|
|
].tap do |arr|
|
||||||
arr.flatten!
|
arr.flatten!
|
||||||
arr.compact!
|
arr.compact!
|
||||||
|
@ -88,6 +89,10 @@ class StatusReachFinder
|
||||||
@status.replies.pluck(:account_id) if distributable? || unsafe?
|
@status.replies.pluck(:account_id) if distributable? || unsafe?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def quoted_account_id
|
||||||
|
@status.quote.account_id if @status.quote?
|
||||||
|
end
|
||||||
|
|
||||||
def followers_inboxes
|
def followers_inboxes
|
||||||
if @status.in_reply_to_local_account? && distributable?
|
if @status.in_reply_to_local_account? && distributable?
|
||||||
@status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where.not(domain: banned_domains).inboxes
|
@status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where.not(domain: banned_domains).inboxes
|
||||||
|
|
|
@ -330,6 +330,13 @@ class Account < ApplicationRecord
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def allow_quote?
|
||||||
|
return user.setting_allow_quote if local? && user.present?
|
||||||
|
return settings['allow_quote'] if settings.present? && settings.key?('allow_quote')
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
def public_statuses_count
|
def public_statuses_count
|
||||||
hide_statuses_count? ? 0 : statuses_count
|
hide_statuses_count? ? 0 : statuses_count
|
||||||
end
|
end
|
||||||
|
@ -407,6 +414,7 @@ class Account < ApplicationRecord
|
||||||
'hide_followers_count' => hide_followers_count?,
|
'hide_followers_count' => hide_followers_count?,
|
||||||
'translatable_private' => translatable_private?,
|
'translatable_private' => translatable_private?,
|
||||||
'link_preview' => link_preview?,
|
'link_preview' => link_preview?,
|
||||||
|
'allow_quote' => allow_quote?,
|
||||||
}
|
}
|
||||||
if Setting.enable_emoji_reaction
|
if Setting.enable_emoji_reaction
|
||||||
config = config.merge({
|
config = config.merge({
|
||||||
|
|
|
@ -111,6 +111,22 @@ module HasUserSettings
|
||||||
settings['web.use_system_font']
|
settings['web.use_system_font']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def setting_show_quote_in_home
|
||||||
|
settings['web.show_quote_in_home']
|
||||||
|
end
|
||||||
|
|
||||||
|
def setting_show_quote_in_public
|
||||||
|
settings['web.show_quote_in_public']
|
||||||
|
end
|
||||||
|
|
||||||
|
def setting_hide_blocking_quote
|
||||||
|
settings['web.hide_blocking_quote']
|
||||||
|
end
|
||||||
|
|
||||||
|
def setting_allow_quote
|
||||||
|
settings['allow_quote']
|
||||||
|
end
|
||||||
|
|
||||||
def setting_noindex
|
def setting_noindex
|
||||||
settings['noindex']
|
settings['noindex']
|
||||||
end
|
end
|
||||||
|
@ -127,10 +143,6 @@ module HasUserSettings
|
||||||
settings['link_preview']
|
settings['link_preview']
|
||||||
end
|
end
|
||||||
|
|
||||||
def setting_single_ref_to_quote
|
|
||||||
settings['single_ref_to_quote']
|
|
||||||
end
|
|
||||||
|
|
||||||
def setting_dtl_force_with_tag
|
def setting_dtl_force_with_tag
|
||||||
settings['dtl_force_with_tag']&.to_sym || :none
|
settings['dtl_force_with_tag']&.to_sym || :none
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
# action :integer default("warn"), not null
|
# action :integer default("warn"), not null
|
||||||
# exclude_follows :boolean default(FALSE), not null
|
# exclude_follows :boolean default(FALSE), not null
|
||||||
# exclude_localusers :boolean default(FALSE), not null
|
# exclude_localusers :boolean default(FALSE), not null
|
||||||
|
# with_quote :boolean default(TRUE), not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class CustomFilter < ApplicationRecord
|
class CustomFilter < ApplicationRecord
|
||||||
|
@ -33,7 +34,7 @@ class CustomFilter < ApplicationRecord
|
||||||
include Expireable
|
include Expireable
|
||||||
include Redisable
|
include Redisable
|
||||||
|
|
||||||
enum action: { warn: 0, hide: 1 }, _suffix: :action
|
enum action: { warn: 0, hide: 1, half_warn: 2 }, _suffix: :action
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy
|
has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy
|
||||||
|
@ -103,11 +104,15 @@ class CustomFilter < ApplicationRecord
|
||||||
|
|
||||||
if rules[:keywords].present?
|
if rules[:keywords].present?
|
||||||
match = rules[:keywords].match(status.proper.searchable_text)
|
match = rules[:keywords].match(status.proper.searchable_text)
|
||||||
match = rules[:keywords].match(status.proper.references.pluck(:text).join("\n\n")) if match.nil? && status.proper.references.exists?
|
if match.nil? && filter.with_quote && status.proper.references.exists?
|
||||||
|
match = rules[:keywords].match(status.proper.references.pluck(:text).join("\n\n"))
|
||||||
|
match = rules[:keywords].match(status.proper.references.pluck(:spoiler_text).join("\n\n")) if match.nil?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
keyword_matches = [match.to_s] unless match.nil?
|
keyword_matches = [match.to_s] unless match.nil?
|
||||||
|
|
||||||
status_matches = [status.id, status.reblog_of_id].compact & rules[:status_ids] if rules[:status_ids].present?
|
reference_ids = filter.with_quote ? status.proper.references.pluck(:id) : []
|
||||||
|
status_matches = ([status.id, status.reblog_of_id] + reference_ids).compact & rules[:status_ids] if rules[:status_ids].present?
|
||||||
|
|
||||||
next if keyword_matches.blank? && status_matches.blank?
|
next if keyword_matches.blank? && status_matches.blank?
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
# searchability :integer
|
# searchability :integer
|
||||||
# markdown :boolean default(FALSE)
|
# markdown :boolean default(FALSE)
|
||||||
# limited_scope :integer
|
# limited_scope :integer
|
||||||
|
# quote_of_id :bigint(8)
|
||||||
#
|
#
|
||||||
|
|
||||||
require 'ostruct'
|
require 'ostruct'
|
||||||
|
@ -69,12 +70,14 @@ class Status < ApplicationRecord
|
||||||
|
|
||||||
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
|
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
|
||||||
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
|
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
|
||||||
|
belongs_to :quote, foreign_key: 'quote_of_id', class_name: 'Status', inverse_of: :quotes, optional: true
|
||||||
|
|
||||||
has_many :favourites, inverse_of: :status, dependent: :destroy
|
has_many :favourites, inverse_of: :status, dependent: :destroy
|
||||||
has_many :emoji_reactions, inverse_of: :status, dependent: :destroy
|
has_many :emoji_reactions, inverse_of: :status, dependent: :destroy
|
||||||
has_many :bookmarks, inverse_of: :status, dependent: :destroy
|
has_many :bookmarks, inverse_of: :status, dependent: :destroy
|
||||||
has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy
|
has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy
|
||||||
has_many :reblogged_by_accounts, through: :reblogs, class_name: 'Account', source: :account
|
has_many :reblogged_by_accounts, through: :reblogs, class_name: 'Account', source: :account
|
||||||
|
has_many :quotes, foreign_key: 'quote_of_id', class_name: 'Status', inverse_of: :quote
|
||||||
has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread
|
has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread
|
||||||
has_many :mentions, dependent: :destroy, inverse_of: :status
|
has_many :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'
|
||||||
|
@ -193,6 +196,19 @@ class Status < ApplicationRecord
|
||||||
account: [:account_stat, user: :role],
|
account: [:account_stat, user: :role],
|
||||||
active_mentions: { account: :account_stat },
|
active_mentions: { account: :account_stat },
|
||||||
],
|
],
|
||||||
|
quote: [
|
||||||
|
:application,
|
||||||
|
:tags,
|
||||||
|
:preview_cards,
|
||||||
|
:media_attachments,
|
||||||
|
:conversation,
|
||||||
|
:status_stat,
|
||||||
|
:preloadable_poll,
|
||||||
|
:reference_objects,
|
||||||
|
:scheduled_expiration_status,
|
||||||
|
account: [:account_stat, user: :role],
|
||||||
|
active_mentions: { account: :account_stat },
|
||||||
|
],
|
||||||
thread: { account: :account_stat }
|
thread: { account: :account_stat }
|
||||||
|
|
||||||
delegate :domain, to: :account, prefix: true
|
delegate :domain, to: :account, prefix: true
|
||||||
|
@ -227,8 +243,8 @@ class Status < ApplicationRecord
|
||||||
!reblog_of_id.nil?
|
!reblog_of_id.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
def quote
|
def quote?
|
||||||
reference_objects.where(attribute_type: 'QT').first&.target_status
|
!quote_of_id.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
def within_realtime_window?
|
def within_realtime_window?
|
||||||
|
@ -480,12 +496,16 @@ class Status < ApplicationRecord
|
||||||
ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).each_with_object({}) { |m, h| h[m.conversation_id] = true }
|
ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).each_with_object({}) { |m, h| h[m.conversation_id] = true }
|
||||||
end
|
end
|
||||||
|
|
||||||
def pins_map(status_ids, account_id)
|
def blocks_map(account_ids, account_id)
|
||||||
StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true }
|
Block.where(account_id: account_id, target_account_id: account_ids).each_with_object({}) { |b, h| h[b.target_account_id] = true }
|
||||||
end
|
end
|
||||||
|
|
||||||
def emoji_reactions_map(status_ids, account_id)
|
def domain_blocks_map(domains, account_id)
|
||||||
EmojiReaction.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |e, h| h[e.status_id] = true }
|
AccountDomainBlock.where(account_id: account_id, domain: domains).each_with_object({}) { |d, h| h[d.domain] = true }
|
||||||
|
end
|
||||||
|
|
||||||
|
def pins_map(status_ids, account_id)
|
||||||
|
StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true }
|
||||||
end
|
end
|
||||||
|
|
||||||
def emoji_reaction_allows_map(status_ids, account_id)
|
def emoji_reaction_allows_map(status_ids, account_id)
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# attribute_type :string
|
# attribute_type :string
|
||||||
|
# quote :boolean default(FALSE), not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class StatusReference < ApplicationRecord
|
class StatusReference < ApplicationRecord
|
||||||
|
@ -19,6 +20,8 @@ class StatusReference < ApplicationRecord
|
||||||
has_one :notification, as: :activity, dependent: :destroy
|
has_one :notification, as: :activity, dependent: :destroy
|
||||||
|
|
||||||
after_commit :reset_parent_cache
|
after_commit :reset_parent_cache
|
||||||
|
after_create_commit :set_quote
|
||||||
|
after_destroy_commit :remove_quote
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
@ -26,4 +29,18 @@ class StatusReference < ApplicationRecord
|
||||||
Rails.cache.delete("statuses/#{status_id}")
|
Rails.cache.delete("statuses/#{status_id}")
|
||||||
Rails.cache.delete("statuses/#{target_status_id}")
|
Rails.cache.delete("statuses/#{target_status_id}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_quote
|
||||||
|
return unless quote
|
||||||
|
return if status.quote_of_id.present?
|
||||||
|
|
||||||
|
status.quote_of_id = target_status_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_quote
|
||||||
|
return unless quote
|
||||||
|
return unless status.quote_of_id == target_status_id
|
||||||
|
|
||||||
|
status.quote_of_id = nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -41,7 +41,7 @@ class UserSettings
|
||||||
setting :dtl_force_with_tag, default: :none, in: %w(full searchability none)
|
setting :dtl_force_with_tag, default: :none, in: %w(full searchability none)
|
||||||
setting :dtl_force_subscribable, default: false
|
setting :dtl_force_subscribable, default: false
|
||||||
setting :lock_follow_from_bot, default: false
|
setting :lock_follow_from_bot, default: false
|
||||||
setting :single_ref_to_quote, default: false
|
setting :allow_quote, default: true
|
||||||
|
|
||||||
setting_inverse_alias :indexable, :noindex
|
setting_inverse_alias :indexable, :noindex
|
||||||
|
|
||||||
|
@ -67,6 +67,9 @@ class UserSettings
|
||||||
setting :display_media_expand, default: true
|
setting :display_media_expand, default: true
|
||||||
setting :auto_play, default: true
|
setting :auto_play, default: true
|
||||||
setting :simple_timeline_menu, default: false
|
setting :simple_timeline_menu, default: false
|
||||||
|
setting :show_quote_in_home, default: true
|
||||||
|
setting :show_quote_in_public, default: false
|
||||||
|
setting :hide_blocking_quote, default: true
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :notification_emails do
|
namespace :notification_emails do
|
||||||
|
|
|
@ -40,6 +40,10 @@ class StatusPolicy < ApplicationPolicy
|
||||||
show? && !blocking_author?
|
show? && !blocking_author?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def quote?
|
||||||
|
%i(public public_unlisted unlisted).include?(record.visibility.to_sym) && show? && !blocking_author?
|
||||||
|
end
|
||||||
|
|
||||||
def destroy?
|
def destroy?
|
||||||
owned?
|
owned?
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
class StatusRelationshipsPresenter
|
class StatusRelationshipsPresenter
|
||||||
PINNABLE_VISIBILITIES = %w(public public_unlisted unlisted login private).freeze
|
PINNABLE_VISIBILITIES = %w(public public_unlisted unlisted login private).freeze
|
||||||
|
|
||||||
attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map,
|
attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map, :blocks_map, :domain_blocks_map,
|
||||||
:bookmarks_map, :filters_map, :emoji_reactions_map, :attributes_map, :emoji_reaction_allows_map
|
:bookmarks_map, :filters_map, :attributes_map, :emoji_reaction_allows_map
|
||||||
|
|
||||||
def initialize(statuses, current_account_id = nil, **options)
|
def initialize(statuses, current_account_id = nil, **options)
|
||||||
@current_account_id = current_account_id
|
@current_account_id = current_account_id
|
||||||
|
@ -14,25 +14,28 @@ class StatusRelationshipsPresenter
|
||||||
@favourites_map = {}
|
@favourites_map = {}
|
||||||
@bookmarks_map = {}
|
@bookmarks_map = {}
|
||||||
@mutes_map = {}
|
@mutes_map = {}
|
||||||
|
@blocks_map = {}
|
||||||
|
@domain_blocks_map = {}
|
||||||
@pins_map = {}
|
@pins_map = {}
|
||||||
@filters_map = {}
|
@filters_map = {}
|
||||||
@emoji_reactions_map = {}
|
|
||||||
@emoji_reaction_allows_map = nil
|
@emoji_reaction_allows_map = nil
|
||||||
else
|
else
|
||||||
statuses = statuses.compact
|
statuses = statuses.compact
|
||||||
|
statuses += statuses.filter_map(&:quote)
|
||||||
status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact
|
status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact
|
||||||
conversation_ids = statuses.filter_map(&:conversation_id).uniq
|
conversation_ids = statuses.filter_map(&:conversation_id).uniq
|
||||||
pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && PINNABLE_VISIBILITIES.include?(s.visibility) }
|
pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && PINNABLE_VISIBILITIES.include?(s.visibility) }
|
||||||
|
|
||||||
@filters_map = build_filters_map(statuses, current_account_id).merge(options[:filters_map] || {})
|
@filters_map = build_filters_map(statuses, current_account_id).merge(options[:filters_map] || {})
|
||||||
@reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {})
|
@reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {})
|
||||||
@favourites_map = Status.favourites_map(status_ids, current_account_id).merge(options[:favourites_map] || {})
|
@favourites_map = Status.favourites_map(status_ids, current_account_id).merge(options[:favourites_map] || {})
|
||||||
@bookmarks_map = Status.bookmarks_map(status_ids, current_account_id).merge(options[:bookmarks_map] || {})
|
@bookmarks_map = Status.bookmarks_map(status_ids, current_account_id).merge(options[:bookmarks_map] || {})
|
||||||
@mutes_map = Status.mutes_map(conversation_ids, current_account_id).merge(options[:mutes_map] || {})
|
@mutes_map = Status.mutes_map(conversation_ids, current_account_id).merge(options[:mutes_map] || {})
|
||||||
@pins_map = Status.pins_map(pinnable_status_ids, current_account_id).merge(options[:pins_map] || {})
|
@blocks_map = Status.blocks_map(statuses.map(&:account_id), current_account_id).merge(options[:blocks_map] || {})
|
||||||
@emoji_reactions_map = Status.emoji_reactions_map(status_ids, current_account_id).merge(options[:emoji_reactions_map] || {})
|
@domain_blocks_map = Status.domain_blocks_map(statuses.filter_map { |status| status.account.domain }.uniq, current_account_id).merge(options[:domain_blocks_map] || {})
|
||||||
|
@pins_map = Status.pins_map(pinnable_status_ids, current_account_id).merge(options[:pins_map] || {})
|
||||||
@emoji_reaction_allows_map = Status.emoji_reaction_allows_map(status_ids, current_account_id).merge(options[:emoji_reaction_allows_map] || {})
|
@emoji_reaction_allows_map = Status.emoji_reaction_allows_map(status_ids, current_account_id).merge(options[:emoji_reaction_allows_map] || {})
|
||||||
@attributes_map = options[:attributes_map] || {}
|
@attributes_map = options[:attributes_map] || {}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -170,12 +170,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
object.account.local?
|
object.account.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
def quote?
|
delegate :quote?, to: :object
|
||||||
@quote ||= (object.reference_objects.count == 1 && object.account.user&.settings&.[]('single_ref_to_quote')) || object.reference_objects.where(attribute_type: 'QT').count == 1
|
|
||||||
end
|
|
||||||
|
|
||||||
def quote_post
|
def quote_post
|
||||||
@quote_post ||= object.quote || object.references.first
|
@quote_post ||= object.quote
|
||||||
end
|
end
|
||||||
|
|
||||||
def quote_uri
|
def quote_uri
|
||||||
|
|
|
@ -62,6 +62,9 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
store[:show_trends] = Setting.trends && object.current_account.user.setting_trends
|
store[:show_trends] = Setting.trends && object.current_account.user.setting_trends
|
||||||
store[:bookmark_category_needed] = object.current_account.user.setting_bookmark_category_needed
|
store[:bookmark_category_needed] = object.current_account.user.setting_bookmark_category_needed
|
||||||
store[:simple_timeline_menu] = object.current_account.user.setting_simple_timeline_menu
|
store[:simple_timeline_menu] = object.current_account.user.setting_simple_timeline_menu
|
||||||
|
store[:show_quote_in_home] = object.current_account.user.setting_show_quote_in_home
|
||||||
|
store[:show_quote_in_public] = object.current_account.user.setting_show_quote_in_public
|
||||||
|
store[:hide_blocking_quote] = object.current_account.user.setting_hide_blocking_quote
|
||||||
else
|
else
|
||||||
store[:auto_play_gif] = Setting.auto_play_gif
|
store[:auto_play_gif] = Setting.auto_play_gif
|
||||||
store[:display_media] = Setting.display_media
|
store[:display_media] = Setting.display_media
|
||||||
|
@ -69,6 +72,8 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
store[:use_blurhash] = Setting.use_blurhash
|
store[:use_blurhash] = Setting.use_blurhash
|
||||||
store[:enable_emoji_reaction] = Setting.enable_emoji_reaction
|
store[:enable_emoji_reaction] = Setting.enable_emoji_reaction
|
||||||
store[:show_emoji_reaction_on_timeline] = Setting.enable_emoji_reaction
|
store[:show_emoji_reaction_on_timeline] = Setting.enable_emoji_reaction
|
||||||
|
store[:show_quote_in_home] = true
|
||||||
|
store[:show_quote_in_public] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
store[:disabled_account_id] = object.disabled_account.id.to_s if object.disabled_account
|
store[:disabled_account_id] = object.disabled_account.id.to_s if object.disabled_account
|
||||||
|
|
|
@ -16,6 +16,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||||
attribute :pinned, if: :pinnable?
|
attribute :pinned, if: :pinnable?
|
||||||
attribute :reactions, if: :reactions?
|
attribute :reactions, if: :reactions?
|
||||||
attribute :expires_at, if: :will_expire?
|
attribute :expires_at, if: :will_expire?
|
||||||
|
attribute :quote_id, if: :quote?
|
||||||
has_many :filtered, serializer: REST::FilterResultSerializer, if: :current_user?
|
has_many :filtered, serializer: REST::FilterResultSerializer, if: :current_user?
|
||||||
|
|
||||||
attribute :content, unless: :source_requested?
|
attribute :content, unless: :source_requested?
|
||||||
|
@ -33,6 +34,23 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||||
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
||||||
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
||||||
|
|
||||||
|
class QuotedStatusSerializer < REST::StatusSerializer
|
||||||
|
attribute :quote_muted, if: :current_user?
|
||||||
|
|
||||||
|
def quote
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def quote_muted
|
||||||
|
if relationships
|
||||||
|
muted || relationships.blocks_map[object.account_id] || relationships.domain_blocks_map[object.account.domain] || false
|
||||||
|
else
|
||||||
|
muted || current_user.account.blocking?(object.account_id) || current_user.account.domain_blocking?(object.account.domain)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
belongs_to :quote, if: :quote?, serializer: QuotedStatusSerializer, relationships: -> { relationships }
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
end
|
end
|
||||||
|
@ -159,6 +177,12 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def quote_id
|
||||||
|
object.quote_of_id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
delegate :quote?, to: :object
|
||||||
|
|
||||||
def reblogged
|
def reblogged
|
||||||
if relationships
|
if relationships
|
||||||
relationships.reblogs_map[object.id] || false
|
relationships.reblogs_map[object.id] || false
|
||||||
|
|
|
@ -254,9 +254,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
def update_references!
|
def update_references!
|
||||||
references = @json['references'].nil? ? [] : ActivityPub::FetchReferencesService.new.call(@status, @json['references'])
|
references = @json['references'].nil? ? [] : ActivityPub::FetchReferencesService.new.call(@status, @json['references'])
|
||||||
quote = @json['quote'] || @json['quoteUrl'] || @json['quoteURL'] || @json['_misskey_quote']
|
quote = @json['quote'] || @json['quoteUrl'] || @json['quoteURL'] || @json['_misskey_quote']
|
||||||
references << quote if quote
|
|
||||||
|
|
||||||
ProcessReferencesService.perform_worker_async(@status, [], references)
|
ProcessReferencesService.perform_worker_async(@status, [], references, [quote].compact)
|
||||||
end
|
end
|
||||||
|
|
||||||
def expected_type?
|
def expected_type?
|
||||||
|
|
|
@ -10,14 +10,17 @@ class ProcessReferencesService < BaseService
|
||||||
REFURL_EXP = /(RT|QT|BT|RN|RE)((:|;)?\s+|:|;)(#{URI::DEFAULT_PARSER.make_regexp(%w(http https))})/
|
REFURL_EXP = /(RT|QT|BT|RN|RE)((:|;)?\s+|:|;)(#{URI::DEFAULT_PARSER.make_regexp(%w(http https))})/
|
||||||
MAX_REFERENCES = 5
|
MAX_REFERENCES = 5
|
||||||
|
|
||||||
def call(status, reference_parameters, urls: nil, fetch_remote: true, no_fetch_urls: nil)
|
def call(status, reference_parameters, urls: nil, fetch_remote: true, no_fetch_urls: nil, quote_urls: nil)
|
||||||
@status = status
|
@status = status
|
||||||
@reference_parameters = reference_parameters || []
|
@reference_parameters = reference_parameters || []
|
||||||
@urls = urls || []
|
@quote_urls = quote_urls || []
|
||||||
|
@urls = (urls - @quote_urls) || []
|
||||||
@no_fetch_urls = no_fetch_urls || []
|
@no_fetch_urls = no_fetch_urls || []
|
||||||
@fetch_remote = fetch_remote
|
@fetch_remote = fetch_remote
|
||||||
@again = false
|
@again = false
|
||||||
|
|
||||||
|
@attributes = {}
|
||||||
|
|
||||||
with_redis_lock("process_status_refs:#{@status.id}") do
|
with_redis_lock("process_status_refs:#{@status.id}") do
|
||||||
@references_count = old_references.size
|
@references_count = old_references.size
|
||||||
|
|
||||||
|
@ -38,27 +41,27 @@ class ProcessReferencesService < BaseService
|
||||||
launch_worker if @again
|
launch_worker if @again
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.need_process?(status, reference_parameters, urls)
|
def self.need_process?(status, reference_parameters, urls, quote_urls)
|
||||||
reference_parameters.any? || (urls || []).any? || FormattingHelper.extract_status_plain_text(status).scan(REFURL_EXP).pluck(3).uniq.any?
|
reference_parameters.any? || (urls || []).any? || (quote_urls || []).any? || FormattingHelper.extract_status_plain_text(status).scan(REFURL_EXP).pluck(3).uniq.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.perform_worker_async(status, reference_parameters, urls)
|
def self.perform_worker_async(status, reference_parameters, urls, quote_urls)
|
||||||
return unless need_process?(status, reference_parameters, urls)
|
return unless need_process?(status, reference_parameters, urls, quote_urls)
|
||||||
|
|
||||||
Rails.cache.write("status_reference:#{status.id}", true, expires_in: 10.minutes)
|
Rails.cache.write("status_reference:#{status.id}", true, expires_in: 10.minutes)
|
||||||
ProcessReferencesWorker.perform_async(status.id, reference_parameters, urls, [])
|
ProcessReferencesWorker.perform_async(status.id, reference_parameters, urls, [], quote_urls || [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.call_service(status, reference_parameters, urls)
|
def self.call_service(status, reference_parameters, urls, quote_urls = [])
|
||||||
return unless need_process?(status, reference_parameters, urls)
|
return unless need_process?(status, reference_parameters, urls, quote_urls)
|
||||||
|
|
||||||
ProcessReferencesService.new.call(status, reference_parameters || [], urls: urls || [], fetch_remote: false)
|
ProcessReferencesService.new.call(status, reference_parameters || [], urls: urls || [], fetch_remote: false, quote_urls: quote_urls)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def references
|
def references
|
||||||
@references ||= @reference_parameters + scan_text!
|
@references ||= @reference_parameters + scan_text! + quote_status_ids
|
||||||
end
|
end
|
||||||
|
|
||||||
def old_references
|
def old_references
|
||||||
|
@ -88,12 +91,24 @@ class ProcessReferencesService < BaseService
|
||||||
target_urls = urls + @urls
|
target_urls = urls + @urls
|
||||||
|
|
||||||
target_urls.map do |url|
|
target_urls.map do |url|
|
||||||
status = ResolveURLService.new.call(url, on_behalf_of: @status.account, fetch_remote: @fetch_remote && @no_fetch_urls.exclude?(url))
|
status = url_to_status(url)
|
||||||
@no_fetch_urls << url if !@fetch_remote && status.present? && status.local?
|
@no_fetch_urls << url if !@fetch_remote && status.present?
|
||||||
status
|
status
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def url_to_status(url)
|
||||||
|
ResolveURLService.new.call(url, on_behalf_of: @status.account, fetch_remote: @fetch_remote && @no_fetch_urls.exclude?(url))
|
||||||
|
end
|
||||||
|
|
||||||
|
def quote_status_ids
|
||||||
|
@quote_status_ids ||= @quote_urls.filter_map { |url| url_to_status(url) }.map(&:id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def quotable?(target_status)
|
||||||
|
@status.account.allow_quote? && StatusPolicy.new(@status.account, target_status).quote?
|
||||||
|
end
|
||||||
|
|
||||||
def add_references
|
def add_references
|
||||||
return if added_references.empty?
|
return if added_references.empty?
|
||||||
|
|
||||||
|
@ -101,7 +116,12 @@ class ProcessReferencesService < BaseService
|
||||||
|
|
||||||
statuses = Status.where(id: added_references)
|
statuses = Status.where(id: added_references)
|
||||||
statuses.each do |status|
|
statuses.each do |status|
|
||||||
@added_objects << @status.reference_objects.new(target_status: status, attribute_type: @attributes[status.id])
|
attribute_type = quote_status_ids.include?(status.id) ? 'QT' : @attributes[status.id]
|
||||||
|
attribute_type = 'BT' unless quotable?(status)
|
||||||
|
quote_type = attribute_type.present? ? attribute_type.casecmp('QT').zero? : false
|
||||||
|
@status.quote_of_id = status.id if quote_type && (@status.quote_of_id.nil? || references.exclude?(@status.quote_of_id))
|
||||||
|
@added_objects << @status.reference_objects.new(target_status: status, attribute_type: attribute_type, quote: quote_type)
|
||||||
|
|
||||||
status.increment_count!(:status_referred_by_count)
|
status.increment_count!(:status_referred_by_count)
|
||||||
@references_count += 1
|
@references_count += 1
|
||||||
|
|
||||||
|
@ -133,6 +153,6 @@ class ProcessReferencesService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def launch_worker
|
def launch_worker
|
||||||
ProcessReferencesWorker.perform_async(@status.id, @reference_parameters, @urls, @no_fetch_urls)
|
ProcessReferencesWorker.perform_async(@status.id, @reference_parameters, @urls, @no_fetch_urls, @quote_urls)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,16 +6,16 @@ class ResolveURLService < BaseService
|
||||||
|
|
||||||
USERNAME_STATUS_RE = %r{/@(?<username>#{Account::USERNAME_RE})/(?<status_id>[0-9]+)\Z}
|
USERNAME_STATUS_RE = %r{/@(?<username>#{Account::USERNAME_RE})/(?<status_id>[0-9]+)\Z}
|
||||||
|
|
||||||
def call(url, on_behalf_of: nil, fetch_remote: true)
|
def call(url, on_behalf_of: nil, fetch_remote: true, local_only: false)
|
||||||
@url = url
|
@url = url
|
||||||
@on_behalf_of = on_behalf_of
|
@on_behalf_of = on_behalf_of
|
||||||
@fetch_remote = fetch_remote
|
@fetch_remote = fetch_remote
|
||||||
|
|
||||||
if local_url?
|
if local_url?
|
||||||
process_local_url
|
process_local_url
|
||||||
elsif fetch_remote && !fetched_resource.nil?
|
elsif !local_only && fetch_remote && !fetched_resource.nil?
|
||||||
process_url
|
process_url
|
||||||
else
|
elsif !local_only
|
||||||
process_url_from_db
|
process_url_from_db
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,12 +10,15 @@
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :filter_action, as: :radio_buttons, collection: %i(warn hide), include_blank: false, wrapper: :with_block_label, label_method: ->(action) { safe_join([t("simple_form.labels.filters.actions.#{action}"), content_tag(:span, t("simple_form.hints.filters.actions.#{action}"), class: 'hint')]) }, hint: t('simple_form.hints.filters.action'), required: true
|
= f.input :filter_action, as: :radio_buttons, collection: %i(half_warn warn hide), include_blank: false, wrapper: :with_block_label, label_method: ->(action) { safe_join([t("simple_form.labels.filters.actions.#{action}"), content_tag(:span, t("simple_form.hints.filters.actions.#{action}"), class: 'hint')]) }, hint: t('simple_form.hints.filters.action'), required: true
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :exclude_follows, wrapper: :with_label, kmyblue: true, label: t('simple_form.labels.filters.options.exclude_follows')
|
= f.input :exclude_follows, wrapper: :with_label, kmyblue: true, label: t('simple_form.labels.filters.options.exclude_follows')
|
||||||
= f.input :exclude_localusers, wrapper: :with_label, kmyblue: true, label: t('simple_form.labels.filters.options.exclude_localusers')
|
= f.input :exclude_localusers, wrapper: :with_label, kmyblue: true, label: t('simple_form.labels.filters.options.exclude_localusers')
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= f.input :with_quote, wrapper: :with_label, kmyblue: true, label: t('simple_form.labels.filters.options.with_quote')
|
||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
- unless f.object.statuses.empty?
|
- unless f.object.statuses.empty?
|
||||||
|
|
|
@ -46,6 +46,11 @@
|
||||||
.fields-group
|
.fields-group
|
||||||
= ff.input :'web.bookmark_category_needed', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_bookmark_category_needed'), hint: I18n.t('simple_form.hints.defaults.setting_bookmark_category_needed')
|
= ff.input :'web.bookmark_category_needed', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_bookmark_category_needed'), hint: I18n.t('simple_form.hints.defaults.setting_bookmark_category_needed')
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= ff.input :'web.show_quote_in_home', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_show_quote_in_home'), hint: false
|
||||||
|
= ff.input :'web.show_quote_in_public', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_show_quote_in_public'), hint: false
|
||||||
|
= ff.input :'web.hide_blocking_quote', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_hide_blocking_quote'), hint: false
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= ff.input :'web.simple_timeline_menu', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_simple_timeline_menu')
|
= ff.input :'web.simple_timeline_menu', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_simple_timeline_menu')
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,6 @@
|
||||||
.fields-group
|
.fields-group
|
||||||
= ff.input :lock_follow_from_bot, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_lock_follow_from_bot')
|
= ff.input :lock_follow_from_bot, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_lock_follow_from_bot')
|
||||||
|
|
||||||
.fields-group
|
|
||||||
= ff.input :single_ref_to_quote, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_single_ref_to_quote'), hint: I18n.t('simple_form.hints.defaults.setting_single_ref_to_quote')
|
|
||||||
|
|
||||||
%h4= t 'preferences.posting_defaults'
|
%h4= t 'preferences.posting_defaults'
|
||||||
|
|
||||||
.fields-row
|
.fields-row
|
||||||
|
|
|
@ -21,6 +21,9 @@
|
||||||
.fields-group
|
.fields-group
|
||||||
= ff.input :link_preview, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_link_preview'), hint: I18n.t('simple_form.hints.defaults.setting_link_preview')
|
= ff.input :link_preview, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_link_preview'), hint: I18n.t('simple_form.hints.defaults.setting_link_preview')
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= ff.input :allow_quote, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_allow_quote'), hint: false
|
||||||
|
|
||||||
%h4= t 'privacy_extra.stop_deliver'
|
%h4= t 'privacy_extra.stop_deliver'
|
||||||
|
|
||||||
%p.lead= t('privacy_extra.stop_deliver_hint_html')
|
%p.lead= t('privacy_extra.stop_deliver_hint_html')
|
||||||
|
|
|
@ -5,8 +5,8 @@ class ProcessReferencesWorker
|
||||||
|
|
||||||
sidekiq_options queue: 'pull', retry: 3
|
sidekiq_options queue: 'pull', retry: 3
|
||||||
|
|
||||||
def perform(status_id, ids, urls, no_fetch_urls = nil)
|
def perform(status_id, ids, urls, no_fetch_urls = nil, quote_urls = nil)
|
||||||
ProcessReferencesService.new.call(Status.find(status_id), ids || [], urls: urls || [], no_fetch_urls: no_fetch_urls)
|
ProcessReferencesService.new.call(Status.find(status_id), ids || [], urls: urls || [], no_fetch_urls: no_fetch_urls, quote_urls: quote_urls || [])
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
|
@ -87,6 +87,7 @@ en:
|
||||||
filters:
|
filters:
|
||||||
action: Chose which action to perform when a post matches the filter
|
action: Chose which action to perform when a post matches the filter
|
||||||
actions:
|
actions:
|
||||||
|
half_warn: Hide the filtered content (exclude account info) behind a warning mentioning the filter's title
|
||||||
hide: Completely hide the filtered content, behaving as if it did not exist
|
hide: Completely hide the filtered content, behaving as if it did not exist
|
||||||
warn: Hide the filtered content behind a warning mentioning the filter's title
|
warn: Hide the filtered content behind a warning mentioning the filter's title
|
||||||
form_admin_settings:
|
form_admin_settings:
|
||||||
|
@ -226,6 +227,7 @@ en:
|
||||||
phrase: Keyword or phrase
|
phrase: Keyword or phrase
|
||||||
setting_advanced_layout: Enable advanced web interface
|
setting_advanced_layout: Enable advanced web interface
|
||||||
setting_aggregate_reblogs: Group boosts in timelines
|
setting_aggregate_reblogs: Group boosts in timelines
|
||||||
|
setting_allow_quote: Allow quote your posts
|
||||||
setting_always_send_emails: Always send e-mail notifications
|
setting_always_send_emails: Always send e-mail notifications
|
||||||
setting_auto_play_gif: Auto-play animated GIFs
|
setting_auto_play_gif: Auto-play animated GIFs
|
||||||
setting_bio_markdown: Enable profile markdown
|
setting_bio_markdown: Enable profile markdown
|
||||||
|
@ -259,6 +261,7 @@ en:
|
||||||
mutuals_only: Mutuals only
|
mutuals_only: Mutuals only
|
||||||
outside_only: Followings or followers only
|
outside_only: Followings or followers only
|
||||||
setting_expand_spoilers: Always expand posts marked with content warnings
|
setting_expand_spoilers: Always expand posts marked with content warnings
|
||||||
|
setting_hide_blocking_quote: Hide posts which have a quote written by the user you are blocking
|
||||||
setting_hide_followers_count: Hide followers count
|
setting_hide_followers_count: Hide followers count
|
||||||
setting_hide_following_count: Hide following count
|
setting_hide_following_count: Hide following count
|
||||||
setting_hide_network: Hide your social graph
|
setting_hide_network: Hide your social graph
|
||||||
|
@ -274,6 +277,8 @@ en:
|
||||||
setting_send_without_domain_blocks: Send your post to all server with administrator set as rejecting-post-server for protect you [DEPRECATED]
|
setting_send_without_domain_blocks: Send your post to all server with administrator set as rejecting-post-server for protect you [DEPRECATED]
|
||||||
setting_show_application: Disclose application used to send posts
|
setting_show_application: Disclose application used to send posts
|
||||||
setting_show_emoji_reaction_on_timeline: Show all stamps on timeline
|
setting_show_emoji_reaction_on_timeline: Show all stamps on timeline
|
||||||
|
setting_show_quote_in_home: Show quotes in home, list or antenna timelines
|
||||||
|
setting_show_quote_in_public: Show quotes in public timelines
|
||||||
setting_simple_timeline_menu: Reduce post menu on timeline
|
setting_simple_timeline_menu: Reduce post menu on timeline
|
||||||
setting_single_ref_to_quote: Deliver single reference to other server as quote
|
setting_single_ref_to_quote: Deliver single reference to other server as quote
|
||||||
setting_stay_privacy: Not change privacy after post
|
setting_stay_privacy: Not change privacy after post
|
||||||
|
@ -308,11 +313,13 @@ en:
|
||||||
name: Hashtag
|
name: Hashtag
|
||||||
filters:
|
filters:
|
||||||
actions:
|
actions:
|
||||||
|
half_warn: Half hide with a warning
|
||||||
hide: Hide completely
|
hide: Hide completely
|
||||||
warn: Hide with a warning
|
warn: Hide with a warning
|
||||||
options:
|
options:
|
||||||
exclude_follows: Exclude following users
|
exclude_follows: Exclude following users
|
||||||
exclude_localusers: Exclude local users
|
exclude_localusers: Exclude local users
|
||||||
|
with_quote: Also check quote or references
|
||||||
form_admin_settings:
|
form_admin_settings:
|
||||||
activity_api_enabled: Publish aggregate statistics about user activity in the API
|
activity_api_enabled: Publish aggregate statistics about user activity in the API
|
||||||
backups_retention_period: User archive retention period
|
backups_retention_period: User archive retention period
|
||||||
|
|
|
@ -100,6 +100,7 @@ ja:
|
||||||
filters:
|
filters:
|
||||||
action: 投稿がフィルタに一致したときに実行するアクションを選択
|
action: 投稿がフィルタに一致したときに実行するアクションを選択
|
||||||
actions:
|
actions:
|
||||||
|
half_warn: フィルターに一致した投稿の本文のみを非表示にし、フィルターのタイトルを含む警告を表示します
|
||||||
hide: フィルタに一致した投稿を完全に非表示にします
|
hide: フィルタに一致した投稿を完全に非表示にします
|
||||||
warn: フィルタに一致した投稿を非表示にし、フィルタのタイトルを含む警告を表示します
|
warn: フィルタに一致した投稿を非表示にし、フィルタのタイトルを含む警告を表示します
|
||||||
form_admin_settings:
|
form_admin_settings:
|
||||||
|
@ -239,6 +240,7 @@ ja:
|
||||||
phrase: キーワードまたはフレーズ
|
phrase: キーワードまたはフレーズ
|
||||||
setting_advanced_layout: 上級者向けUIを有効にする
|
setting_advanced_layout: 上級者向けUIを有効にする
|
||||||
setting_aggregate_reblogs: ブーストをまとめる
|
setting_aggregate_reblogs: ブーストをまとめる
|
||||||
|
setting_allow_quote: 引用を許可する
|
||||||
setting_always_send_emails: 常にメール通知を送信する
|
setting_always_send_emails: 常にメール通知を送信する
|
||||||
setting_auto_play_gif: アニメーションGIFを自動再生する
|
setting_auto_play_gif: アニメーションGIFを自動再生する
|
||||||
setting_bio_markdown: プロフィールのMarkdownを有効にする
|
setting_bio_markdown: プロフィールのMarkdownを有効にする
|
||||||
|
@ -273,6 +275,7 @@ ja:
|
||||||
setting_emoji_reaction_streaming_notify_impl2: Nyastodon, Catstodon, glitch-soc互換のスタンプ機能を有効にする
|
setting_emoji_reaction_streaming_notify_impl2: Nyastodon, Catstodon, glitch-soc互換のスタンプ機能を有効にする
|
||||||
setting_enable_emoji_reaction: スタンプ機能を使用する
|
setting_enable_emoji_reaction: スタンプ機能を使用する
|
||||||
setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する
|
setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する
|
||||||
|
setting_hide_blocking_quote: ブロックしたユーザーの投稿を引用した投稿を隠す
|
||||||
setting_hide_followers_count: フォロワー数を隠す
|
setting_hide_followers_count: フォロワー数を隠す
|
||||||
setting_hide_following_count: フォロー数を隠す
|
setting_hide_following_count: フォロー数を隠す
|
||||||
setting_hide_network: 繋がりを隠す
|
setting_hide_network: 繋がりを隠す
|
||||||
|
@ -280,6 +283,8 @@ ja:
|
||||||
setting_hide_statuses_count: 投稿数を隠す
|
setting_hide_statuses_count: 投稿数を隠す
|
||||||
setting_link_preview: リンクのプレビューを生成する
|
setting_link_preview: リンクのプレビューを生成する
|
||||||
setting_lock_follow_from_bot: botからのフォローを承認制にする
|
setting_lock_follow_from_bot: botからのフォローを承認制にする
|
||||||
|
setting_show_quote_in_home: ホーム・リスト・アンテナなどで引用を表示する
|
||||||
|
setting_show_quote_in_public: 公開タイムライン(ローカル・連合)で引用を表示する
|
||||||
setting_stay_privacy: 投稿時に公開範囲を保存する
|
setting_stay_privacy: 投稿時に公開範囲を保存する
|
||||||
setting_noai: 自分のコンテンツのAI学習利用に対して不快感を表明する
|
setting_noai: 自分のコンテンツのAI学習利用に対して不快感を表明する
|
||||||
setting_public_post_to_unlisted: サードパーティから公開範囲「公開」で投稿した場合、「ローカル公開」に変更する
|
setting_public_post_to_unlisted: サードパーティから公開範囲「公開」で投稿した場合、「ローカル公開」に変更する
|
||||||
|
@ -323,11 +328,13 @@ ja:
|
||||||
name: ハッシュタグ
|
name: ハッシュタグ
|
||||||
filters:
|
filters:
|
||||||
actions:
|
actions:
|
||||||
|
half_warn: アカウント名だけを出し、本文は警告で隠す
|
||||||
hide: 完全に隠す
|
hide: 完全に隠す
|
||||||
warn: 警告付きで隠す
|
warn: 警告付きで隠す
|
||||||
options:
|
options:
|
||||||
exclude_follows: フォロー中のユーザーをフィルターの対象にしない
|
exclude_follows: フォロー中のユーザーをフィルターの対象にしない
|
||||||
exclude_localusers: ローカルユーザーをフィルターの対象にしない
|
exclude_localusers: ローカルユーザーをフィルターの対象にしない
|
||||||
|
with_quote: 引用・参照の内容をフィルターの対象に含める
|
||||||
form_admin_settings:
|
form_admin_settings:
|
||||||
activity_api_enabled: APIでユーザーアクティビティに関する集計統計を公開する
|
activity_api_enabled: APIでユーザーアクティビティに関する集計統計を公開する
|
||||||
backups_retention_period: ユーザーアーカイブの保持期間
|
backups_retention_period: ユーザーアーカイブの保持期間
|
||||||
|
|
24
db/migrate/20230930233930_add_quote_to_status_references.rb
Normal file
24
db/migrate/20230930233930_add_quote_to_status_references.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||||
|
|
||||||
|
class AddQuoteToStatusReferences < ActiveRecord::Migration[7.0]
|
||||||
|
include Mastodon::MigrationHelpers
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
class StatusReference < ApplicationRecord; end
|
||||||
|
|
||||||
|
def up
|
||||||
|
safety_assured do
|
||||||
|
add_column_with_default :status_references, :quote, :boolean, default: false, allow_null: false
|
||||||
|
StatusReference.where(attribute_type: 'QT').update_all(quote: true) # rubocop:disable Rails/SkipsModelValidations
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
safety_assured do
|
||||||
|
remove_column :status_references, :quote
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
32
db/migrate/20231001031337_add_quote_to_statuses.rb
Normal file
32
db/migrate/20231001031337_add_quote_to_statuses.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||||
|
|
||||||
|
class AddQuoteToStatuses < ActiveRecord::Migration[7.0]
|
||||||
|
include Mastodon::MigrationHelpers
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
class StatusReference < ApplicationRecord
|
||||||
|
belongs_to :status
|
||||||
|
belongs_to :target_status, class_name: 'Status'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
safety_assured do
|
||||||
|
add_column_with_default :statuses, :quote_of_id, :bigint, default: nil, allow_null: true
|
||||||
|
|
||||||
|
StatusReference.transaction do
|
||||||
|
StatusReference.where(quote: true).includes(:status).each do |ref|
|
||||||
|
ref.status.update(quote_of_id: ref.target_status_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
safety_assured do
|
||||||
|
remove_column :statuses, :quote_of_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||||
|
|
||||||
|
class AddWithQuoteToCustomFilters < ActiveRecord::Migration[7.0]
|
||||||
|
include Mastodon::MigrationHelpers
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def change
|
||||||
|
safety_assured do
|
||||||
|
add_column_with_default :custom_filters, :with_quote, :boolean, default: true, allow_null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2023_09_23_103430) do
|
ActiveRecord::Schema[7.0].define(version: 2023_10_01_050733) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
|
@ -536,6 +536,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_23_103430) do
|
||||||
t.integer "action", default: 0, null: false
|
t.integer "action", default: 0, null: false
|
||||||
t.boolean "exclude_follows", default: false, null: false
|
t.boolean "exclude_follows", default: false, null: false
|
||||||
t.boolean "exclude_localusers", default: false, null: false
|
t.boolean "exclude_localusers", default: false, null: false
|
||||||
|
t.boolean "with_quote", default: true, null: false
|
||||||
t.index ["account_id"], name: "index_custom_filters_on_account_id"
|
t.index ["account_id"], name: "index_custom_filters_on_account_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1143,6 +1144,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_23_103430) do
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.string "attribute_type"
|
t.string "attribute_type"
|
||||||
|
t.boolean "quote", default: false, null: false
|
||||||
t.index ["status_id"], name: "index_status_references_on_status_id"
|
t.index ["status_id"], name: "index_status_references_on_status_id"
|
||||||
t.index ["target_status_id"], name: "index_status_references_on_target_status_id"
|
t.index ["target_status_id"], name: "index_status_references_on_target_status_id"
|
||||||
end
|
end
|
||||||
|
@ -1199,6 +1201,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_23_103430) do
|
||||||
t.integer "searchability"
|
t.integer "searchability"
|
||||||
t.boolean "markdown", default: false
|
t.boolean "markdown", default: false
|
||||||
t.integer "limited_scope"
|
t.integer "limited_scope"
|
||||||
|
t.bigint "quote_of_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)"
|
||||||
|
|
8
spec/fabricators/status_reference_fabricator.rb
Normal file
8
spec/fabricators/status_reference_fabricator.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:status_reference) do
|
||||||
|
status { Fabricate.build(:status) }
|
||||||
|
target_status { Fabricate.build(:status) }
|
||||||
|
attribute_type 'BT'
|
||||||
|
quote false
|
||||||
|
end
|
|
@ -1089,6 +1089,97 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with references' do
|
||||||
|
let(:recipient) { Fabricate(:account) }
|
||||||
|
let!(:target_status) { Fabricate(:status, account: Fabricate(:account, domain: nil)) }
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
references: {
|
||||||
|
id: 'target_status',
|
||||||
|
type: 'Collection',
|
||||||
|
first: {
|
||||||
|
type: 'CollectionPage',
|
||||||
|
next: nil,
|
||||||
|
partOf: 'target_status',
|
||||||
|
items: [
|
||||||
|
ActivityPub::TagManager.instance.uri_for(target_status),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.quote).to be_nil
|
||||||
|
expect(status.references.pluck(:id)).to eq [target_status.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with quote' do
|
||||||
|
let(:recipient) { Fabricate(:account) }
|
||||||
|
let!(:target_status) { Fabricate(:status, account: Fabricate(:account, domain: nil)) }
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
quote: ActivityPub::TagManager.instance.uri_for(target_status),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.references.pluck(:id)).to eq [target_status.id]
|
||||||
|
expect(status.quote).to_not be_nil
|
||||||
|
expect(status.quote.id).to eq target_status.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with references and quote' do
|
||||||
|
let(:recipient) { Fabricate(:account) }
|
||||||
|
let!(:target_status) { Fabricate(:status, account: Fabricate(:account, domain: nil)) }
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
quote: ActivityPub::TagManager.instance.uri_for(target_status),
|
||||||
|
references: {
|
||||||
|
id: 'target_status',
|
||||||
|
type: 'Collection',
|
||||||
|
first: {
|
||||||
|
type: 'CollectionPage',
|
||||||
|
next: nil,
|
||||||
|
partOf: 'target_status',
|
||||||
|
items: [
|
||||||
|
ActivityPub::TagManager.instance.uri_for(target_status),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.references.pluck(:id)).to eq [target_status.id]
|
||||||
|
expect(status.quote).to_not be_nil
|
||||||
|
expect(status.quote.id).to eq target_status.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with language' do
|
context 'with language' do
|
||||||
let(:to) { 'https://www.w3.org/ns/activitystreams#Public' }
|
let(:to) { 'https://www.w3.org/ns/activitystreams#Public' }
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
|
@ -1274,6 +1365,53 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when sender quotes to local status' do
|
||||||
|
subject { described_class.new(json, sender, delivery: true) }
|
||||||
|
|
||||||
|
let!(:local_status) { Fabricate(:status) }
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
quote: ActivityPub::TagManager.instance.uri_for(local_status),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.text).to eq 'Lorem ipsum'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sender quotes to non-local status' do
|
||||||
|
subject { described_class.new(json, sender, delivery: true) }
|
||||||
|
|
||||||
|
let!(:remote_status) { Fabricate(:status, uri: 'https://foo.bar/among', account: Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar/account')) }
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
quote: ActivityPub::TagManager.instance.uri_for(remote_status),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
expect(sender.statuses.count).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when sender targets a local user' do
|
context 'when sender targets a local user' do
|
||||||
subject { described_class.new(json, sender, delivery: true) }
|
subject { described_class.new(json, sender, delivery: true) }
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,11 @@ describe StatusReachFinder do
|
||||||
subject { described_class.new(status) }
|
subject { described_class.new(status) }
|
||||||
|
|
||||||
let(:parent_status) { nil }
|
let(:parent_status) { nil }
|
||||||
|
let(:quoted_status) { nil }
|
||||||
let(:visibility) { :public }
|
let(:visibility) { :public }
|
||||||
let(:searchability) { :public }
|
let(:searchability) { :public }
|
||||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||||
let(:status) { Fabricate(:status, account: alice, thread: parent_status, visibility: visibility, searchability: searchability) }
|
let(:status) { Fabricate(:status, account: alice, thread: parent_status, quote_of_id: quoted_status&.id, visibility: visibility, searchability: searchability) }
|
||||||
|
|
||||||
context 'with a simple case' do
|
context 'with a simple case' do
|
||||||
let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') }
|
let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') }
|
||||||
|
@ -165,6 +166,15 @@ describe StatusReachFinder do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when it is a quote to a remote account' do
|
||||||
|
let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') }
|
||||||
|
let(:quoted_status) { Fabricate(:status, account: bob) }
|
||||||
|
|
||||||
|
it 'includes the inbox of the quoted-to account' do
|
||||||
|
expect(subject.inboxes).to include 'https://foo.bar/inbox'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with extended domain block' do
|
context 'with extended domain block' do
|
||||||
|
|
|
@ -217,6 +217,39 @@ RSpec.describe Status do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#quote' do
|
||||||
|
let(:target_status) { Fabricate(:status) }
|
||||||
|
let(:quote) { true }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:status_reference, status: subject, target_status: target_status, quote: quote)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when quoting single' do
|
||||||
|
it 'get quote' do
|
||||||
|
expect(subject.quote).to_not be_nil
|
||||||
|
expect(subject.quote.id).to eq target_status.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when multiple quotes' do
|
||||||
|
it 'get quote' do
|
||||||
|
target2 = Fabricate(:status)
|
||||||
|
Fabricate(:status_reference, status: subject, quote: quote)
|
||||||
|
expect(subject.quote).to_not be_nil
|
||||||
|
expect([target_status.id, target2.id].include?(subject.quote.id)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when no quote but reference' do
|
||||||
|
let(:quote) { false }
|
||||||
|
|
||||||
|
it 'get quote' do
|
||||||
|
expect(subject.quote).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#content' do
|
describe '#content' do
|
||||||
it 'returns the text of the status if it is not a reblog' do
|
it 'returns the text of the status if it is not a reblog' do
|
||||||
expect(subject.content).to eql subject.text
|
expect(subject.content).to eql subject.text
|
||||||
|
@ -324,6 +357,38 @@ RSpec.describe Status do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.blocks_map' do
|
||||||
|
subject { described_class.blocks_map([status.account.id], account) }
|
||||||
|
|
||||||
|
let(:status) { Fabricate(:status) }
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
|
||||||
|
it 'returns a hash' do
|
||||||
|
expect(subject).to be_a Hash
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'contains true value' do
|
||||||
|
account.block!(status.account)
|
||||||
|
expect(subject[status.account.id]).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.domain_blocks_map' do
|
||||||
|
subject { described_class.domain_blocks_map([status.account.domain], account) }
|
||||||
|
|
||||||
|
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar/status')) }
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
|
||||||
|
it 'returns a hash' do
|
||||||
|
expect(subject).to be_a Hash
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'contains true value' do
|
||||||
|
account.block_domain!(status.account.domain)
|
||||||
|
expect(subject[status.account.domain]).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '.favourites_map' do
|
describe '.favourites_map' do
|
||||||
subject { described_class.favourites_map([status], account) }
|
subject { described_class.favourites_map([status], account) }
|
||||||
|
|
||||||
|
|
|
@ -167,6 +167,48 @@ RSpec.describe StatusPolicy, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with the permission of emoji_reaction?' do
|
||||||
|
permissions :emoji_reaction? do
|
||||||
|
it 'grants access when viewer is not blocked' do
|
||||||
|
follow = Fabricate(:follow)
|
||||||
|
status.account = follow.target_account
|
||||||
|
|
||||||
|
expect(subject).to permit(follow.account, status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'denies when viewer is blocked' do
|
||||||
|
block = Fabricate(:block)
|
||||||
|
status.account = block.target_account
|
||||||
|
|
||||||
|
expect(subject).to_not permit(block.account, status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the permission of quote?' do
|
||||||
|
permissions :quote? do
|
||||||
|
it 'grants access when viewer is not blocked' do
|
||||||
|
follow = Fabricate(:follow)
|
||||||
|
status.account = follow.target_account
|
||||||
|
|
||||||
|
expect(subject).to permit(follow.account, status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'denies when viewer is blocked' do
|
||||||
|
block = Fabricate(:block)
|
||||||
|
status.account = block.target_account
|
||||||
|
|
||||||
|
expect(subject).to_not permit(block.account, status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'denies when private visibility' do
|
||||||
|
status.visibility = :private
|
||||||
|
|
||||||
|
expect(subject).to_not permit(Fabricate(:account), status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with the permission of update?' do
|
context 'with the permission of update?' do
|
||||||
permissions :update? do
|
permissions :update? do
|
||||||
it 'grants access if owner' do
|
it 'grants access if owner' do
|
||||||
|
|
|
@ -15,12 +15,10 @@ describe ActivityPub::NoteSerializer do
|
||||||
let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) }
|
let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) }
|
||||||
let!(:referred) { nil }
|
let!(:referred) { nil }
|
||||||
let!(:referred2) { nil }
|
let!(:referred2) { nil }
|
||||||
let(:convert_to_quote) { false }
|
|
||||||
|
|
||||||
before(:each) do
|
before(:each) do
|
||||||
parent.references << referred if referred.present?
|
parent.references << referred if referred.present?
|
||||||
parent.references << referred2 if referred2.present?
|
parent.references << referred2 if referred2.present?
|
||||||
account.user&.settings&.[]=('single_ref_to_quote', true) if convert_to_quote
|
|
||||||
@serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: described_class, adapter: ActivityPub::Adapter)
|
@serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: described_class, adapter: ActivityPub::Adapter)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -64,28 +62,4 @@ describe ActivityPub::NoteSerializer do
|
||||||
expect(subject['references']['first']['items']).to include referred.uri
|
expect(subject['references']['first']['items']).to include referred.uri
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when has quote and convert setting' do
|
|
||||||
let(:referred) { Fabricate(:status) }
|
|
||||||
let(:convert_to_quote) { true }
|
|
||||||
|
|
||||||
it 'has as quote' do
|
|
||||||
expect(subject['quoteUri']).to_not be_nil
|
|
||||||
expect(subject['quoteUri']).to eq referred.uri
|
|
||||||
expect(subject['_misskey_quote']).to eq referred.uri
|
|
||||||
expect(subject['references']['first']['items']).to include referred.uri
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when has multiple references and convert setting' do
|
|
||||||
let(:referred) { Fabricate(:status) }
|
|
||||||
let(:referred2) { Fabricate(:status) }
|
|
||||||
let(:convert_to_quote) { true }
|
|
||||||
|
|
||||||
it 'has as quote' do
|
|
||||||
expect(subject['quoteUri']).to be_nil
|
|
||||||
expect(subject['references']['first']['items']).to include referred.uri
|
|
||||||
expect(subject['references']['first']['items']).to include referred2.uri
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,7 @@ RSpec.describe ProcessReferencesService, type: :service do
|
||||||
let(:status) { Fabricate(:status, account: account, text: text, visibility: visibility) }
|
let(:status) { Fabricate(:status, account: account, text: text, visibility: visibility) }
|
||||||
let(:target_status) { Fabricate(:status, account: Fabricate(:user).account, visibility: target_status_visibility) }
|
let(:target_status) { Fabricate(:status, account: Fabricate(:user).account, visibility: target_status_visibility) }
|
||||||
let(:target_status_uri) { ActivityPub::TagManager.instance.uri_for(target_status) }
|
let(:target_status_uri) { ActivityPub::TagManager.instance.uri_for(target_status) }
|
||||||
|
let(:quote_urls) { nil }
|
||||||
|
|
||||||
def notify?(target_status_id = nil)
|
def notify?(target_status_id = nil)
|
||||||
target_status_id ||= target_status.id
|
target_status_id ||= target_status.id
|
||||||
|
@ -18,7 +19,7 @@ RSpec.describe ProcessReferencesService, type: :service do
|
||||||
|
|
||||||
describe 'posting new status' do
|
describe 'posting new status' do
|
||||||
subject do
|
subject do
|
||||||
described_class.new.call(status, reference_parameters, urls: urls, fetch_remote: fetch_remote)
|
described_class.new.call(status, reference_parameters, urls: urls, fetch_remote: fetch_remote, quote_urls: quote_urls)
|
||||||
status.reference_objects.pluck(:target_status_id, :attribute_type)
|
status.reference_objects.pluck(:target_status_id, :attribute_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -35,6 +36,10 @@ RSpec.describe ProcessReferencesService, type: :service do
|
||||||
expect(subject.pluck(1)).to include 'RT'
|
expect(subject.pluck(1)).to include 'RT'
|
||||||
expect(notify?).to be true
|
expect(notify?).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'not quote' do
|
||||||
|
expect(status.quote).to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when multiple references' do
|
context 'when multiple references' do
|
||||||
|
@ -86,6 +91,48 @@ RSpec.describe ProcessReferencesService, type: :service do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with quote as parameter only' do
|
||||||
|
let(:text) { 'Hello' }
|
||||||
|
let(:quote_urls) { [ActivityPub::TagManager.instance.uri_for(target_status)] }
|
||||||
|
|
||||||
|
it 'post status' do
|
||||||
|
expect(subject.size).to eq 1
|
||||||
|
expect(subject.pluck(0)).to include target_status.id
|
||||||
|
expect(subject.pluck(1)).to include 'QT'
|
||||||
|
expect(status.quote).to_not be_nil
|
||||||
|
expect(status.quote.id).to eq target_status.id
|
||||||
|
expect(notify?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with quote as parameter and embed' do
|
||||||
|
let(:text) { "Hello QT #{target_status_uri}" }
|
||||||
|
let(:quote_urls) { [ActivityPub::TagManager.instance.uri_for(target_status)] }
|
||||||
|
|
||||||
|
it 'post status' do
|
||||||
|
expect(subject.size).to eq 1
|
||||||
|
expect(subject.pluck(0)).to include target_status.id
|
||||||
|
expect(subject.pluck(1)).to include 'QT'
|
||||||
|
expect(status.quote).to_not be_nil
|
||||||
|
expect(status.quote.id).to eq target_status.id
|
||||||
|
expect(notify?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with quote as parameter but embed is not quote' do
|
||||||
|
let(:text) { "Hello RE #{target_status_uri}" }
|
||||||
|
let(:quote_urls) { [ActivityPub::TagManager.instance.uri_for(target_status)] }
|
||||||
|
|
||||||
|
it 'post status' do
|
||||||
|
expect(subject.size).to eq 1
|
||||||
|
expect(subject.pluck(0)).to include target_status.id
|
||||||
|
expect(subject.pluck(1)).to include 'QT'
|
||||||
|
expect(status.quote).to_not be_nil
|
||||||
|
expect(status.quote.id).to eq target_status.id
|
||||||
|
expect(notify?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with quote and reference' do
|
context 'with quote and reference' do
|
||||||
let(:target_status2) { Fabricate(:status) }
|
let(:target_status2) { Fabricate(:status) }
|
||||||
let(:target_status2_uri) { ActivityPub::TagManager.instance.uri_for(target_status2) }
|
let(:target_status2_uri) { ActivityPub::TagManager.instance.uri_for(target_status2) }
|
||||||
|
@ -240,6 +287,17 @@ RSpec.describe ProcessReferencesService, type: :service do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when remove quote' do
|
||||||
|
let(:text) { "QT #{target_status_uri}" }
|
||||||
|
let(:new_text) { 'Hello' }
|
||||||
|
|
||||||
|
it 'post status' do
|
||||||
|
expect(subject.size).to eq 0
|
||||||
|
expect(status.quote).to be_nil
|
||||||
|
expect(notify?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when change reference' do
|
context 'when change reference' do
|
||||||
let(:text) { "BT #{target_status_uri}" }
|
let(:text) { "BT #{target_status_uri}" }
|
||||||
let(:new_text) { "BT #{target_status2_uri}" }
|
let(:new_text) { "BT #{target_status2_uri}" }
|
||||||
|
@ -250,5 +308,43 @@ RSpec.describe ProcessReferencesService, type: :service do
|
||||||
expect(notify?(target_status2.id)).to be true
|
expect(notify?(target_status2.id)).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when change quote' do
|
||||||
|
let(:text) { "QT #{target_status_uri}" }
|
||||||
|
let(:new_text) { "QT #{target_status2_uri}" }
|
||||||
|
|
||||||
|
it 'post status' do
|
||||||
|
expect(subject.size).to eq 1
|
||||||
|
expect(subject).to include target_status2.id
|
||||||
|
expect(status.quote).to_not be_nil
|
||||||
|
expect(status.quote.id).to eq target_status2.id
|
||||||
|
expect(notify?(target_status2.id)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when change quote to reference', pending: 'Will fix later' do
|
||||||
|
let(:text) { "QT #{target_status_uri}" }
|
||||||
|
let(:new_text) { "RT #{target_status_uri}" }
|
||||||
|
|
||||||
|
it 'post status' do
|
||||||
|
expect(subject.size).to eq 1
|
||||||
|
expect(subject).to include target_status.id
|
||||||
|
expect(status.quote).to be_nil
|
||||||
|
expect(notify?(target_status.id)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when change reference to quote', pending: 'Will fix later' do
|
||||||
|
let(:text) { "RT #{target_status_uri}" }
|
||||||
|
let(:new_text) { "QT #{target_status_uri}" }
|
||||||
|
|
||||||
|
it 'post status' do
|
||||||
|
expect(subject.size).to eq 1
|
||||||
|
expect(subject).to include target_status.id
|
||||||
|
expect(status.quote).to_not be_nil
|
||||||
|
expect(status.quote.id).to eq target_status.id
|
||||||
|
expect(notify?(target_status.id)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -801,8 +801,10 @@ const startServer = async () => {
|
||||||
|
|
||||||
// reference_texts property is not working if ProcessReferencesWorker is
|
// reference_texts property is not working if ProcessReferencesWorker is
|
||||||
// used on PostStatusService and so on. (Asynchronous processing)
|
// used on PostStatusService and so on. (Asynchronous processing)
|
||||||
const reference_texts = payload.reference_texts || [];
|
const reference_texts = payload?.reference_texts || [];
|
||||||
delete payload.reference_texts;
|
if (payload && typeof payload.reference_texts !== 'undefined') {
|
||||||
|
delete payload.reference_texts;
|
||||||
|
}
|
||||||
|
|
||||||
// Streaming only needs to apply filtering to some channels and only to
|
// Streaming only needs to apply filtering to some channels and only to
|
||||||
// some events. This is because majority of the filtering happens on the
|
// some events. This is because majority of the filtering happens on the
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue