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:
KMY(雪あすか) 2023-10-02 11:12:51 +09:00 committed by GitHub
parent 3c649aa74d
commit 44b739a39a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 1362 additions and 120 deletions

View file

@ -13,12 +13,13 @@ import AttachmentList from 'mastodon/components/attachment_list';
import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import CompactedStatusContainer from '../containers/compacted_status_container'
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, enableEmojiReaction, showEmojiReactionOnTimeline } from '../initial_state';
import { displayMedia, enableEmojiReaction, showEmojiReactionOnTimeline, showQuoteInHome, showQuoteInPublic } from '../initial_state';
import { Avatar } from './avatar';
import { AvatarOverlay } from './avatar_overlay';
@ -87,6 +88,7 @@ class Status extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
contextType: PropTypes.string,
previousId: PropTypes.string,
nextInReplyToId: PropTypes.string,
rootId: PropTypes.string,
@ -357,15 +359,17 @@ class Status extends ImmutablePureComponent {
};
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;
const contextType = (this.props.contextType || '').split(':')[0];
if (status === null) {
return null;
}
const handlers = this.props.muted ? {} : {
const handlers = muted ? {} : {
reply: this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost,
@ -384,7 +388,7 @@ class Status extends ImmutablePureComponent {
if (hidden) {
return (
<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.get('content')}</span>
</div>
@ -412,12 +416,53 @@ class Status extends ImmutablePureComponent {
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) {
const minHandlers = this.props.muted ? {} : {
const minHandlers = muted ? {} : {
moveUp: this.handleHotkeyMoveUp,
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 (
<HotKeys handlers={minHandlers}>
<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) {
const language = status.getIn(['translation', 'language']) || status.get('language');
if (this.props.muted) {
if (muted) {
media = (
<AttachmentList
compact
@ -556,7 +601,7 @@ class Status extends ImmutablePureComponent {
</Bundle>
);
}
} else if (status.get('card') && !this.props.muted) {
} else if (status.get('card') && !muted) {
media = (
<Card
onOpenMedia={this.handleOpenMedia}
@ -568,12 +613,6 @@ class Status extends ImmutablePureComponent {
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')];
let emojiReactionsBar = null;
@ -588,20 +627,24 @@ class Status extends ImmutablePureComponent {
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 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 quote = !muted && status.get('quote_id') && (['public', 'community'].includes(contextType) ? showQuoteInPublic : showQuoteInHome) && <CompactedStatusContainer id={status.get('quote_id')} />
return (
<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}
<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 })} />}
{/* 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'>
{withQuote}
{withReference}
{withExpiration}
{withLimited}
@ -629,6 +672,8 @@ class Status extends ImmutablePureComponent {
{...statusContentProps}
/>
{(!status.get('spoiler_text') || expanded) && quote}
{(!isCardMediaWithSensitive || !status.get('hidden')) && media}
{(!status.get('spoiler_text') || expanded) && hashtagBar}