Merge pull request #132 from Y-zu-don-maintenance-org/features/quote

Features/quote
This commit is contained in:
YorimiMochida 2024-02-04 11:40:37 +09:00 committed by GitHub
commit c4712ed22c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 168 additions and 111 deletions

View File

@ -8,9 +8,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def index def index
cache_if_unauthenticated! cache_if_unauthenticated!
@statuses = load_statuses @statuses = load_statuses
accounts = @statuses.filter_map { |status| status.quote&.account }.uniq
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
accounts = Account.where(id: account_ids)
render json: @statuses, each_serializer: REST::StatusSerializer, render json: @statuses, each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(accounts, current_user&.account_id) account_relationships: AccountRelationshipsPresenter.new(accounts, current_user&.account_id)

View File

@ -6,9 +6,9 @@ class Api::V1::BookmarksController < Api::BaseController
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
@statuses = load_statuses @statuses = load_statuses
accounts = @statuses.filter_map { |status| status.quote&.account }.uniq
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
accounts = Account.where(id: account_ids)
render json: @statuses, each_serializer: REST::StatusSerializer, render json: @statuses, each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(accounts, current_user&.account_id) account_relationships: AccountRelationshipsPresenter.new(accounts, current_user&.account_id)

View File

@ -6,9 +6,9 @@ class Api::V1::FavouritesController < Api::BaseController
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
@statuses = load_statuses @statuses = load_statuses
accounts = @statuses.filter_map { |status| status.quote&.account }.uniq
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
accounts = Account.where(id: account_ids)
render json: @statuses, each_serializer: REST::StatusSerializer, render json: @statuses, each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(accounts, current_user&.account_id) account_relationships: AccountRelationshipsPresenter.new(accounts, current_user&.account_id)

View File

@ -47,10 +47,10 @@ class Api::V1::StatusesController < Api::BaseController
loaded_ancestors = cache_collection(ancestors_results, Status) loaded_ancestors = cache_collection(ancestors_results, Status)
loaded_descendants = cache_collection(descendants_results, Status) loaded_descendants = cache_collection(descendants_results, Status)
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants) @context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
statuses = [@status] + @context.ancestors + @context.descendants statuses = [@status] + @context.ancestors + @context.descendants
accounts = statuses.filter_map { |status| status.quote&.account }.uniq
account_ids = statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq account_ids = statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
accounts = Account.where(id: account_ids)
render json: @context, serializer: REST::ContextSerializer, render json: @context, serializer: REST::ContextSerializer,
relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id), relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id),

View File

@ -10,8 +10,8 @@ class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController
with_read_replica do with_read_replica do
@statuses = load_statuses @statuses = load_statuses
@relationships = StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) @relationships = StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
accounts = @statuses.filter_map { |status| status.quote&.account }.uniq
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
accounts = Account.where(id: account_ids)
@account_relationships = AccountRelationshipsPresenter.new(accounts, current_user&.account_id) @account_relationships = AccountRelationshipsPresenter.new(accounts, current_user&.account_id)
end end

View File

@ -9,8 +9,8 @@ class Api::V1::Timelines::ListController < Api::V1::Timelines::BaseController
PERMITTED_PARAMS = %i(limit).freeze PERMITTED_PARAMS = %i(limit).freeze
def show def show
accounts = @statuses.filter_map { |status| status.quote&.account }.uniq
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
accounts = Account.where(id: account_ids)
render json: @statuses, render json: @statuses,
each_serializer: REST::StatusSerializer, each_serializer: REST::StatusSerializer,

View File

@ -7,9 +7,9 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
def show def show
cache_if_unauthenticated! cache_if_unauthenticated!
@statuses = load_statuses @statuses = load_statuses
accounts = @statuses.filter_map { |status| status.quote&.account }.uniq
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
accounts = Account.where(id: account_ids)
render json: @statuses, each_serializer: REST::StatusSerializer, render json: @statuses, each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),

View File

@ -8,9 +8,9 @@ class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController
def show def show
cache_if_unauthenticated! cache_if_unauthenticated!
@statuses = load_statuses @statuses = load_statuses
accounts = @statuses.filter_map { |status| status.quote&.account }.uniq
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
accounts = Account.where(id: account_ids)
render json: @statuses, each_serializer: REST::StatusSerializer, render json: @statuses, each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),

View File

@ -25,7 +25,6 @@ export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY'; export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE'; export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_MENTION = 'COMPOSE_MENTION';
export const COMPOSE_RESET = 'COMPOSE_RESET'; export const COMPOSE_RESET = 'COMPOSE_RESET';
@ -137,13 +136,7 @@ export function quoteCompose(status, routerHistory) {
ensureComposeIsVisible(getState, routerHistory); ensureComposeIsVisible(getState, routerHistory);
}; };
}; }
export function cancelQuoteCompose() {
return {
type: COMPOSE_QUOTE_CANCEL,
};
};
export function resetCompose() { export function resetCompose() {
return { return {

View File

@ -361,7 +361,7 @@ export function hideQuote(ids) {
type: QUOTE_HIDE, type: QUOTE_HIDE,
ids, ids,
}; };
}; }
export function revealQuote(ids) { export function revealQuote(ids) {
if (!Array.isArray(ids)) { if (!Array.isArray(ids)) {
@ -372,4 +372,4 @@ export function revealQuote(ids) {
type: QUOTE_REVEAL, type: QUOTE_REVEAL,
ids, ids,
}; };
}; }

View File

@ -134,7 +134,7 @@ export const quote = (status, muted, quoteMuted, handleQuoteClick, handleExpande
return ( return (
<div> <div>
<div className='status__info'> <div className='status__info'>
{identity(quoteStatus, null, null, true)} {identity(quoteStatus, null, true)}
</div> </div>
<StatusContent status={quoteStatus} onClick={handleQuoteClick} expanded={!status.get('quote_hidden')} onExpandedToggle={handleExpandedQuoteToggle} quote /> <StatusContent status={quoteStatus} onClick={handleQuoteClick} expanded={!status.get('quote_hidden')} onExpandedToggle={handleExpandedQuoteToggle} quote />
{media(quoteStatus, true)} {media(quoteStatus, true)}
@ -146,7 +146,6 @@ export const quote = (status, muted, quoteMuted, handleQuoteClick, handleExpande
<div <div
className={classNames('quote-status', `status-${quoteStatus.get('visibility')}`, { muted: muted })} className={classNames('quote-status', `status-${quoteStatus.get('visibility')}`, { muted: muted })}
data-id={quoteStatus.get('id')} data-id={quoteStatus.get('id')}
dataurl={quoteStatus.get('url')}
> >
{quoteInner} {quoteInner}
</div> </div>
@ -238,7 +237,7 @@ class Status extends ImmutablePureComponent {
handleToggleQuoteMediaVisibility = () => { handleToggleQuoteMediaVisibility = () => {
this.setState({ showQuoteMedia: !this.state.showQuoteMedia }); this.setState({ showQuoteMedia: !this.state.showQuoteMedia });
} };
handleClick = e => { handleClick = e => {
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) { if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
@ -253,10 +252,6 @@ class Status extends ImmutablePureComponent {
}; };
handlePrependAccountClick = e => { handlePrependAccountClick = e => {
this.handleAccountClick(e, false);
};
handleAccountClick = (e, proper = true) => {
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) { if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
return; return;
} }
@ -266,17 +261,29 @@ class Status extends ImmutablePureComponent {
e.stopPropagation(); e.stopPropagation();
} }
this._openProfile(proper); this._openProfile(false);
}; };
handleQuoteClick = () => { handleAccountClick = (e) => {
if (!this.props) { if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
return; return;
} }
const { status } = this.props; if (e) {
this.props.history.push(`/statuses/${status.getIn(['reblog', 'quote', 'id'], status.getIn(['quote', 'id']))}`); e.preventDefault();
} e.stopPropagation();
}
const acct = e.currentTarget.getAttribute('data-acct');
this.props.history.push(`/@${acct}`);
};
handleQuoteClick = () => {
if (this.props.history) {
const status = this._properStatus();
this.props.history.push(`/@${status.getIn(['quote', 'account', 'acct'])}/${status.getIn(['quote', 'id'])}`);
}
};
handleQuoteUserClick = () =>{ handleQuoteUserClick = () =>{
if (!this.props) { if (!this.props) {
@ -301,7 +308,7 @@ class Status extends ImmutablePureComponent {
handleExpandedQuoteToggle = () => { handleExpandedQuoteToggle = () => {
this.props.onQuoteToggleHidden(this._properStatus()); this.props.onQuoteToggleHidden(this._properStatus());
} };
getAttachmentAspectRatio () { getAttachmentAspectRatio () {
const attachments = this._properStatus().get('media_attachments'); const attachments = this._properStatus().get('media_attachments');
@ -554,9 +561,10 @@ class Status extends ImmutablePureComponent {
</div> </div>
); );
} }
const media = (status, quote = false) => { const media = (status, quote = false) => {
if (pictureInPicture.get('inUse')) { if (pictureInPicture.get('inUse')) {
return <PictureInPicturePlaceholder aspectRatio={this.getAttachmentAspectRatio()} />; return <PictureInPicturePlaceholder aspectRatio={this.getAttachmentAspectRatio()} width={this.props.cachedMediaWidth} />;
} 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');
@ -655,9 +663,9 @@ class Status extends ImmutablePureComponent {
return <AvatarOverlay account={status.get('account')} friend={account} />; return <AvatarOverlay account={status.get('account')} friend={account} />;
} }
}; };
const identity = (status, account, _0, quote = false) => ( const identity = (status, account) => (
<a onClick={quote ? this.handleQuoteUserClick : this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' rel='noopener noreferrer'> <a onClick={this.handleAccountClick} data-acct={status.getIn(['account', 'acct'])} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'> <div className='status__avatar'>
{statusAvatar(status, account)} {statusAvatar(status, account)}
</div> </div>
@ -721,4 +729,4 @@ class Status extends ImmutablePureComponent {
} }
export default withOptionalRouter(injectIntl(Status)); export default connect(mapStateToProps)(withOptionalRouter(injectIntl(Status)));

View File

@ -11,6 +11,7 @@ import { connect } from 'react-redux';
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg'; import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg';
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react'; import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
import FormatQuoteIcon from '@/material-icons/400-24px/format_quote.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
@ -160,8 +161,8 @@ class StatusActionBar extends ImmutablePureComponent {
}; };
handleQuoteClick = () => { handleQuoteClick = () => {
this.props.onQuote(this.props.status, this.props.history); this.props.onQuote(this.props.status);
} };
handleBookmarkClick = () => { handleBookmarkClick = () => {
this.props.onBookmark(this.props.status); this.props.onBookmark(this.props.status);
@ -262,7 +263,7 @@ class StatusActionBar extends ImmutablePureComponent {
} else { } else {
return intl.formatMessage(messages.cannot_quote); return intl.formatMessage(messages.cannot_quote);
} }
} };
render () { render () {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
@ -406,8 +407,8 @@ class StatusActionBar extends ImmutablePureComponent {
<div className='status__action-bar'> <div className='status__action-bar'>
<IconButton className='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} /> <IconButton className='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} /> <IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} /> <IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar__button' disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='quote' iconComponent={QuoteIcon} onClick={this.handleQuoteClick} /> <IconButton className='status__action-bar__button' disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='format-quote' iconComponent={FormatQuoteIcon} onClick={this.handleQuoteClick} />
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /> <IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
{filterButton} {filterButton}

View File

@ -241,7 +241,7 @@ class StatusContent extends PureComponent {
}; };
render () { render () {
const { status, intl, statusContent, quote } = this.props; const { status, intl, quote, statusContent } = this.props;
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed'); const renderReadMore = this.props.onClick && status.get('collapsed');

View File

@ -29,6 +29,7 @@ import { CharacterCounter } from './character_counter';
import { EditIndicator } from './edit_indicator'; import { EditIndicator } from './edit_indicator';
import { NavigationBar } from './navigation_bar'; import { NavigationBar } from './navigation_bar';
import { PollForm } from "./poll_form"; import { PollForm } from "./poll_form";
import { QuoteIndicator } from './quote_indicator';
import { ReplyIndicator } from './reply_indicator'; import { ReplyIndicator } from './reply_indicator';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d'; const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
@ -296,6 +297,8 @@ class ComposeForm extends ImmutablePureComponent {
<UploadFormContainer /> <UploadFormContainer />
<PollForm /> <PollForm />
<QuoteIndicator />
<div className='compose-form__footer'> <div className='compose-form__footer'>
<div className='compose-form__dropdowns'> <div className='compose-form__dropdowns'>
<PrivacyDropdownContainer disabled={this.props.isEditing} /> <PrivacyDropdownContainer disabled={this.props.isEditing} />

View File

@ -0,0 +1,62 @@
import { useCallback } from "react";
import { FormattedMessage, defineMessages, useIntl } from "react-intl";
import { Link } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import BarChart4BarsIcon from 'mastodon/../material-icons/400-24px/bar_chart_4_bars.svg?react';
import CloseIcon from 'mastodon/../material-icons/400-24px/close.svg?react';
import PhotoLibraryIcon from 'mastodon/../material-icons/400-24px/photo_library.svg?react';
import { cancelReplyCompose } from "mastodon/actions/compose";
import { Avatar } from "mastodon/components/avatar";
import { DisplayName } from "mastodon/components/display_name";
import { Icon } from "mastodon/components/icon";
import { IconButton } from "mastodon/components/icon_button";
const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
});
export const QuoteIndicator = () => {
const intl = useIntl();
const dispatch = useDispatch();
const id = useSelector(state => state.getIn(['compose', 'quote_from']));
const status = useSelector(state => state.getIn(['statuses', id]));
const account = useSelector(state => state.getIn(['accounts', status?.get('account')]));
const handleCancelClick = useCallback(() => {
dispatch(cancelReplyCompose());
}, [dispatch]);
if (!status) {
return null;
}
const content = { __html: status.get('contentHtml') };
return (
<div className='edit-indicator'>
<div className='edit-indicator__header'>
<Link to={`/@${account.get('acct')}`} className='status__display-name'>
<Avatar account={account} size={46} />
<DisplayName account={account} />
</Link>
<div className='edit-indicator__cancel'>
<IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={handleCancelClick} inverted />
</div>
</div>
<div className='edit-indicator__content translate' dangerouslySetInnerHTML={content} />
{(status.get('poll') || status.get('media_attachments').size > 0) && (
<div className='edit-indicator__attachments'>
{status.get('poll') && <><Icon icon={BarChart4BarsIcon} /><FormattedMessage id='reply_indicator.poll' defaultMessage='Poll' /></>}
{status.get('media_attachments').size > 0 && <><Icon icon={PhotoLibraryIcon} /><FormattedMessage id='reply_indicator.attachments' defaultMessage='{count, plural, one {# attachment} other {# attachments}}' values={{ count: status.get('media_attachments').size }} /></>}
</div>
)}
</div>
);
};

View File

@ -167,7 +167,7 @@ class Footer extends ImmutablePureComponent {
} }
dispatch(quoteCompose(status, router.history)); dispatch(quoteCompose(status, router.history));
} };
handleQuoteClick = () => { handleQuoteClick = () => {
const { dispatch, askReplyConfirmation, intl } = this.props; const { dispatch, askReplyConfirmation, intl } = this.props;
@ -181,7 +181,7 @@ class Footer extends ImmutablePureComponent {
} else { } else {
this._performQuote(); this._performQuote();
} }
} };
handleOpenClick = e => { handleOpenClick = e => {
if (e.button !== 0 || !history) { if (e.button !== 0 || !history) {

View File

@ -11,6 +11,7 @@ import { connect } from 'react-redux';
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react'; import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react'; import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
import FormatQuoteIcon from '@/material-icons/400-24px/format_quote.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
@ -111,8 +112,8 @@ class ActionBar extends PureComponent {
}; };
handleQuoteClick = () => { handleQuoteClick = () => {
this.props.onQuote(this.props.status, this.props.history); this.props.onQuote(this.props.status);
} };
handleFavouriteClick = () => { handleFavouriteClick = () => {
this.props.onFavourite(this.props.status); this.props.onFavourite(this.props.status);
@ -324,7 +325,7 @@ class ActionBar extends PureComponent {
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} /></div> <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div> <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'><IconButton disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='quote' iconComponent={QuoteIcon} onClick={this.handleQuoteClick} /></div> <div className='detailed-status__button'><IconButton disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='format-quote' iconComponent={FormatQuoteIcon} onClick={this.handleQuoteClick} /></div>
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div>
<div className='detailed-status__action-bar-dropdown'> <div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer icon='ellipsis-h' iconComponent={MoreHorizIcon} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} /> <DropdownMenuContainer icon='ellipsis-h' iconComponent={MoreHorizIcon} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />

View File

@ -152,9 +152,9 @@ export default class Card extends PureComponent {
}; };
if (largeImage && card.get('type') === 'video') { if (largeImage && card.get('type') === 'video') {
thumbnailStyle.aspectRatio = `16 / 9`; thumbnailStyle.aspectRatio = `${quote ? 8 : 16} / 9`;
} else if (largeImage) { } else if (largeImage) {
thumbnailStyle.aspectRatio = '1.91 / 1'; thumbnailStyle.aspectRatio = `1.91 / ${quote ? 2 : 1}`;
} else { } else {
thumbnailStyle.aspectRatio = 1; thumbnailStyle.aspectRatio = 1;
} }

View File

@ -1,6 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedDate, FormattedMessage } from 'react-intl'; import { FormattedDate, FormattedMessage, injectIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link, withRouter } from 'react-router-dom'; import { Link, withRouter } from 'react-router-dom';
@ -62,7 +62,8 @@ class DetailedStatus extends ImmutablePureComponent {
handleAccountClick = (e) => { handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.props.history) { if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.props.history) {
e.preventDefault(); e.preventDefault();
this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); const acct = e.currentTarget.getAttribute('data-acct');
this.props.history.push(`/@${acct}`);
} }
e.stopPropagation(); e.stopPropagation();
@ -78,17 +79,15 @@ class DetailedStatus extends ImmutablePureComponent {
handleExpandedQuoteToggle = () => { handleExpandedQuoteToggle = () => {
this.props.onQuoteToggleHidden(this.props.status); this.props.onQuoteToggleHidden(this.props.status);
} };
handleQuoteClick = () => { handleQuoteClick = () => {
if (!this.props) { if (this.props.history) {
return; const status = this._properStatus();
this.props.history.push(`/@${status.getIn(['quote', 'account', 'acct'])}/${status.getIn(['quote', 'id'])}`);
} }
};
const { status } = this.props;
this.props.history.push(`/statuses/${status.getIn(['quote', 'id'])}`);
}
handleQuoteUserClick = () =>{ handleQuoteUserClick = () =>{
if (!this.props) { if (!this.props) {
return; return;
@ -161,7 +160,7 @@ class DetailedStatus extends ImmutablePureComponent {
render () { render () {
const status = this._properStatus(); const status = this._properStatus();
const outerStyle = { boxSizing: 'border-box' }; const outerStyle = { boxSizing: 'border-box' };
const { compact, pictureInPicture, quoteMuted } = this.props; const { compact, pictureInPicture, quoteMuted } = this.props;
if (!status) { if (!status) {
return null; return null;
@ -180,11 +179,19 @@ class DetailedStatus extends ImmutablePureComponent {
const language = status.getIn(['translation', 'language']) || status.get('language'); const language = status.getIn(['translation', 'language']) || status.get('language');
const identity = (status, _0, _1, quote = false) => ( const identity = (status, _, quote) => (
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={quote ? this.handleQuoteUserClick : this.handleAccountClick} data-acct={status.getIn(['account', 'acct'])} className='detailed-status__display-name'> <>
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={46} /></div> {status.get('visibility') === 'direct' && (
<DisplayName account={status.get('account')} localDomain={this.props.domain} /> <div className='status__prepend'>
</a> <div className='status__prepend-icon-wrapper'><Icon id='at' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' />
</div>
)}
<a href={`/@${status.getIn(['account', 'acct'])}`} data-acct={status.getIn(['account', 'acct'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={quote ? 20 : 48} /></div>
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
</a>
</>
); );
const media = (status, quote = false) => { const media = (status, quote = false) => {
@ -251,7 +258,8 @@ class DetailedStatus extends ImmutablePureComponent {
); );
} }
} else if (status.get('spoiler_text').length === 0) { } else if (status.get('spoiler_text').length === 0) {
return <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} quote={quote} />; return (<Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia}
card={status.get('card', null)} quote={quote} />);
} }
} }
@ -331,10 +339,7 @@ class DetailedStatus extends ImmutablePureComponent {
<FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' /> <FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' />
</div> </div>
)} )}
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='detailed-status__display-name'> {identity(status, null, false)}
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={46} /></div>
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
</a>
<StatusContent <StatusContent
status={status} status={status}
@ -362,4 +367,4 @@ class DetailedStatus extends ImmutablePureComponent {
} }
export default withRouter(DetailedStatus); export default connect(mapStateToProps)(withRouter(injectIntl(DetailedStatus)));

View File

@ -255,7 +255,7 @@ class Status extends ImmutablePureComponent {
handleToggleQuoteMediaVisibility = () => { handleToggleQuoteMediaVisibility = () => {
this.setState({ showQuoteMedia: !this.state.showQuoteMedia }); this.setState({ showQuoteMedia: !this.state.showQuoteMedia });
} };
handleFavouriteClick = (status) => { handleFavouriteClick = (status) => {
const { dispatch } = this.props; const { dispatch } = this.props;
@ -327,7 +327,7 @@ class Status extends ImmutablePureComponent {
} else { } else {
dispatch(quoteCompose(status, this.props.history)); dispatch(quoteCompose(status, this.props.history));
} }
} };
handleModalReblog = (status, privacy) => { handleModalReblog = (status, privacy) => {
this.props.dispatch(reblog(status, privacy)); this.props.dispatch(reblog(status, privacy));
@ -450,7 +450,7 @@ class Status extends ImmutablePureComponent {
} else { } else {
this.props.dispatch(hideQuote(status.get('id'))); this.props.dispatch(hideQuote(status.get('id')));
} }
} };
handleToggleAll = () => { handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;

View File

@ -683,6 +683,7 @@
"status.translate": "翻訳", "status.translate": "翻訳",
"status.translated_from_with": "{provider}を使って{lang}から翻訳", "status.translated_from_with": "{provider}を使って{lang}から翻訳",
"status.uncached_media_warning": "プレビューは使用できません", "status.uncached_media_warning": "プレビューは使用できません",
"status.unlisted_quote": "未収載の引用",
"status.unmute_conversation": "会話のミュートを解除", "status.unmute_conversation": "会話のミュートを解除",
"status.unpin": "プロフィールへの固定を解除", "status.unpin": "プロフィールへの固定を解除",
"subscribed_languages.lead": "選択した言語のトゥートだけがホームとリストのタイムラインに表示されます。全ての言語のトゥートを受け取る場合は全てのチェックを外して下さい。", "subscribed_languages.lead": "選択した言語のトゥートだけがホームとリストのタイムラインに表示されます。全ての言語のトゥートを受け取る場合は全てのチェックを外して下さい。",

View File

@ -7,7 +7,6 @@ import {
COMPOSE_REPLY, COMPOSE_REPLY,
COMPOSE_REPLY_CANCEL, COMPOSE_REPLY_CANCEL,
COMPOSE_QUOTE, COMPOSE_QUOTE,
COMPOSE_QUOTE_CANCEL,
COMPOSE_DIRECT, COMPOSE_DIRECT,
COMPOSE_MENTION, COMPOSE_MENTION,
COMPOSE_SUBMIT_REQUEST, COMPOSE_SUBMIT_REQUEST,
@ -397,7 +396,6 @@ export default function compose(state = initialState, action) {
case COMPOSE_UPLOAD_CHANGE_REQUEST: case COMPOSE_UPLOAD_CHANGE_REQUEST:
return state.set('is_changing_upload', true); return state.set('is_changing_upload', true);
case COMPOSE_REPLY_CANCEL: case COMPOSE_REPLY_CANCEL:
case COMPOSE_QUOTE_CANCEL:
case COMPOSE_RESET: case COMPOSE_RESET:
case COMPOSE_SUBMIT_SUCCESS: case COMPOSE_SUBMIT_SUCCESS:
return clearAll(state); return clearAll(state);
@ -530,6 +528,8 @@ export default function compose(state = initialState, action) {
map.set('id', action.status.get('id')); map.set('id', action.status.get('id'));
map.set('text', action.text); map.set('text', action.text);
map.set('in_reply_to', action.status.get('in_reply_to_id')); map.set('in_reply_to', action.status.get('in_reply_to_id'));
map.set('quote_from', action.status.getIn(['quote', 'id']));
map.set('quote_from_url', action.status.getIn(['quote', 'url']));
map.set('privacy', action.status.get('visibility')); map.set('privacy', action.status.get('visibility'));
map.set('media_attachments', action.status.get('media_attachments')); map.set('media_attachments', action.status.get('media_attachments'));
map.set('focusDate', new Date()); map.set('focusDate', new Date());

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m228-240 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T458-480L320-240h-92Zm360 0 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T818-480L680-240h-92Z"/></svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m228-240 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T458-480L320-240h-92Zm360 0 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T818-480L680-240h-92ZM320-500q25 0 42.5-17.5T380-560q0-25-17.5-42.5T320-620q-25 0-42.5 17.5T260-560q0 25 17.5 42.5T320-500Zm360 0q25 0 42.5-17.5T740-560q0-25-17.5-42.5T680-620q-25 0-42.5 17.5T620-560q0 25 17.5 42.5T680-500Zm0-60Zm-360 0Z"/></svg>

After

Width:  |  Height:  |  Size: 538 B

View File

@ -1105,6 +1105,10 @@ body > [data-popper-placement] {
grid-template-rows: 46px max-content; grid-template-rows: 46px max-content;
gap: 0 10px; gap: 0 10px;
&.quote-indicator {
background: $success-green;
}
.detailed-status__display-name { .detailed-status__display-name {
margin-bottom: 4px; margin-bottom: 4px;
} }
@ -1422,17 +1426,6 @@ body > [data-popper-placement] {
appearance: none; appearance: none;
} }
.status__avatar,
.detailed-status__display-avatar {
position: absolute;
top: 5px !important;
left: 5px !important;
}
.display-name {
padding-left: 56px;
}
.detailed-status__display-name { .detailed-status__display-name {
margin-bottom: 0; margin-bottom: 0;
line-height: unset; line-height: unset;
@ -1630,7 +1623,8 @@ body > [data-popper-placement] {
color: $dark-text-color; color: $dark-text-color;
} }
.status__info .status__display-name { .status__info .status__display-name,
.edit-indicator .status__display-name {
max-width: 100%; max-width: 100%;
display: flex; display: flex;
font-size: 15px; font-size: 15px;

View File

@ -13,7 +13,7 @@
%span.display-name %span.display-name
%bdi %bdi
%strong.display-name__html.p-name.emojify= display_name(author, custom_emojify: true, autoplay: prefers_autoplay?) %strong.display-name__html.p-name.emojify= display_name(author, custom_emojify: true, autoplay: prefers_autoplay?)
&nbsp;
%span.display-name__account %span.display-name__account
= acct(author) = acct(author)
- if !inline && author.locked? - if !inline && author.locked?

View File

@ -12,21 +12,8 @@
* *
%data.dt-published{ value: status.created_at.to_time.iso8601 } %data.dt-published{ value: status.created_at.to_time.iso8601 }
.p-author.h-card = render 'statuses/author', author: status.account
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener noreferrer' do
.status__avatar
%div
- if prefers_autoplay?
= image_tag status.account.avatar_original_url, alt: '', class: 'u-photo account__avatar'
- else
= image_tag status.account.avatar_static_url, alt: '', class: 'u-photo account__avatar'
%span.display-name
%bdi
%strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: prefers_autoplay?)
&nbsp;
%span.display-name__account
= acct(status.account)
= fa_icon('lock') if status.account.locked?
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< .status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
- if status.spoiler_text? - if status.spoiler_text?
%p< %p<