Merge remote-tracking branch 'origin/features/main' into features/4.3alpha

This commit is contained in:
YoheiZuho 2024-02-02 22:18:11 +09:00
commit c09bab16e1
94 changed files with 1734 additions and 444 deletions

View File

@ -8,8 +8,12 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def index
cache_if_unauthenticated!
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
@statuses = load_statuses
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,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(accounts, current_user&.account_id)
end
private

View File

@ -6,8 +6,12 @@ class Api::V1::BookmarksController < Api::BaseController
after_action :insert_pagination_headers
def index
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
@statuses = load_statuses
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,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(accounts, current_user&.account_id)
end
private

View File

@ -6,8 +6,12 @@ class Api::V1::FavouritesController < Api::BaseController
after_action :insert_pagination_headers
def index
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
@statuses = load_statuses
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,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(accounts, current_user&.account_id)
end
private

View File

@ -47,10 +47,14 @@ class Api::V1::StatusesController < Api::BaseController
loaded_ancestors = cache_collection(ancestors_results, Status)
loaded_descendants = cache_collection(descendants_results, Status)
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
statuses = [@status] + @context.ancestors + @context.descendants
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
statuses = [@status] + @context.ancestors + @context.descendants
account_ids = statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
accounts = Account.where(id: account_ids)
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
render json: @context, serializer: REST::ContextSerializer,
relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(accounts, current_user&.account_id)
end
def create
@ -68,7 +72,8 @@ class Api::V1::StatusesController < Api::BaseController
poll: status_params[:poll],
allowed_mentions: status_params[:allowed_mentions],
idempotency: request.headers['Idempotency-Key'],
with_rate_limit: true
with_rate_limit: true,
quote_id: status_params[:quote_id].presence
)
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
@ -138,6 +143,7 @@ class Api::V1::StatusesController < Api::BaseController
:visibility,
:language,
:scheduled_at,
:quote_id,
allowed_mentions: [],
media_ids: [],
media_attributes: [

View File

@ -10,11 +10,16 @@ class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController
with_read_replica do
@statuses = load_statuses
@relationships = StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
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)
end
render json: @statuses,
each_serializer: REST::StatusSerializer,
relationships: @relationships,
account_relationships: @account_relationships,
status: account_home_feed.regenerating? ? 206 : 200
end

View File

@ -9,9 +9,13 @@ class Api::V1::Timelines::ListController < Api::V1::Timelines::BaseController
PERMITTED_PARAMS = %i(limit).freeze
def show
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,
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)
end
private

View File

@ -7,8 +7,13 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
def show
cache_if_unauthenticated!
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
@statuses = load_statuses
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,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(accounts, current_user&.account_id)
end
private

View File

@ -8,8 +8,13 @@ class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController
def show
cache_if_unauthenticated!
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
@statuses = load_statuses
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,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(accounts, current_user&.account_id)
end
private

View File

@ -11,6 +11,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
skip_before_action :require_functional!
include Localized

View File

@ -32,6 +32,7 @@ module ContextHelper
'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText'
},
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
quoteUrl: { 'quoteUrl' => 'as:quoteUrl' },
}.freeze
def full_context

View File

@ -19,7 +19,12 @@ module FormattingHelper
module_function :extract_status_plain_text
def status_content_format(status)
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []))
html_aware_format(
status.text,
status.local?,
preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []),
quote: status.respond_to?(:quote) && status.quote
)
end
def rss_status_content_format(status)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 588 B

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,89 @@
import { changeCompose } from '../actions/compose';
export const UTILBTNS_GOJI = 'UTILBTNS_GOJI';
export const UTILBTNS_HARUKIN = 'UTILBTNS_HARUKIN';
export const UTILBTNS_RISA = 'UTILBTNS_RISA';
export function submitGoji (textarea) {
return function (dispatch, getState) {
if (!textarea.value) {
let text = [
"#ゴジモリィィィィイイ",
":goji:"
].join("\r\n");
dispatch(submitGojiRequest());
dispatch(changeCompose(text));
textarea.focus();
}
}
}
export function submitGojiRequest () {
return {
type: UTILBTNS_GOJI
}
}
export function submitHarukin (textarea) {
return function (dispatch, getState) {
const HARUKINS = [":harukin: ", ":harukin_old: ", ":harukin_ika: ", ":harukin_tako: "];
const MAX = 6;
if (!textarea.value) {
let text = "";
let quantity = Math.round(Math.random() * MAX + 1);
let type = Math.round(Math.random() * (HARUKINS.length - 1));
let harukin = HARUKINS[type];
switch (quantity) {
default:
text = [
harukin.repeat(quantity),
"🔥 ".repeat(quantity)
].join("\r\n");
break;
case MAX + 1:
text = `${harukin}💕\r\n`.repeat(6);
break;
}
dispatch(submitHarukinRequest());
dispatch(changeCompose(text));
textarea.focus();
}
}
}
export function submitHarukinRequest () {
return {
type: UTILBTNS_HARUKIN
}
}
export function submitRisa (textarea) {
return function (dispatch, getState) {
if (!textarea.value) {
let text = [
"@risa2 "
].join("\r\n");
dispatch(submitRisaRequest());
dispatch(changeCompose(text));
textarea.focus();
}
}
}
export function submitRisaRequest () {
return {
type: UTILBTNS_RISA
}
}

View File

@ -1,6 +1,8 @@
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedStatuses } from './importer';
import { uniq } from '../utils/uniq';
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
@ -21,6 +23,7 @@ export function fetchBookmarkedStatuses() {
api(getState).get('/api/v1/bookmarks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchBookmarkedStatusesFail(error));
@ -62,6 +65,7 @@ export function expandBookmarkedStatuses() {
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandBookmarkedStatusesFail(error));

View File

@ -24,6 +24,8 @@ export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
export const COMPOSE_RESET = 'COMPOSE_RESET';
@ -126,6 +128,23 @@ export function cancelReplyCompose() {
};
}
export function quoteCompose(status, routerHistory) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_QUOTE,
status: status,
});
ensureComposeIsVisible(getState, routerHistory);
};
};
export function cancelQuoteCompose() {
return {
type: COMPOSE_QUOTE_CANCEL,
};
};
export function resetCompose() {
return {
type: COMPOSE_RESET,
@ -208,6 +227,7 @@ export function submitCompose(routerHistory) {
visibility: getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null),
language: getState().getIn(['compose', 'language']),
quote_id: getState().getIn(['compose', 'quote_from'], null),
},
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),

View File

@ -1,6 +1,8 @@
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedStatuses } from './importer';
import { uniq } from '../utils/uniq';
export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
@ -21,6 +23,7 @@ export function fetchFavouritedStatuses() {
api(getState).get('/api/v1/favourites').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchFavouritedStatusesFail(error));
@ -65,6 +68,7 @@ export function expandFavouritedStatuses() {
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandFavouritedStatusesFail(error));

View File

@ -72,6 +72,10 @@ export function importFetchedStatuses(statuses) {
processStatus(status.reblog);
}
if (status.quote && status.quote.id) {
processStatus(status.quote);
}
if (status.poll && status.poll.id) {
pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
}

View File

@ -49,6 +49,8 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
normalStatus.spoiler_text = normalOldStatus.get('spoiler_text');
normalStatus.hidden = normalOldStatus.get('hidden');
normalStatus.quote = normalOldStatus.get('quote');
normalStatus.quote_hidden = normalOldStatus.get('quote_hidden');
if (normalOldStatus.get('translation')) {
normalStatus.translation = normalOldStatus.get('translation');
@ -69,6 +71,11 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
if (status.quote) {
normalStatus.quote = normalizeStatus(status.quote, null);
normalStatus.quote_hidden = normalStatus.quote.hidden;
}
}
if (normalOldStatus) {

View File

@ -28,6 +28,9 @@ export const STATUS_REVEAL = 'STATUS_REVEAL';
export const STATUS_HIDE = 'STATUS_HIDE';
export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
export const QUOTE_REVEAL = 'QUOTE_REVEAL';
export const QUOTE_HIDE = 'QUOTE_HIDE';
export const REDRAFT = 'REDRAFT';
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
@ -348,3 +351,25 @@ export const undoStatusTranslation = (id, pollId) => ({
id,
pollId,
});
export function hideQuote(ids) {
if (!Array.isArray(ids)) {
ids = [ids];
}
return {
type: QUOTE_HIDE,
ids,
};
};
export function revealQuote(ids) {
if (!Array.isArray(ids)) {
ids = [ids];
}
return {
type: QUOTE_REVEAL,
ids,
};
};

View File

@ -4,6 +4,9 @@ import api, { getLinks } from 'mastodon/api';
import { compareId } from 'mastodon/compare_id';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import { uniq } from '../utils/uniq';
import { fetchRelationships } from './accounts';
import { importFetchedStatus, importFetchedStatuses } from './importer';
import { submitMarkers } from './markers';
@ -45,6 +48,7 @@ export function updateTimeline(timeline, status, accept) {
}
dispatch(importFetchedStatus(status));
dispatch(fetchRelationships([status.reblog ? status.reblog.account.id : status.account.id, status.quote ? status.quote.account.id : null].filter(x => x)));
dispatch({
type: TIMELINE_UPDATE,
@ -118,6 +122,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchRelationships(uniq(response.data.map(item => [item.reblog ? item.reblog.account.id : item.account.id, item.quote ? item.quote.account.id : null]).flat().filter(x => x))));
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
if (timelineId === 'home' && !isLoadingMore && !isLoadingRecent) {

View File

@ -1,4 +1,4 @@
import logo from '@/images/logo.svg';
import logo from '@/images/y-zu-logo.svg';
export const WordmarkLogo: React.FC = () => (
<svg viewBox='0 0 261 66' className='logo logo--wordmark' role='img'>
@ -9,4 +9,4 @@ export const WordmarkLogo: React.FC = () => (
export const SymbolLogo: React.FC = () => (
<img src={logo} alt='Mastodon' className='logo logo--icon' />
);
);

View File

@ -221,6 +221,7 @@ class MediaGallery extends PureComponent {
visible: PropTypes.bool,
autoplay: PropTypes.bool,
onToggleVisibility: PropTypes.func,
quote: PropTypes.bool,
};
state = {
@ -291,7 +292,7 @@ class MediaGallery extends PureComponent {
}
render () {
const { media, lang, intl, sensitive, defaultWidth, autoplay } = this.props;
const { media, lang, intl, sensitive, defaultWidth, autoplay, quote } = this.props;
const { visible } = this.state;
const width = this.state.width || defaultWidth;
@ -305,6 +306,14 @@ class MediaGallery extends PureComponent {
style.aspectRatio = '3 / 2';
}
if (quote && style.height) {
style.height /= 2;
}
if (quote && style.height) {
style.height /= 2;
}
const size = media.take(4).size;
const uncached = media.every(attachment => attachment.get('type') === 'unknown');

View File

@ -35,6 +35,29 @@ import { VisibilityIcon } from './visibility_icon';
const domParser = new DOMParser();
export const mapStateToProps = (state, props) => {
let status = props.status;
if (status === null) {
return null;
}
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
status = status.get('reblog');
}
if (status.get('quote', null) === null) {
return {
quoteMuted: !!status.get('quote_id', null),
};
}
const id = status.getIn(['quote', 'account', 'id'], null);
return {
quoteMuted: id !== null && (state.getIn(['relationships', id, 'muting']) || state.getIn(['relationships', id, 'blocking']) || state.getIn(['relationships', id, 'blocked_by']) || state.getIn(['relationships', id, 'domain_blocking']) || status.getIn(['quote', 'muted'])),
};
};
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']);
@ -76,6 +99,60 @@ const messages = defineMessages({
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
});
export const quote = (status, muted, quoteMuted, handleQuoteClick, handleExpandedQuoteToggle, identity, media, router, contextType = 'home') => {
const quoteStatus = status.get('quote', null);
if (!quoteStatus) {
return null;
}
const quoteInner = (() => {
const hideUnlisted = quoteStatus.get('visibility') === 'unlisted'
&& ['public', 'community', 'hashtag'].includes(contextType);
if (quoteMuted || hideUnlisted) {
const content = (() => {
if (quoteMuted) {
return (
<FormattedMessage id='status.muted_quote' defaultMessage='Muted quote' />
);
}
return (
<button onClick={handleQuoteClick}>
<FormattedMessage id='status.unlisted_quote' defaultMessage='Unlisted quote' />
</button>
);
})();
return (
<div className={classNames('status__content', { 'muted-quote': quoteMuted, 'unlisted-quote': hideUnlisted, 'status__content--with-action': router })}>
{content}
</div>
);
}
return (
<div>
<div className='status__info'>
{identity(quoteStatus, null, null, true)}
</div>
<StatusContent status={quoteStatus} onClick={handleQuoteClick} expanded={!status.get('quote_hidden')} onExpandedToggle={handleExpandedQuoteToggle} quote />
{media(quoteStatus, true)}
</div>
);
})();
return (
<div
className={classNames('quote-status', `status-${quoteStatus.get('visibility')}`, { muted: muted })}
data-id={quoteStatus.get('id')}
dataurl={quoteStatus.get('url')}
>
{quoteInner}
</div>
);
};
class Status extends ImmutablePureComponent {
static propTypes = {
@ -102,7 +179,9 @@ class Status extends ImmutablePureComponent {
onToggleCollapsed: PropTypes.func,
onTranslate: PropTypes.func,
onInteractionModal: PropTypes.func,
onQuoteToggleHidden: PropTypes.func,
muted: PropTypes.bool,
quoteMuted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
onMoveUp: PropTypes.func,
@ -118,6 +197,7 @@ class Status extends ImmutablePureComponent {
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
contextType: PropTypes.string,
...WithOptionalRouterPropTypes,
};
@ -130,10 +210,12 @@ class Status extends ImmutablePureComponent {
'hidden',
'unread',
'pictureInPicture',
'quoteMuted',
];
state = {
showMedia: defaultMediaVisibility(this.props.status),
showQuoteMedia: defaultMediaVisibility(this.props.status ? this.props.status.get('quote', null) : null),
statusId: undefined,
forceFilter: undefined,
};
@ -142,6 +224,7 @@ class Status extends ImmutablePureComponent {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return {
showMedia: defaultMediaVisibility(nextProps.status),
showQuoteMedia: defaultMediaVisibility(nextProps.status ? nextProps.status.get('quote', null) : null),
statusId: nextProps.status.get('id'),
};
} else {
@ -153,6 +236,10 @@ class Status extends ImmutablePureComponent {
this.setState({ showMedia: !this.state.showMedia });
};
handleToggleQuoteMediaVisibility = () => {
this.setState({ showQuoteMedia: !this.state.showQuoteMedia });
}
handleClick = e => {
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
return;
@ -182,6 +269,24 @@ class Status extends ImmutablePureComponent {
this._openProfile(proper);
};
handleQuoteClick = () => {
if (!this.props) {
return;
}
const { status } = this.props;
this.props.history.push(`/statuses/${status.getIn(['reblog', 'quote', 'id'], status.getIn(['quote', 'id']))}`);
}
handleQuoteUserClick = () =>{
if (!this.props) {
return;
}
const { status } = this.props;
this.location.href(`/@${status.getIn(['account', 'acct'])}`);
}
handleExpandedToggle = () => {
this.props.onToggleHidden(this._properStatus());
};
@ -194,6 +299,10 @@ class Status extends ImmutablePureComponent {
this.props.onTranslate(this._properStatus());
};
handleExpandedQuoteToggle = () => {
this.props.onQuoteToggleHidden(this._properStatus());
}
getAttachmentAspectRatio () {
const attachments = this._properStatus().get('media_attachments');
@ -348,7 +457,7 @@ class Status extends ImmutablePureComponent {
};
render () {
const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId } = this.props;
const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture, previousId, quoteMuted, nextInReplyToId, rootId, contextType } = this.props;
let { status, account, ...other } = this.props;
@ -370,7 +479,7 @@ class Status extends ImmutablePureComponent {
openMedia: this.handleHotkeyOpenMedia,
};
let media, statusAvatar, prepend, rebloggedByText;
let prepend, rebloggedByText;
if (hidden) {
return (
@ -445,99 +554,117 @@ class Status extends ImmutablePureComponent {
</div>
);
}
const media = (status, quote = false) => {
if (pictureInPicture.get('inUse')) {
return <PictureInPicturePlaceholder aspectRatio={this.getAttachmentAspectRatio()} />;
} else if (status.get('media_attachments').size > 0) {
const language = status.getIn(['translation', 'language']) || status.get('language');
if (pictureInPicture.get('inUse')) {
media = <PictureInPicturePlaceholder aspectRatio={this.getAttachmentAspectRatio()} />;
} else if (status.get('media_attachments').size > 0) {
const language = status.getIn(['translation', 'language']) || status.get('language');
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
return (
<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}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
sensitive={status.get('sensitive')}
blurhash={attachment.get('blurhash')}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
quote={quote}
/>
)}
</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={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}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
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}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
} else {
media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
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>
return (
<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}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
quote={quote}
/>
)}
</Bundle>
);
} else {
return (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
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}
quote={quote}
/>
)}
</Bundle>
);
}
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
return (
<Card
onOpenMedia={this.handleOpenMedia}
card={status.get('card')}
compact
sensitive={status.get('sensitive')}
quote={quote}
/>
);
}
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
media = (
<Card
onOpenMedia={this.handleOpenMedia}
card={status.get('card')}
compact
sensitive={status.get('sensitive')}
/>
);
}
return null;
};
if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={46} />;
} else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
}
const statusAvatar = (status, account) => {
if (account === undefined || account === null) {
return <Avatar account={status.get('account')} size={46} />;
} else {
return <AvatarOverlay account={status.get('account')} friend={account} />;
}
};
const identity = (status, account, _0, quote = false) => (
<a onClick={quote ? this.handleQuoteUserClick : this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' rel='noopener noreferrer'>
<div className='status__avatar'>
{statusAvatar(status, account)}
</div>
<DisplayName account={status.get('account')} />
</a>
);
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
@ -557,13 +684,15 @@ class Status extends ImmutablePureComponent {
<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'>
{/* <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>
</a> */}
{identity(status, account, false)}
</div>
<StatusContent
@ -577,8 +706,10 @@ class Status extends ImmutablePureComponent {
{...statusContentProps}
/>
{media}
{media(status)}
{quote(status, this.props.muted, quoteMuted, this.handleQuoteClick, this.handleExpandedQuoteToggle, identity, media, this.props, contextType)}
{expanded && hashtagBar}
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
@ -590,4 +721,4 @@ class Status extends ImmutablePureComponent {
}
export default withOptionalRouter(injectIntl(Status));
export default withOptionalRouter(injectIntl(Status));

View File

@ -16,6 +16,7 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import QuoteIcon from '@material-symbols/400-24px/format_quote.svg?react';
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
@ -46,6 +47,8 @@ const messages = defineMessages({
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
@ -85,6 +88,7 @@ class StatusActionBar extends ImmutablePureComponent {
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onQuote: PropTypes.func,
onDelete: PropTypes.func,
onDirect: PropTypes.func,
onMention: PropTypes.func,
@ -155,6 +159,10 @@ class StatusActionBar extends ImmutablePureComponent {
}
};
handleQuoteClick = () => {
this.props.onQuote(this.props.status, this.props.history);
}
handleBookmarkClick = () => {
this.props.onBookmark(this.props.status);
};
@ -248,6 +256,14 @@ class StatusActionBar extends ImmutablePureComponent {
this.props.onFilter();
};
static quoteTitle = (intl, messages, publicStatus) => {
if (publicStatus) {
return intl.formatMessage(messages.quote);
} else {
return intl.formatMessage(messages.cannot_quote);
}
}
render () {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
const { signedIn, permissions } = this.context.identity;
@ -391,6 +407,7 @@ class StatusActionBar extends ImmutablePureComponent {
<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='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' disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='quote' iconComponent={QuoteIcon} 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} />
{filterButton}

View File

@ -15,6 +15,7 @@ import PollContainer from 'mastodon/containers/poll_container';
import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
const QUOTE_MAX_HEIGHT = 112; // 22px * 5 (+ 2px padding at the top)
/**
*
@ -83,6 +84,7 @@ class StatusContent extends PureComponent {
onCollapsedToggle: PropTypes.func,
languages: ImmutablePropTypes.map,
intl: PropTypes.object,
quote: PropTypes.bool,
// from react-router
match: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
@ -130,12 +132,12 @@ class StatusContent extends PureComponent {
}
if (status.get('collapsed', null) === null && onCollapsedToggle) {
const { collapsible, onClick } = this.props;
const { collapsible, onClick, quote } = this.props;
const collapsed =
collapsible
&& onClick
&& node.clientHeight > MAX_HEIGHT
&& node.clientHeight > (quote ? QUOTE_MAX_HEIGHT : MAX_HEIGHT)
&& status.get('spoiler_text').length === 0;
onCollapsedToggle(collapsed);
@ -239,7 +241,7 @@ class StatusContent extends PureComponent {
};
render () {
const { status, intl, statusContent } = this.props;
const { status, intl, statusContent, quote } = this.props;
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed');
@ -270,6 +272,12 @@ class StatusContent extends PureComponent {
<PollContainer pollId={status.get('poll')} lang={language} />
);
if (quote) {
const doc = new DOMParser().parseFromString(content.__html, 'text/html').documentElement;
Array.from(doc.querySelectorAll('br')).forEach(nl => nl.replaceWith(' '));
content.__html = doc.outerHTML;
}
if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = '';

View File

@ -11,6 +11,7 @@ import { initBlockModal } from '../actions/blocks';
import { initBoostModal } from '../actions/boosts';
import {
replyCompose,
quoteCompose,
mentionCompose,
directCompose,
} from '../actions/compose';
@ -45,6 +46,8 @@ import {
editStatus,
translateStatus,
undoStatusTranslation,
hideQuote,
revealQuote,
} from '../actions/statuses';
import Status from '../components/status';
import { boostModal, deleteModal } from '../initial_state';
@ -57,6 +60,8 @@ const messages = defineMessages({
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
@ -111,6 +116,22 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}
},
onQuote (status, router) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.quoteMessage),
confirm: intl.formatMessage(messages.quoteConfirm),
onConfirm: () => dispatch(quoteCompose(status, router)),
}));
} else {
dispatch(quoteCompose(status, router));
}
});
},
onFavourite (status) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
@ -253,6 +274,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
},
onQuoteToggleHidden (status) {
if (status.get('quote_hidden')) {
dispatch(revealQuote(status.get('id')));
} else {
dispatch(hideQuote(status.get('id')));
}
},
onBlockDomain (domain) {
dispatch(openModal({
modalType: 'CONFIRM',

View File

@ -61,6 +61,7 @@ class Audio extends PureComponent {
volume: PropTypes.number,
muted: PropTypes.bool,
deployPictureInPicture: PropTypes.func,
quote: PropTypes.bool,
};
state = {
@ -105,7 +106,11 @@ class Audio extends PureComponent {
_setDimensions () {
const width = this.player.offsetWidth;
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
let height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
if (this.props.quote) {
height /= 2;
}
if (this.props.cacheWidth) {
this.props.cacheWidth(width);

View File

@ -39,6 +39,9 @@ const messages = defineMessages({
publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Update' },
reply: { id: 'compose_form.reply', defaultMessage: 'Reply' },
utilBtns_goji: { id: 'compose_form.utilBtns_goji', defaultMessage: 'Typo!!!' },
utilBtns_harukin: { id: 'compose_form.utilBtns_harukin', defaultMessage: 'Burn Harukin' },
utilBtns_risa: { id: 'compose_form.utilBtns_risa', defaultMessage: 'Risa' }
});
class ComposeForm extends ImmutablePureComponent {
@ -71,6 +74,9 @@ class ComposeForm extends ImmutablePureComponent {
singleColumn: PropTypes.bool,
lang: PropTypes.string,
maxChars: PropTypes.number,
onGojiSubmit: PropTypes.func.isRequired,
onHarukinSubmit: PropTypes.func.isRequired,
onRisaSubmit: PropTypes.func.isRequired,
...WithOptionalRouterPropTypes
};
@ -224,6 +230,10 @@ class ComposeForm extends ImmutablePureComponent {
this.props.onPickEmoji(position, data, needsSpace);
};
handleOnGojiSubmit = () => this.props.onGojiSubmit(this.textareaRef.current);
handleOnHarukinSubmit = () => this.props.onHarukinSubmit(this.textareaRef.current);
handleOnRisaSubmit = () => this.props.onRisaSubmit(this.textareaRef.current);
render () {
const { intl, onPaste, autoFocus, withoutNavigation, maxChars } = this.props;
const { highlighted } = this.state;
@ -311,6 +321,10 @@ class ComposeForm extends ImmutablePureComponent {
</div>
</div>
</div>
<div className="compose-form__utilBtns">
<Button className="compose-form__utilBtns-goji" text={intl.formatMessage(messages.utilBtns_goji)} onClick={this.handleOnGojiSubmit} block />
<Button className="compose-form__utilBtns-harukin" text={intl.formatMessage(messages.utilBtns_harukin)} onClick={this.handleOnHarukinSubmit} block />
</div>
</form>
);
}

View File

@ -45,4 +45,4 @@ export const ReplyIndicator = () => {
</div>
</div>
);
};
};

View File

@ -10,6 +10,11 @@ import {
insertEmojiCompose,
uploadCompose,
} from '../../../actions/compose';
import {
submitGoji,
submitHarukin,
submitRisa
} from '../../../actions/UtilBtns';
import ComposeForm from '../components/compose_form';
const mapStateToProps = state => ({
@ -65,6 +70,18 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(insertEmojiCompose(position, data, needsSpace));
},
onRisaSubmit (textarea) {
dispatch(submitRisa(textarea));
},
onGojiSubmit (textarea) {
dispatch(submitGoji(textarea));
},
onHarukinSubmit (textarea) {
dispatch(submitHarukin(textarea));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm);

View File

@ -0,0 +1,36 @@
import { connect } from 'react-redux';
import { cancelReplyCompose, cancelQuoteCompose } from '../../../actions/compose';
import { makeGetStatus } from '../../../selectors';
import ReplyIndicator from '../components/reply_indicator';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = (state, props) => {
let statusId = state.getIn(['compose', 'id'], null);
let editing = true;
if (statusId === null) {
statusId = state.getIn(['compose', props.quote ? 'quote_from' : 'in_reply_to']);
editing = false;
}
return {
status: getStatus(state, { id: statusId }),
quote: props.quote,
editing,
};
};
return mapStateToProps;
};
const mapDispatchToProps = dispatch => ({
onCancel (quote) {
dispatch(quote ? cancelQuoteCompose() : cancelReplyCompose());
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator);

View File

@ -15,7 +15,7 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import { initBoostModal } from 'mastodon/actions/boosts';
import { replyCompose } from 'mastodon/actions/compose';
import { replyCompose, quoteCompose } from 'mastodon/actions/compose';
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
import { openModal } from 'mastodon/actions/modal';
import { IconButton } from 'mastodon/components/icon_button';
@ -30,9 +30,13 @@ const messages = defineMessages({
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
});
@ -154,6 +158,31 @@ class Footer extends ImmutablePureComponent {
}
};
_performQuote = () => {
const { dispatch, status, onClose } = this.props;
const { router } = this.context;
if (onClose) {
onClose();
}
dispatch(quoteCompose(status, router.history));
}
handleQuoteClick = () => {
const { dispatch, askReplyConfirmation, intl } = this.props;
if (askReplyConfirmation) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.quoteMessage),
confirm: intl.formatMessage(messages.quoteConfirm),
onConfirm: this._performQuote,
}));
} else {
this._performQuote();
}
}
handleOpenClick = e => {
if (e.button !== 0 || !history) {
return;

View File

@ -16,6 +16,7 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import QuoteIcon from '@/material-icons/400-24px/format_quote.svg?react';
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
@ -25,6 +26,7 @@ import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { IconButton } from '../../../components/icon_button';
import StatusActionBar from '../../../components/status_action_bar';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { me } from '../../../initial_state';
@ -39,6 +41,8 @@ const messages = defineMessages({
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
more: { id: 'status.more', defaultMessage: 'More' },
@ -77,6 +81,7 @@ class ActionBar extends PureComponent {
relationship: ImmutablePropTypes.map,
onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired,
onQuote: PropTypes.func,
onFavourite: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
@ -105,6 +110,10 @@ class ActionBar extends PureComponent {
this.props.onReblog(this.props.status, e);
};
handleQuoteClick = () => {
this.props.onQuote(this.props.status, this.props.history);
}
handleFavouriteClick = () => {
this.props.onFavourite(this.props.status);
};
@ -315,8 +324,8 @@ 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 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 disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='quote' iconComponent={QuoteIcon} 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__action-bar-dropdown'>
<DropdownMenuContainer icon='ellipsis-h' iconComponent={MoreHorizIcon} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
</div>

View File

@ -62,6 +62,7 @@ export default class Card extends PureComponent {
card: ImmutablePropTypes.map,
onOpenMedia: PropTypes.func.isRequired,
sensitive: PropTypes.bool,
quote: PropTypes.bool,
};
state = {
@ -121,7 +122,7 @@ export default class Card extends PureComponent {
}
render () {
const { card } = this.props;
const { card, quote } = this.props;
const { embedded, revealed } = this.state;
if (card === null) {

View File

@ -22,6 +22,7 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { Avatar } from '../../../components/avatar';
import { DisplayName } from '../../../components/display_name';
import MediaGallery from '../../../components/media_gallery';
import { quote } from '../../../components/status';
import StatusContent from '../../../components/status_content';
import Audio from '../../audio';
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
@ -36,17 +37,21 @@ class DetailedStatus extends ImmutablePureComponent {
onOpenMedia: PropTypes.func.isRequired,
onOpenVideo: PropTypes.func.isRequired,
onToggleHidden: PropTypes.func.isRequired,
onQuoteToggleHidden: PropTypes.func.isRequired,
onTranslate: PropTypes.func.isRequired,
measureHeight: PropTypes.bool,
onHeightChange: PropTypes.func,
domain: PropTypes.string.isRequired,
compact: PropTypes.bool,
quoteMuted: PropTypes.bool,
showMedia: PropTypes.bool,
showQuoteMedia: PropTypes.bool,
pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
onToggleMediaVisibility: PropTypes.func,
onQuoteToggleMediaVisibility: PropTypes.func,
...WithRouterPropTypes,
};
@ -71,6 +76,28 @@ class DetailedStatus extends ImmutablePureComponent {
this.props.onToggleHidden(this.props.status);
};
handleExpandedQuoteToggle = () => {
this.props.onQuoteToggleHidden(this.props.status);
}
handleQuoteClick = () => {
if (!this.props) {
return;
}
const { status } = this.props;
this.props.history.push(`/statuses/${status.getIn(['quote', 'id'])}`);
}
handleQuoteUserClick = () =>{
if (!this.props) {
return;
}
const { status } = this.props;
this.location.href(`/@${status.getIn(['account', 'acct'])}`);
}
_measureHeight (heightJustChanged) {
if (this.props.measureHeight && this.node) {
scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
@ -134,13 +161,12 @@ class DetailedStatus extends ImmutablePureComponent {
render () {
const status = this._properStatus();
const outerStyle = { boxSizing: 'border-box' };
const { compact, pictureInPicture } = this.props;
const { compact, pictureInPicture, quoteMuted } = this.props;
if (!status) {
return null;
}
let media = '';
let applicationLink = '';
let reblogLink = '';
const reblogIcon = 'retweet';
@ -154,69 +180,82 @@ class DetailedStatus extends ImmutablePureComponent {
const language = status.getIn(['translation', 'language']) || status.get('language');
if (pictureInPicture.get('inUse')) {
media = <PictureInPicturePlaceholder aspectRatio={this.getAttachmentAspectRatio()} />;
} else if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
const identity = (status, _0, _1, quote = false) => (
<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>
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
</a>
);
media = (
<Audio
src={attachment.get('url')}
alt={description}
lang={language}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
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'])}
sensitive={status.get('sensitive')}
visible={this.props.showMedia}
blurhash={attachment.get('blurhash')}
height={150}
onToggleVisibility={this.props.onToggleMediaVisibility}
/>
);
} 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');
const media = (status, quote = false) => {
if (pictureInPicture.get('inUse')) {
return <PictureInPicturePlaceholder aspectRatio={this.getAttachmentAspectRatio()} />;
} else if (status.get('media_attachments').size > 0) {
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 = (
<Video
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}
width={300}
height={150}
onOpenVideo={this.handleOpenVideo}
sensitive={status.get('sensitive')}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
/>
);
} else {
media = (
<MediaGallery
standalone
sensitive={status.get('sensitive')}
media={status.get('media_attachments')}
lang={language}
height={300}
onOpenMedia={this.props.onOpenMedia}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
/>
);
return (
<Audio
src={attachment.get('url')}
alt={description}
lang={language}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
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'])}
sensitive={status.get('sensitive')}
visible={this.props.showMedia}
blurhash={attachment.get('blurhash')}
height={150}
onToggleVisibility={this.props.onToggleMediaVisibility}
quote={quote}
/>
);
} 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');
return (
<Video
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}
width={300}
height={150}
onOpenVideo={this.handleOpenVideo}
sensitive={status.get('sensitive')}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
quote={quote}
/>
);
} else {
return (
<MediaGallery
standalone
sensitive={status.get('sensitive')}
media={status.get('media_attachments')}
lang={language}
height={300}
onOpenMedia={this.props.onOpenMedia}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
quote={quote}
/>
);
}
} 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} />;
}
} else if (status.get('spoiler_text').length === 0) {
media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
}
}
if (status.get('application')) {
applicationLink = <> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></>;
}
@ -305,7 +344,9 @@ class DetailedStatus extends ImmutablePureComponent {
{...statusContentProps}
/>
{media}
{media(status, false)}
{quote(status, false, quoteMuted, this.handleQuoteClick, this.handleExpandedQuoteToggle, identity, media, this.props)}
{expanded && hashtagBar}

View File

@ -27,6 +27,8 @@ import {
deleteStatus,
hideStatus,
revealStatus,
hideQuote,
revealQuote,
} from '../../../actions/statuses';
import { boostModal, deleteModal } from '../../../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../../../selectors';
@ -182,6 +184,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onQuoteToggleHidden (status) {
if (status.get('quote_hidden')) {
dispatch(revealQuote(status.get('id')));
} else {
dispatch(hideQuote(status.get('id')));
}
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus));

View File

@ -30,6 +30,7 @@ import { initBlockModal } from '../../actions/blocks';
import { initBoostModal } from '../../actions/boosts';
import {
replyCompose,
quoteCompose,
mentionCompose,
directCompose,
} from '../../actions/compose';
@ -60,6 +61,8 @@ import {
revealStatus,
translateStatus,
undoStatusTranslation,
hideQuote,
revealQuote,
} from '../../actions/statuses';
import ColumnHeader from '../../components/column_header';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
@ -84,6 +87,8 @@ const messages = defineMessages({
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
});
@ -216,6 +221,7 @@ class Status extends ImmutablePureComponent {
state = {
fullscreen: false,
showMedia: defaultMediaVisibility(this.props.status),
showQuoteMedia: defaultMediaVisibility(this.props.status ? this.props.status.get('quote', null) : null),
loadedStatusId: undefined,
};
@ -235,7 +241,11 @@ class Status extends ImmutablePureComponent {
}
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') });
this.setState({
showMedia: defaultMediaVisibility(nextProps.status),
showQuoteMedia: defaultMediaVisibility(nextProps.status.get('quote', null)),
loadedStatusId: nextProps.status.get('id'),
});
}
}
@ -243,6 +253,10 @@ class Status extends ImmutablePureComponent {
this.setState({ showMedia: !this.state.showMedia });
};
handleToggleQuoteMediaVisibility = () => {
this.setState({ showQuoteMedia: !this.state.showQuoteMedia });
}
handleFavouriteClick = (status) => {
const { dispatch } = this.props;
const { signedIn } = this.context.identity;
@ -302,6 +316,19 @@ class Status extends ImmutablePureComponent {
}
};
handleQuoteClick = (status) => {
let { askReplyConfirmation, dispatch, intl } = this.props;
if (askReplyConfirmation) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.quoteMessage),
confirm: intl.formatMessage(messages.quoteConfirm),
onConfirm: () => dispatch(quoteCompose(status, this.props.history)),
}));
} else {
dispatch(quoteCompose(status, this.props.history));
}
}
handleModalReblog = (status, privacy) => {
this.props.dispatch(reblog(status, privacy));
};
@ -417,6 +444,14 @@ class Status extends ImmutablePureComponent {
}
};
handleQuoteToggleHidden = (status) => {
if (status.get('quote_hidden')) {
this.props.dispatch(revealQuote(status.get('id')));
} else {
this.props.dispatch(hideQuote(status.get('id')));
}
}
handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds } = this.props;
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
@ -707,9 +742,12 @@ class Status extends ImmutablePureComponent {
onOpenMedia={this.handleOpenMedia}
onToggleHidden={this.handleToggleHidden}
onTranslate={this.handleTranslate}
onQuoteToggleHidden={this.handleQuoteToggleHidden}
domain={domain}
showMedia={this.state.showMedia}
showQuoteMedia={this.state.showQuoteMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
onToggleQuoteMediaVisibility={this.handleToggleQuoteMediaVisibility}
pictureInPicture={pictureInPicture}
/>
@ -719,6 +757,7 @@ class Status extends ImmutablePureComponent {
onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick}
onReblog={this.handleReblogClick}
onQuote={this.handleQuoteClick}
onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick}
onEdit={this.handleEditClick}

View File

@ -29,6 +29,13 @@ import ComposePanel from './compose_panel';
import DrawerLoading from './drawer_loading';
import NavigationPanel from './navigation_panel';
import { place_tab_bar_at_bottom } from 'mastodon/initial_state';
import { Link } from 'react-router-dom';
import { Icon } from 'mastodon/components/icon';
import TabsBar from './tabs_bar';
import { ReactComponent as EditIcon } from '@material-symbols/svg-600/outlined/edit.svg';
const componentMap = {
'COMPOSE': Compose,
'HOME': HomeTimeline,
@ -149,13 +156,40 @@ export default class ColumnsArea extends ImmutablePureComponent {
const { renderComposePanel } = this.state;
if (singleColumn) {
return (
<div className='columns-area__panels'>
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
{renderComposePanel && <ComposePanel />}
if (place_tab_bar_at_bottom) {
return (
<div className='columns-area__panels tab-ber-bottom'>
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
{renderComposePanel && <ComposePanel />}
</div>
</div>
<div className='columns-area__panels__main timeline'>
<div className='tabs-bar__wrapper'><TabsBarPortal /></div>
<div className='columns-area columns-area--mobile'>{children}</div>
</div>
<div className='columns-area__panels__main navber'>
{location.pathname !== '/publish' && <Link to='/publish' icon='Edit' className='button bottom_right'><EditIcon fill="white"/></Link>}
<TabsBar key='tabs' />
</div>
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational columns-area__panels__pane-tab-ber'>
<div className='columns-area__panels__pane__inner'>
<NavigationPanel />
</div>
</div>
</div>
);
} else {
return (
<div className='columns-area__panels'>
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
{renderComposePanel && <ComposePanel />}
</div>
</div>
<div className='columns-area__panels__main'>
<div className='tabs-bar__wrapper'><TabsBarPortal /></div>
@ -168,7 +202,8 @@ export default class ColumnsArea extends ImmutablePureComponent {
</div>
</div>
</div>
);
);
}
}
return (

View File

@ -0,0 +1,59 @@
import PropTypes from 'prop-types';
import { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import ColumnLink from './column_link';
import NotificationsCounterIcon from './notifications_counter_icon';
import { ReactComponent as HomeIcon } from '@material-symbols/svg-600/outlined/home-fill.svg';
import { ReactComponent as MoreHorizIcon } from '@material-symbols/svg-600/outlined/more_horiz.svg';
import { ReactComponent as PublicIcon } from '@material-symbols/svg-600/outlined/public.svg';
import { ReactComponent as SearchIcon } from '@material-symbols/svg-600/outlined/search.svg';
const messages = defineMessages({
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
explore: { id: 'explore.title', defaultMessage: 'Explore' },
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' },
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
gettingStarted: { id: 'getting_started.heading', defaultMessage: 'Getting Started' },
});
class TabsBar extends Component {
static contextTypes = {
router: PropTypes.object.isRequired,
identity: PropTypes.object.isRequired,
};
static propTypes = {
intl: PropTypes.object.isRequired,
};
isFirehoseActive = (match, location) => {
return match || location.pathname.startsWith('/public');
};
render () {
const { intl } = this.props;
return (
<div className='tabs-bar__wrapper'>
<ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} text={intl.formatMessage(messages.home)} />
<ColumnLink transparent to='/notifications' icon={<NotificationsCounterIcon className='column-link__icon' />} text={intl.formatMessage(messages.notifications)} />
<ColumnLink transparent to='/public/local' iconComponent={PublicIcon} isActive={this.isFirehoseActive} icon='globe' text={intl.formatMessage(messages.firehose)} />
<ColumnLink transparent to='/search' icon='search' iconComponent={SearchIcon} text={intl.formatMessage(messages.search)} />
<ColumnLink transparent to='/getting-started' icon='bars' iconComponent={MoreHorizIcon} text={intl.formatMessage(messages.gettingStarted)} />
</div>
);
}
}
export default injectIntl(TabsBar);

View File

@ -133,6 +133,7 @@ class Video extends PureComponent {
autoPlay: PropTypes.bool,
volume: PropTypes.number,
muted: PropTypes.bool,
quote: PropTypes.bool,
componentIndex: PropTypes.number,
autoFocus: PropTypes.bool,
};

View File

@ -42,6 +42,7 @@
* @property {boolean} use_blurhash
* @property {boolean=} use_pending_items
* @property {string} version
* @property {boolean} place_tab_bar_at_bottom
* @property {string} sso_redirect
*/
@ -107,6 +108,7 @@ export const languages = initialState?.languages;
export const criticalUpdatesPending = initialState?.critical_updates_pending;
// @ts-expect-error
export const statusPageUrl = getMeta('status_page_url');
export const place_tab_bar_at_bottom = getMeta('place_tab_bar_at_bottom');
export const sso_redirect = getMeta('sso_redirect');
export default initialState;

View File

@ -180,6 +180,8 @@
"confirmations.mute.confirm": "Mute",
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.quote.confirm": "Quote",
"confirmations.quote.message": "Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.redraft.confirm": "Delete & redraft",
"confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.",
"confirmations.reply.confirm": "Reply",
@ -638,6 +640,7 @@
"status.block": "Block @{name}",
"status.bookmark": "Bookmark",
"status.cancel_reblog_private": "Unboost",
"status.cannot_quote": "This post cannot be quoted",
"status.cannot_reblog": "This post cannot be boosted",
"status.copy": "Copy link to post",
"status.delete": "Delete",
@ -662,9 +665,11 @@
"status.more": "More",
"status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation",
"status.muted_quote": "Muted quote",
"status.open": "Expand this post",
"status.pin": "Pin on profile",
"status.pinned": "Pinned post",
"status.quote": "Quote",
"status.read_more": "Read more",
"status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility",
@ -688,6 +693,7 @@
"status.translate": "Translate",
"status.translated_from_with": "Translated from {lang} using {provider}",
"status.uncached_media_warning": "Preview not available",
"status.unlisted_quote": "Unlisted quote",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",

View File

@ -23,13 +23,13 @@
"account.cancel_follow_request": "フォローリクエストの取り消し",
"account.copy": "プロフィールへのリンクをコピー",
"account.direct": "@{name}さんに非公開でメンション",
"account.disable_notifications": "@{name}さんの投稿時の通知を停止",
"account.disable_notifications": "@{name}さんのトゥート時の通知を停止",
"account.domain_blocked": "ドメインブロック中",
"account.edit_profile": "プロフィール編集",
"account.enable_notifications": "@{name}さんの投稿時に通知",
"account.enable_notifications": "@{name}さんのトゥート時に通知",
"account.endorse": "プロフィールで紹介する",
"account.featured_tags.last_status_at": "最終投稿 {date}",
"account.featured_tags.last_status_never": "投稿がありません",
"account.featured_tags.last_status_at": "最終トゥート {date}",
"account.featured_tags.last_status_never": "トゥートがありません",
"account.featured_tags.title": "{name}の注目ハッシュタグ",
"account.follow": "フォロー",
"account.follow_back": "フォローバック",
@ -56,14 +56,14 @@
"account.mutual": "相互フォロー中",
"account.no_bio": "説明が提供されていません。",
"account.open_original_page": "元のページを開く",
"account.posts": "投稿",
"account.posts_with_replies": "投稿と返信",
"account.posts": "トゥート",
"account.posts_with_replies": "トゥートと返信",
"account.report": "@{name}さんを通報",
"account.requested": "フォロー承認待ちです。クリックしてキャンセル",
"account.requested_follow": "{name}さんがあなたにフォローリクエストしました",
"account.share": "@{name}さんのプロフィールを共有する",
"account.show_reblogs": "@{name}さんからのブーストを表示",
"account.statuses_counter": "{counter} 投稿",
"account.statuses_counter": "{counter} トゥート",
"account.unblock": "@{name}さんのブロックを解除",
"account.unblock_domain": "{domain}のブロックを解除",
"account.unblock_short": "ブロック解除",
@ -121,7 +121,7 @@
"column.lists": "リスト",
"column.mutes": "ミュートしたユーザー",
"column.notifications": "通知",
"column.pins": "固定された投稿",
"column.pins": "固定されたトゥート",
"column.public": "連合タイムライン",
"column_back_button.label": "戻る",
"column_header.hide_settings": "設定を隠す",
@ -136,21 +136,31 @@
"community.column_settings.remote_only": "リモートのみ表示",
"compose.language.change": "言語を変更",
"compose.language.search": "言語を検索...",
"compose.published.body": "投稿されました!",
"compose.published.body": "トゥートされました!",
"compose.published.open": "開く",
"compose.saved.body": "変更を保存しました。",
"compose_form.direct_message_warning_learn_more": "もっと詳しく",
"compose_form.encryption_warning": "Mastodonの投稿はエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。",
"compose_form.hashtag_warning": "この投稿は公開設定ではないのでハッシュタグの一覧に表示されません。公開投稿だけがハッシュタグで検索できます。",
"compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。",
"compose_form.encryption_warning": "Mastodonのトゥートはエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。",
"compose_form.hashtag_warning": "このトゥートは公開設定ではないのでハッシュタグの一覧に表示されません。公開トゥートだけがハッシュタグで検索できます。",
"compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定のトゥートを見ることができます。",
"compose_form.lock_disclaimer.lock": "承認制",
"compose_form.placeholder": "今なにしてる?",
"compose_form.poll.duration": "アンケート期間",
"compose_form.poll.switch_to_multiple": "複数選択に変更",
"compose_form.poll.switch_to_single": "単一選択に変更",
"compose_form.publish_form": "投稿",
"compose_form.publish": "トゥート",
"compose_form.publish_form": "トゥート",
"compose_form.publish_loud": "{publish}",
"compose_form.save_changes": "変更を保存",
"compose_form.sensitive.hide": "メディアを閲覧注意にする",
"compose_form.sensitive.marked": "メディアに閲覧注意が設定されています",
"compose_form.sensitive.unmarked": "メディアに閲覧注意が設定されていません",
"compose_form.spoiler.marked": "本文は警告の後ろに隠されます",
"compose_form.spoiler.unmarked": "本文は隠されていません",
"compose_form.spoiler_placeholder": "ここに警告を書いてください",
"compose_form.utilBtns_goji": "誤字盛!",
"compose_form.utilBtns_harukin": "はるきん焼却",
"compose_form.utilBtns_risa": "りさ姉",
"confirmation_modal.cancel": "キャンセル",
"confirmations.block.block_and_report": "ブロックし通報",
"confirmations.block.confirm": "ブロック",
@ -170,10 +180,12 @@
"confirmations.logout.confirm": "ログアウト",
"confirmations.logout.message": "本当にログアウトしますか?",
"confirmations.mute.confirm": "ミュート",
"confirmations.mute.explanation": "これにより相手の投稿と返信は見えなくなりますが、相手はあなたをフォローし続け投稿を見ることができます。",
"confirmations.mute.explanation": "これにより相手のトゥートと返信は見えなくなりますが、相手はあなたをフォローし続けトゥートを見ることができます。",
"confirmations.mute.message": "本当に{name}さんをミュートしますか?",
"confirmations.quote.confirm": "引用",
"confirmations.quote.message": "今引用すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
"confirmations.redraft.confirm": "削除して下書きに戻す",
"confirmations.redraft.message": "投稿を削除して下書きに戻します。この投稿へのお気に入り登録やブーストは失われ、返信は孤立することになります。よろしいですか?",
"confirmations.redraft.message": "トゥートを削除して下書きに戻します。このトゥートへのお気に入り登録やブーストは失われ、返信は孤立することになります。よろしいですか?",
"confirmations.reply.confirm": "返信",
"confirmations.reply.message": "今返信すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
"confirmations.unfollow.confirm": "フォロー解除",
@ -191,12 +203,12 @@
"directory.recently_active": "最近の活動順",
"disabled_account_banner.account_settings": "アカウント設定",
"disabled_account_banner.text": "あなたのアカウント『{disabledAccount}』は現在無効になっています。",
"dismissable_banner.community_timeline": "これらは{domain}がホストしている人たちの最新の公開投稿です。",
"dismissable_banner.community_timeline": "これらは{domain}がホストしている人たちの最新の公開トゥートです。",
"dismissable_banner.dismiss": "閉じる",
"dismissable_banner.explore_links": "ネットワーク上で話題になっているニュースです。たくさんのユーザーにシェアされた記事ほど上位に表示されます。",
"dismissable_banner.explore_statuses": "ネットワーク上で注目を集めている投稿です。ブーストやお気に入り登録の多い新しい投稿が上位に表示されます。",
"dismissable_banner.explore_statuses": "ネットワーク上で注目を集めているトゥートです。ブーストやお気に入り登録の多い新しいトゥートが上位に表示されます。",
"dismissable_banner.explore_tags": "ネットワーク上でトレンドになっているハッシュタグです。たくさんのユーザーに使われたタグほど上位に表示されます。",
"dismissable_banner.public_timeline": "{domain}のユーザーがリモートフォローしているアカウントからの公開投稿のタイムラインです。",
"dismissable_banner.public_timeline": "{domain}のユーザーがリモートフォローしているアカウントからの公開トゥートのタイムラインです。",
"embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。",
"embed.preview": "表示例:",
"emoji_button.activity": "活動",
@ -216,7 +228,7 @@
"emoji_button.travel": "旅行と場所",
"empty_column.account_hides_collections": "このユーザーはこの情報を開示しないことにしています。",
"empty_column.account_suspended": "アカウントは停止されています",
"empty_column.account_timeline": "投稿がありません!",
"empty_column.account_timeline": "トゥートがありません!",
"empty_column.account_unavailable": "プロフィールは利用できません",
"empty_column.blocks": "まだ誰もブロックしていません。",
"empty_column.bookmarked_statuses": "まだ何もブックマーク登録していません。ブックマーク登録するとここに表示されます。",
@ -224,17 +236,17 @@
"empty_column.direct": "非公開の返信はまだありません。非公開でやりとりをするとここに表示されます。",
"empty_column.domain_blocks": "ブロックしているドメインはありません。",
"empty_column.explore_statuses": "まだ何もありません。後で確認してください。",
"empty_column.favourited_statuses": "お気に入りの投稿はまだありません。お気に入りに登録すると、ここに表示されます。",
"empty_column.favourites": "まだ誰もこの投稿をお気に入りに登録していません。お気に入りに登録されると、ここに表示されます。",
"empty_column.favourited_statuses": "お気に入りのトゥートはまだありません。お気に入りに登録すると、ここに表示されます。",
"empty_column.favourites": "まだ誰もこのトゥートをお気に入りに登録していません。お気に入りに登録されると、ここに表示されます。",
"empty_column.follow_requests": "まだフォローリクエストを受けていません。フォローリクエストを受けるとここに表示されます。",
"empty_column.followed_tags": "まだハッシュタグをフォローしていません。フォローするとここに表示されます。",
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
"empty_column.home": "ホームタイムラインはまだ空っぽです。だれかをフォローして埋めてみましょう。",
"empty_column.list": "このリストにはまだなにもありません。このリストのメンバーが新しい投稿をするとここに表示されます。",
"empty_column.list": "このリストにはまだなにもありません。このリストのメンバーが新しいトゥートをするとここに表示されます。",
"empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。",
"empty_column.mutes": "まだ誰もミュートしていません。",
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
"empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう",
"empty_column.public": "ここにはまだ何もありません! 公開で何かをトゥートしたり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう",
"error.unexpected_crash.explanation": "不具合かブラウザの互換性問題のため、このページを正しく表示できませんでした。",
"error.unexpected_crash.explanation_addons": "このページは正しく表示できませんでした。このエラーはブラウザのアドオンや自動翻訳ツールによって引き起こされることがあります。",
"error.unexpected_crash.next_steps": "ページの再読み込みをお試しください。それでも解決しない場合、別のブラウザかアプリを使えば使用できることがあります。",
@ -245,24 +257,24 @@
"explore.suggested_follows": "ユーザー",
"explore.title": "探索する",
"explore.trending_links": "ニュース",
"explore.trending_statuses": "投稿",
"explore.trending_statuses": "トゥート",
"explore.trending_tags": "ハッシュタグ",
"filter_modal.added.context_mismatch_explanation": "このフィルターカテゴリーはあなたがアクセスした投稿のコンテキストには適用されません。この投稿のコンテキストでもフィルターを適用するにはフィルターを編集する必要があります。",
"filter_modal.added.context_mismatch_explanation": "このフィルターカテゴリーはあなたがアクセスしたトゥートのコンテキストには適用されません。このトゥートのコンテキストでもフィルターを適用するにはフィルターを編集する必要があります。",
"filter_modal.added.context_mismatch_title": "コンテキストが一致しません!",
"filter_modal.added.expired_explanation": "このフィルターカテゴリーは有効期限が切れています。適用するには有効期限を更新してください。",
"filter_modal.added.expired_title": "フィルターの有効期限が切れています!",
"filter_modal.added.review_and_configure": "このフィルターカテゴリーを確認して設定するには、{settings_link}に移動します。",
"filter_modal.added.review_and_configure_title": "フィルター設定",
"filter_modal.added.settings_link": "設定",
"filter_modal.added.short_explanation": "この投稿はフィルターカテゴリー『{title}』に追加されました。",
"filter_modal.added.short_explanation": "このトゥートはフィルターカテゴリー『{title}』に追加されました。",
"filter_modal.added.title": "フィルターを追加しました!",
"filter_modal.select_filter.context_mismatch": "このコンテキストには当てはまりません",
"filter_modal.select_filter.expired": "期限切れ",
"filter_modal.select_filter.prompt_new": "新しいカテゴリー: {name}",
"filter_modal.select_filter.search": "検索または新規作成",
"filter_modal.select_filter.subtitle": "既存のカテゴリーを使用するか新規作成します",
"filter_modal.select_filter.title": "この投稿をフィルターする",
"filter_modal.title.status": "投稿をフィルターする",
"filter_modal.select_filter.title": "このトゥートをフィルターする",
"filter_modal.title.status": "トゥートをフィルターする",
"firehose.all": "すべて",
"firehose.local": "このサーバー",
"firehose.remote": "ほかのサーバー",
@ -289,7 +301,7 @@
"hashtag.column_settings.tag_mode.any": "いずれかを含む",
"hashtag.column_settings.tag_mode.none": "これらを除く",
"hashtag.column_settings.tag_toggle": "このカラムに追加のタグを含める",
"hashtag.counter_by_accounts": "{count, plural, other {{counter}人投稿}}",
"hashtag.counter_by_accounts": "{count, plural, other {{counter}人トゥート}}",
"hashtag.counter_by_uses": "{count, plural, other {{counter}件}}",
"hashtag.counter_by_uses_today": "今日{count, plural, other {{counter}件}}",
"hashtag.follow": "ハッシュタグをフォローする",
@ -300,17 +312,17 @@
"home.column_settings.basic": "基本設定",
"home.column_settings.show_reblogs": "ブースト表示",
"home.column_settings.show_replies": "返信表示",
"home.explore_prompt.body": "ユーザーやハッシュタグをフォローすると、この「ホーム」タイムラインに投稿やブーストが流れるようになります。タイムラインをもう少しにぎやかにしてみませんか?",
"home.explore_prompt.body": "ユーザーやハッシュタグをフォローすると、この「ホーム」タイムラインにトゥートやブーストが流れるようになります。タイムラインをもう少しにぎやかにしてみませんか?",
"home.explore_prompt.title": "Mastodonのタイムラインへようこそ。",
"home.hide_announcements": "お知らせを隠す",
"home.pending_critical_update.body": "速やかにMastodonサーバーをアップデートしてください。",
"home.pending_critical_update.link": "詳細",
"home.pending_critical_update.title": "緊急のセキュリティアップデートがあります",
"home.show_announcements": "お知らせを表示",
"interaction_modal.description.favourite": "Mastodonのアカウントがあれば投稿をお気に入り登録して投稿者に気持ちを伝えたり、あとで見返すことができます。",
"interaction_modal.description.follow": "Mastodonのアカウントで{name}さんをフォローしてホームフィードで投稿を受け取れます。",
"interaction_modal.description.reblog": "Mastodonのアカウントでこの投稿をブーストして自分のフォロワーに共有できます。",
"interaction_modal.description.reply": "Mastodonのアカウントでこの投稿に反応できます。",
"interaction_modal.description.favourite": "Mastodonのアカウントがあればトゥートをお気に入り登録してトゥート者に気持ちを伝えたり、あとで見返すことができます。",
"interaction_modal.description.follow": "Mastodonのアカウントで{name}さんをフォローしてホームフィードでトゥートを受け取れます。",
"interaction_modal.description.reblog": "Mastodonのアカウントでこのトゥートをブーストして自分のフォロワーに共有できます。",
"interaction_modal.description.reply": "Mastodonのアカウントでこのトゥートに反応できます。",
"interaction_modal.login.action": "サーバーに移動",
"interaction_modal.login.prompt": "登録したサーバーのドメイン (例: mastodon.social)",
"interaction_modal.no_account_yet": "Mastodonにアカウントがない場合は",
@ -318,10 +330,10 @@
"interaction_modal.on_this_server": "このサーバー",
"interaction_modal.sign_in": "このサーバーにアカウントがなくても、ほかのサーバーや互換性のあるプラットフォームのアカウントを使用できます。",
"interaction_modal.sign_in_hint": "ワンポイント: ここでは自分のアカウントのドメインを入力します。うまくいかない場合はドメインまで含めた完全なユーザー名を入力してみてください (例: @Mastodon@mastodon.social)。ドメインやユーザー名は登録完了時のメールに記載されています。",
"interaction_modal.title.favourite": "{name}さんの投稿をお気に入り登録",
"interaction_modal.title.favourite": "{name}さんのトゥートをお気に入り登録",
"interaction_modal.title.follow": "{name}さんをフォロー",
"interaction_modal.title.reblog": "{name}さんの投稿をブースト",
"interaction_modal.title.reply": "{name}さんの投稿にリプライ",
"interaction_modal.title.reblog": "{name}さんのトゥートをブースト",
"interaction_modal.title.reply": "{name}さんのトゥートにリプライ",
"intervals.full.days": "{number}日",
"intervals.full.hours": "{number}時間",
"intervals.full.minutes": "{number}分",
@ -329,12 +341,12 @@
"keyboard_shortcuts.blocked": "ブロックしたユーザーのリストを開く",
"keyboard_shortcuts.boost": "ブースト",
"keyboard_shortcuts.column": "左からn番目のカラムの最新に移動",
"keyboard_shortcuts.compose": "投稿の入力欄に移動",
"keyboard_shortcuts.compose": "トゥートの入力欄に移動",
"keyboard_shortcuts.description": "説明",
"keyboard_shortcuts.direct": "非公開の返信のカラムを開く",
"keyboard_shortcuts.down": "カラム内一つ下に移動",
"keyboard_shortcuts.enter": "投稿の詳細を表示",
"keyboard_shortcuts.favourite": "お気に入りの投稿",
"keyboard_shortcuts.enter": "トゥートの詳細を表示",
"keyboard_shortcuts.favourite": "お気に入りのトゥート",
"keyboard_shortcuts.favourites": "お気に入りリストを開く",
"keyboard_shortcuts.federated": "連合タイムラインを開く",
"keyboard_shortcuts.heading": "キーボードショートカット",
@ -347,7 +359,7 @@
"keyboard_shortcuts.my_profile": "自分のプロフィールを開く",
"keyboard_shortcuts.notifications": "通知カラムを開く",
"keyboard_shortcuts.open_media": "メディアを開く",
"keyboard_shortcuts.pinned": "固定した投稿のリストを開く",
"keyboard_shortcuts.pinned": "固定したトゥートのリストを開く",
"keyboard_shortcuts.profile": "プロフィールを開く",
"keyboard_shortcuts.reply": "返信",
"keyboard_shortcuts.requests": "フォローリクエストのリストを開く",
@ -356,8 +368,8 @@
"keyboard_shortcuts.start": "\"スタート\" カラムを開く",
"keyboard_shortcuts.toggle_hidden": "CWで隠れた文を見る/隠す",
"keyboard_shortcuts.toggle_sensitivity": "非表示のメディアを見る/隠す",
"keyboard_shortcuts.toot": "新規投稿",
"keyboard_shortcuts.unfocus": "投稿の入力欄・検索欄から離れる",
"keyboard_shortcuts.toot": "新規トゥート",
"keyboard_shortcuts.unfocus": "トゥートの入力欄・検索欄から離れる",
"keyboard_shortcuts.up": "カラム内一つ上に移動",
"lightbox.close": "閉じる",
"lightbox.compress": "画像ビューボックスを閉じる",
@ -372,7 +384,7 @@
"lists.delete": "リストを削除",
"lists.edit": "リストを編集",
"lists.edit.submit": "タイトルを変更",
"lists.exclusive": "ホームタイムラインからこれらの投稿を非表示にする",
"lists.exclusive": "ホームタイムラインからこれらのトゥートを非表示にする",
"lists.new.create": "リストを作成",
"lists.new.title_placeholder": "新規リスト名",
"lists.replies_policy.followed": "フォロー中のユーザー全員",
@ -393,7 +405,7 @@
"navigation_bar.blocks": "ブロックしたユーザー",
"navigation_bar.bookmarks": "ブックマーク",
"navigation_bar.community_timeline": "ローカルタイムライン",
"navigation_bar.compose": "投稿の新規作成",
"navigation_bar.compose": "トゥートの新規作成",
"navigation_bar.direct": "非公開の返信",
"navigation_bar.discover": "見つける",
"navigation_bar.domain_blocks": "ブロックしたドメイン",
@ -408,7 +420,7 @@
"navigation_bar.mutes": "ミュートしたユーザー",
"navigation_bar.opened_in_classic_interface": "投稿やプロフィールを直接開いた場合は一時的に標準UIで表示されます。",
"navigation_bar.personal": "個人用",
"navigation_bar.pins": "固定した投稿",
"navigation_bar.pins": "固定したトゥート",
"navigation_bar.preferences": "ユーザー設定",
"navigation_bar.public_timeline": "連合タイムライン",
"navigation_bar.search": "検索",
@ -416,15 +428,15 @@
"not_signed_in_indicator.not_signed_in": "この機能を使うにはログインする必要があります。",
"notification.admin.report": "{name}さんが{target}さんを通報しました",
"notification.admin.sign_up": "{name}さんがサインアップしました",
"notification.favourite": "{name}さんがお気に入りしました",
"notification.favourite": "{name}さんがあなたのトゥートに╰( ^o^)╮-=ニ=一=三★しました",
"notification.follow": "{name}さんにフォローされました",
"notification.follow_request": "{name}さんがあなたにフォローリクエストしました",
"notification.mention": "{name}さんがあなたに返信しました",
"notification.own_poll": "アンケートが終了しました",
"notification.poll": "アンケートが終了しました",
"notification.reblog": "{name}さんがあなたの投稿をブーストしました",
"notification.status": "{name}さんが投稿しました",
"notification.update": "{name}さんが投稿を編集しました",
"notification.reblog": "{name}さんがあなたのトゥートをブーストしました",
"notification.status": "{name}さんがトゥートしました",
"notification.update": "{name}さんがトゥートを編集しました",
"notifications.clear": "通知を消去",
"notifications.clear_confirmation": "本当に通知を消去しますか?",
"notifications.column_settings.admin.report": "新しい通報:",
@ -442,7 +454,7 @@
"notifications.column_settings.reblog": "ブースト:",
"notifications.column_settings.show": "カラムに表示",
"notifications.column_settings.sound": "通知音を再生",
"notifications.column_settings.status": "新しい投稿:",
"notifications.column_settings.status": "新しいトゥート:",
"notifications.column_settings.unread_notifications.category": "未読の通知:",
"notifications.column_settings.unread_notifications.highlight": "未読の通知を強調表示",
"notifications.column_settings.update": "編集:",
@ -491,7 +503,7 @@
"onboarding.steps.follow_people.body": "ユーザーをフォローしてみましょう。これがMastodonを楽しむ基本です。",
"onboarding.steps.follow_people.title": "ホームタイムラインを埋める",
"onboarding.steps.publish_status.body": "試しになにか書いてみましょう。写真、ビデオ、アンケートなど、なんでも大丈夫です {emoji}",
"onboarding.steps.publish_status.title": "はじめての投稿",
"onboarding.steps.publish_status.title": "はじめてのトゥート",
"onboarding.steps.setup_profile.body": "ほかのユーザーが親しみやすいように、プロフィールを整えましょう。",
"onboarding.steps.setup_profile.title": "プロフィールを完成させる",
"onboarding.steps.share_profile.body": "Mastodonのアカウントをほかの人に紹介しましょう。",
@ -534,7 +546,7 @@
"relative_time.today": "今日",
"reply_indicator.cancel": "キャンセル",
"report.block": "ブロック",
"report.block_explanation": "相手の投稿が表示されなくなります。相手はあなたの投稿を見ることやフォローすることができません。相手はブロックされていることがわかります。",
"report.block_explanation": "相手のトゥートが表示されなくなります。相手はあなたのトゥートを見ることやフォローすることができません。相手はブロックされていることがわかります。",
"report.categories.legal": "法令違反",
"report.categories.other": "その他",
"report.categories.spam": "スパム",
@ -542,13 +554,13 @@
"report.category.subtitle": "近いものを選択してください",
"report.category.title": "この{type}について教えてください",
"report.category.title_account": "プロフィール",
"report.category.title_status": "投稿",
"report.category.title_status": "トゥート",
"report.close": "完了",
"report.comment.title": "その他に私たちに伝えておくべき事はありますか?",
"report.forward": "{target}に転送する",
"report.forward_hint": "このアカウントは別のサーバーに所属しています。通報内容を匿名で転送しますか?",
"report.mute": "ミュート",
"report.mute_explanation": "相手の投稿は表示されなくなります。相手は引き続きあなたをフォローして、あなたの投稿を表示することができますが、ミュートされていることはわかりません。",
"report.mute_explanation": "相手のトゥートは表示されなくなります。相手は引き続きあなたをフォローして、あなたのトゥートを表示することができますが、ミュートされていることはわかりません。",
"report.next": "次へ",
"report.placeholder": "追加コメント",
"report.reasons.dislike": "興味がありません",
@ -564,7 +576,7 @@
"report.rules.subtitle": "当てはまるものをすべて選んでください:",
"report.rules.title": "どのルールに違反していますか?",
"report.statuses.subtitle": "当てはまるものをすべて選んでください:",
"report.statuses.title": "この通報を裏付けるような投稿はありますか?",
"report.statuses.title": "この通報を裏付けるようなトゥートはありますか?",
"report.submit": "通報する",
"report.target": "{target}さんを通報する",
"report.thanks.take_action": "次のような方法はいかがでしょうか?",
@ -572,8 +584,8 @@
"report.thanks.title": "見えないようにしたいですか?",
"report.thanks.title_actionable": "ご報告ありがとうございます、追って確認します。",
"report.unfollow": "@{name}さんのフォローを解除",
"report.unfollow_explanation": "このアカウントをフォローしています。ホームフィードに彼らの投稿を表示しないようにするには、彼らのフォローを外してください。",
"report_notification.attached_statuses": "{count, plural, one {{count}件の投稿} other {{count}件の投稿}}が添付されました。",
"report.unfollow_explanation": "このアカウントをフォローしています。ホームフィードに彼らのトゥートを表示しないようにするには、彼らのフォローを外してください。",
"report_notification.attached_statuses": "{count, plural, one {{count}件のトゥート} other {{count}件のトゥート}}が添付されました。",
"report_notification.categories.legal": "法令違反",
"report_notification.categories.other": "その他",
"report_notification.categories.spam": "スパム",
@ -585,7 +597,7 @@
"search.quick_action.go_to_account": "プロフィール {x} を見る",
"search.quick_action.go_to_hashtag": "ハッシュタグ {x} を見る",
"search.quick_action.open_url": "MastodonでURLを開く",
"search.quick_action.status_search": "{x}に該当する投稿",
"search.quick_action.status_search": "{x}に該当するトゥート",
"search.search_or_paste": "検索またはURLを入力",
"search_popout.full_text_search_disabled_message": "{domain}では利用できません。",
"search_popout.full_text_search_logged_out_message": "ログイン時のみ利用できます。",
@ -600,7 +612,7 @@
"search_results.hashtags": "ハッシュタグ",
"search_results.nothing_found": "この検索条件では何も見つかりませんでした",
"search_results.see_all": "すべて表示",
"search_results.statuses": "投稿",
"search_results.statuses": "トゥート",
"search_results.title": "『{q}』の検索結果",
"server_banner.about_active_users": "過去30日間にこのサーバーを使用している人 (月間アクティブユーザー)",
"server_banner.active_users": "人のアクティブユーザー",
@ -611,40 +623,43 @@
"sign_in_banner.create_account": "アカウント作成",
"sign_in_banner.sign_in": "ログイン",
"sign_in_banner.sso_redirect": "ログインまたは登録",
"sign_in_banner.text": "アカウントがあればユーザーやハッシュタグをフォローしたり、投稿のお気に入り登録やブースト、投稿への返信ができます。別のサーバーのユーザーとの交流も可能です。",
"sign_in_banner.text": "アカウントがあればユーザーやハッシュタグをフォローしたり、トゥートのお気に入り登録やブースト、トゥートへの返信ができます。別のサーバーのユーザーとの交流も可能です。",
"status.admin_account": "@{name}さんのモデレーション画面を開く",
"status.admin_domain": "{domain}のモデレーション画面を開く",
"status.admin_status": "この投稿をモデレーション画面で開く",
"status.admin_status": "このトゥートをモデレーション画面で開く",
"status.block": "@{name}さんをブロック",
"status.bookmark": "ブックマーク",
"status.cancel_reblog_private": "ブースト解除",
"status.cannot_reblog": "この投稿はブーストできません",
"status.copy": "投稿へのリンクをコピー",
"status.cannot_quote": "このトゥートは引用できません",
"status.cannot_reblog": "このトゥートはブーストできません",
"status.copy": "トゥートへのリンクをコピー",
"status.delete": "削除",
"status.detailed_status": "詳細な会話ビュー",
"status.direct": "@{name}さんに非公開で投稿",
"status.direct": "@{name}さんに非公開でトゥート",
"status.direct_indicator": "非公開の返信",
"status.edit": "編集",
"status.edited": "{date}に編集",
"status.edited_x_times": "{count}回編集",
"status.embed": "埋め込み",
"status.favourite": "お気に入り",
"status.filter": "この投稿をフィルターする",
"status.filter": "このトゥートをフィルターする",
"status.filtered": "フィルターされました",
"status.hide": "投稿を非表示",
"status.hide": "トゥートを非表示",
"status.history.created": "{name}さんが{date}に作成",
"status.history.edited": "{name}さんが{date}に編集",
"status.load_more": "もっと見る",
"status.media.open": "クリックして開く",
"status.media.show": "クリックして表示",
"status.media_hidden": "非表示のメディア",
"status.mention": "@{name}さんに投稿",
"status.mention": "@{name}さんにトゥート",
"status.more": "もっと見る",
"status.mute": "@{name}さんをミュート",
"status.mute_conversation": "会話をミュート",
"status.muted_quote": "ミュートされた引用",
"status.open": "詳細を表示",
"status.pin": "プロフィールに固定表示",
"status.pinned": "固定された投稿",
"status.pinned": "固定されたトゥート",
"status.quote": "引用",
"status.read_more": "もっと見る",
"status.reblog": "ブースト",
"status.reblog_private": "ブースト",
@ -664,13 +679,13 @@
"status.show_more": "もっと見る",
"status.show_more_all": "全て見る",
"status.show_original": "原文を表示",
"status.title.with_attachments": "{user}さんの投稿 {attachmentCount, plural, other {({attachmentCount}件のメディア)}}",
"status.title.with_attachments": "{user}さんのトゥート {attachmentCount, plural, other {({attachmentCount}件のメディア)}}",
"status.translate": "翻訳",
"status.translated_from_with": "{provider}を使って{lang}から翻訳",
"status.uncached_media_warning": "プレビューは使用できません",
"status.unmute_conversation": "会話のミュートを解除",
"status.unpin": "プロフィールへの固定を解除",
"subscribed_languages.lead": "選択した言語の投稿だけがホームとリストのタイムラインに表示されます。全ての言語の投稿を受け取る場合は全てのチェックを外して下さい。",
"subscribed_languages.lead": "選択した言語のトゥートだけがホームとリストのタイムラインに表示されます。全ての言語のトゥートを受け取る場合は全てのチェックを外して下さい。",
"subscribed_languages.save": "変更を保存",
"subscribed_languages.target": "{target}さんの購読言語を変更します",
"tabs_bar.home": "ホーム",
@ -683,10 +698,10 @@
"timeline_hint.remote_resource_not_displayed": "他のサーバーの{resource}は表示されません。",
"timeline_hint.resources.followers": "フォロワー",
"timeline_hint.resources.follows": "フォロー",
"timeline_hint.resources.statuses": "以前の投稿",
"timeline_hint.resources.statuses": "以前のトゥート",
"trends.counter_by_accounts": "過去{days, plural, one {{days}日} other {{days}日}}に{count, plural, one {{counter}人} other {{counter} 人}}",
"trends.trending_now": "トレンドタグ",
"ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
"ui.beforeunload": "Mastodonから離れると送信前のトゥートは失われます。",
"units.short.billion": "{count}B",
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",

View File

@ -6,6 +6,8 @@ import {
COMPOSE_CHANGE,
COMPOSE_REPLY,
COMPOSE_REPLY_CANCEL,
COMPOSE_QUOTE,
COMPOSE_QUOTE_CANCEL,
COMPOSE_DIRECT,
COMPOSE_MENTION,
COMPOSE_SUBMIT_REQUEST,
@ -67,6 +69,8 @@ const initialState = ImmutableMap({
caretPosition: null,
preselectDate: null,
in_reply_to: null,
quote_from: null,
quote_from_url: null,
is_composing: false,
is_submitting: false,
is_changing_upload: false,
@ -119,6 +123,8 @@ function clearAll(state) {
map.set('is_submitting', false);
map.set('is_changing_upload', false);
map.set('in_reply_to', null);
map.set('quote_from', null);
map.set('quote_from_url', null);
map.set('privacy', state.get('default_privacy'));
map.set('sensitive', state.get('default_sensitive'));
map.set('language', state.get('default_language'));
@ -250,6 +256,17 @@ const expiresInFromExpiresAt = expires_at => {
return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600;
};
const rejectQuoteAltText = html => {
const fragment = domParser.parseFromString(html, 'text/html').documentElement;
const quote_inline = fragment.querySelector('span.quote-inline');
if (quote_inline) {
quote_inline.remove();
}
return fragment.innerHTML;
};
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
prefix = prefix.toLowerCase();
if (suggestions.length < 4) {
@ -335,10 +352,20 @@ export default function compose(state = initialState, action) {
case COMPOSE_COMPOSING_CHANGE:
return state.set('is_composing', action.value);
case COMPOSE_REPLY:
case COMPOSE_QUOTE:
return state.withMutations(map => {
map.set('id', null);
map.set('in_reply_to', action.status.get('id'));
map.set('text', statusToTextMentions(state, action.status));
if (action.type === COMPOSE_REPLY) {
map.set('in_reply_to', action.status.get('id'));
map.set('quote_from', null);
map.set('quote_from_url', null);
map.set('text', statusToTextMentions(state, action.status));
} else {
map.set('in_reply_to', null);
map.set('quote_from', action.status.get('id'));
map.set('quote_from_url', action.status.get('url'));
map.set('text', '');
}
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
map.set('focusDate', new Date());
map.set('caretPosition', null);
@ -370,6 +397,7 @@ export default function compose(state = initialState, action) {
case COMPOSE_UPLOAD_CHANGE_REQUEST:
return state.set('is_changing_upload', true);
case COMPOSE_REPLY_CANCEL:
case COMPOSE_QUOTE_CANCEL:
case COMPOSE_RESET:
case COMPOSE_SUBMIT_SUCCESS:
return clearAll(state);
@ -468,8 +496,10 @@ export default function compose(state = initialState, action) {
}));
case REDRAFT:
return state.withMutations(map => {
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
map.set('text', action.raw_text || unescapeHTML(rejectQuoteAltText(expandMentions(action.status))));
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('media_attachments', action.status.get('media_attachments').map((media) => media.set('unattached', true)));
map.set('focusDate', new Date());

View File

@ -3,6 +3,7 @@ import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from '
import {
COMPOSE_MENTION,
COMPOSE_REPLY,
COMPOSE_QUOTE,
COMPOSE_DIRECT,
} from '../actions/compose';
import {
@ -45,6 +46,7 @@ export default function search(state = initialState, action) {
case SEARCH_SHOW:
return state.set('hidden', false);
case COMPOSE_REPLY:
case COMPOSE_QUOTE:
case COMPOSE_MENTION:
case COMPOSE_DIRECT:
return state.set('hidden', true);

View File

@ -26,6 +26,8 @@ import {
STATUS_TRANSLATE_UNDO,
STATUS_FETCH_REQUEST,
STATUS_FETCH_FAIL,
QUOTE_REVEAL,
QUOTE_HIDE,
} from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines';
@ -121,6 +123,14 @@ export default function statuses(state = initialState, action) {
});
case STATUS_COLLAPSE:
return state.setIn([action.id, 'collapsed'], action.isCollapsed);
case QUOTE_REVEAL:
return state.withMutations(map => {
action.ids.forEach(id => map.setIn([id, 'quote_hidden'], false));
});
case QUOTE_HIDE:
return state.withMutations(map => {
action.ids.forEach(id => map.setIn([id, 'quote_hidden'], true));
});
case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.references);
case STATUS_TRANSLATE_SUCCESS:

View File

@ -4,6 +4,7 @@ import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { toServerSideType } from 'mastodon/utils/filters';
import { me } from '../initial_state';
import {reblogRequest} from '../actions/interactions';
export { makeGetAccount } from "./accounts";
@ -21,22 +22,54 @@ export const makeGetStatus = () => {
[
(state, { id }) => state.getIn(['statuses', id]),
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id']), 'account'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'quote', 'account'])]),
(state, { id }) => state.getIn(['relationships', state.getIn(['statuses', id, 'account'])]),
(state, { id }) => state.getIn(['relationships', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
(state, { id }) => state.getIn(['relationships', state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id']), 'account'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', id, 'account']), 'moved'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account']), 'moved'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id']), 'account']), 'moved'])]),
getFilters,
],
(statusBase, statusReblog, accountBase, accountReblog, filters) => {
(statusBase, statusReblog, statusQuote, accountBase, accountReblog, accountQuote, accountReblogQuote, relationshipBase, relationshipReblog, relationshipQuote, movedBase, movedReblog, movedQuote, filters) => {
if (!statusBase || statusBase.get('isLoading')) {
return null;
}
accountBase = accountBase.withMutations(map => {
map.set('relationship', relationshipBase);
map.set('moved', movedBase);
});
if (statusReblog) {
accountReblog = accountReblog.withMutations(map => {
map.set('relationship', relationshipReblog);
map.set('moved', movedReblog);
});
statusReblog = statusReblog.set('account', accountReblog);
} else {
statusReblog = null;
}
if (statusQuote) {
accountQuote = accountQuote.withMutations(map => {
map.set('relationship', relationshipQuote);
map.set('moved', movedQuote);
});
statusQuote = statusQuote.set('account', accountQuote);
} else {
statusQuote = null;
}
if (statusReblog && accountReblogQuote) {
statusReblog = statusReblog.setIn(['quote', 'account'], accountReblogQuote);
}
let filtered = false;
if ((accountReblog || accountBase).get('id') !== me && filters) {
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
@ -51,6 +84,7 @@ export const makeGetStatus = () => {
return statusBase.withMutations(map => {
map.set('reblog', statusReblog);
map.set('quote', statusQuote);
map.set('account', accountBase);
map.set('matched_filters', filtered);
});

View File

@ -0,0 +1 @@
export const uniq = (array) => array.filter((x, i, self) => self.indexOf(x) === i);

View File

@ -190,6 +190,29 @@ function loaded() {
const message = (statusEl.dataset.spoiler === 'expanded') ? (localeData['status.show_less'] || 'Show less') : (localeData['status.show_more'] || 'Show more');
spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
});
delegate(document, '.quote-status', 'click', ({ target }) => {
if (target.closest('.status__content__spoiler-link') ||
target.closest('.media-gallery') ||
target.closest('.video-player') ||
target.closest('.audio-player')) {
return false;
}
let url = target.closest('.quote-status').getAttribute('dataurl');
if (target.closest('.status__display-name')) {
url = target.closest('.status__display-name').getAttribute('href');
} else if (target.closest('.status-card')) {
url = target.closest('.status-card').getAttribute('href');
}
if (window.location.hostname === url.split('/')[2].split(':')[0]) {
window.location.href = url;
} else {
window.open(url, 'blank');
}
return false;
});
}
Rails.delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {

View File

@ -23,3 +23,5 @@
@import 'mastodon/rtl';
@import 'mastodon/accessibility';
@import 'mastodon/rich_text';
@import 'plugin';

View File

@ -0,0 +1,3 @@
@import 'light-pink/variables';
@import 'application';
@import 'mastodon-light/diff';

View File

@ -0,0 +1,61 @@
// Dependent colors
$black: #000000;
$white: #ffffff;
$classic-base-color: #6e202f;
$classic-primary-color: #ffa7ae;
$classic-secondary-color: #faeef1;
$classic-highlight-color: #ff375b;
$blurple-600: #6e202f; // Iris
$blurple-500: #ff375b; // Brand purple
$blurple-300: #ffa7ae; // Faded Blue
$grey-600: #5a4c4f; // Trout
$grey-100: #f3dae1; // Topaz
// Differences
$success-green: lighten(#3c754d, 8%);
$base-overlay-background: $white !default;
$valid-value-color: $success-green !default;
$ui-base-color: $classic-secondary-color !default;
$ui-base-lighter-color: #ffe1e9;
$ui-primary-color: #f1adbf;
$ui-secondary-color: $classic-base-color !default;
$ui-highlight-color: $classic-highlight-color !default;
$ui-button-secondary-color: $grey-600 !default;
$ui-button-secondary-border-color: $grey-600 !default;
$ui-button-secondary-focus-color: $white !default;
$ui-button-tertiary-color: $blurple-500 !default;
$ui-button-tertiary-border-color: $blurple-500 !default;
$ui-button-color: $white !default;
$ui-button-background-color: $blurple-500 !default;
$ui-button-focus-background-color: $blurple-600 !default;
$primary-text-color: $black !default;
$darker-text-color: $classic-base-color !default;
$highlight-text-color: darken($ui-highlight-color, 8%) !default;
$dark-text-color: #6e202f;
$action-button-color: #ffa7ae;
$inverted-text-color: $black !default;
$lighter-text-color: $classic-base-color !default;
$light-text-color: #ffa7ae;
// Newly added colors
$account-background-color: $white !default;
// Invert darkened and lightened colors
@function darken($color, $amount) {
@return hsl(hue($color), saturation($color), lightness($color) + $amount);
}
@function lighten($color, $amount) {
@return hsl(hue($color), saturation($color), lightness($color) - $amount);
}
$emojis-requiring-inversion: 'chains';

View File

@ -875,6 +875,29 @@ body > [data-popper-placement] {
background: $ui-highlight-color;
border-color: $ui-highlight-color;
color: $primary-text-color;
padding-top: 10px;
.compose-form__publish-button-wrapper {
padding-top: 15px;
button {
display: inline-block;
width: auto;
margin-right: 0.5em;
}
button:last-child {
margin-right: auto;
}
}
}
.compose-form__utilBtns {
padding-top: 10px;
* {
margin-bottom: 0.5em;
}
}
}
@ -935,6 +958,45 @@ body > [data-popper-placement] {
}
}
.reply-indicator {
border-radius: 4px;
margin-bottom: 10px;
background: $ui-primary-color;
padding: 10px;
min-height: 23px;
overflow-y: auto;
flex: 0 2 auto;
&.quote-indicator {
background: $success-green;
}
}
.reply-indicator__header {
margin-bottom: 5px;
overflow: hidden;
}
.reply-indicator__cancel {
float: right;
line-height: 24px;
}
.reply-indicator__display-name {
color: $inverted-text-color;
display: block;
max-width: 100%;
line-height: 24px;
overflow: hidden;
padding-inline-end: 25px;
text-decoration: none;
}
.reply-indicator__display-avatar {
float: left;
margin-inline-end: 5px;
}
.status__content--with-action {
cursor: pointer;
}
@ -1250,6 +1312,10 @@ body > [data-popper-placement] {
.status__content.status__content--collapsed {
max-height: 22px * 15; // 15 lines is roughly above 500 characters
.quote-status & {
max-height: 22px * 5;
}
}
.status__content__read-more-button,
@ -1329,6 +1395,70 @@ body > [data-popper-placement] {
}
}
.quote-status {
border: solid 1px $ui-base-lighter-color;
border-radius: 4px !important;
padding: 5px !important;
margin-top: 8px;
position: relative;
.muted-quote,
.unlisted-quote button {
color: $dark-text-color;
font-size: 15px;
width: 100%;
border: 0;
padding: 0;
}
.muted-quote {
text-align: center;
cursor: default;
}
.unlisted-quote button {
background-color: transparent;
cursor: pointer;
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 {
margin-bottom: 0;
line-height: unset;
strong,
span {
display: block;
line-height: 22px;
}
}
.status__content__text {
p {
display: inline;
&::after {
content: ' ';
}
}
}
}
.quote-inline {
display: none;
}
.focusable {
&:focus {
outline: 0;
@ -1344,9 +1474,12 @@ body > [data-popper-placement] {
.status {
padding: 16px;
min-height: 54px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: auto;
&:not(.quote-status) {
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
@keyframes fade {
0% {
opacity: 0;

View File

@ -40,7 +40,7 @@
&:last-child {
.detailed-status,
.status,
.status:not(.quote-status),
.load-more {
border-bottom: 0;
border-radius: 0 0 4px 4px;
@ -63,9 +63,18 @@
}
}
.detailed-status .quote-status {
width: 100%;
}
.quote-status {
margin-top: 15px;
cursor: pointer;
}
@media screen and (width <= 740px) {
.detailed-status,
.status,
.status:not(.quote-status),
.load-more {
border-radius: 0 !important;
}
@ -77,6 +86,10 @@
}
}
.standalone-timeline .quote-status {
cursor: pointer;
}
.button.logo-button svg {
width: 20px;
height: auto;

View File

@ -0,0 +1,60 @@
// ここから下タブバーの実装
//投稿ボタン
.columns-area__panels__main .button.bottom_right {
position: fixed;
right: 18px;
bottom: 65px;
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
font-size: 24px;
border-radius: 50%;
}
.tab-ber-bottom .navber {
display: none;
}
.column-link--transparent{
justify-content: center;
}
@media screen and (max-width: 630px) {
.tab-ber-bottom .timeline{
width:100%;
margin: 0 0 50px 0;
}
.tab-ber-bottom .navber{
display: flex;
width:100%;
bottom: 0;
position: fixed;
}
.tab-ber-bottom .navber .tabs-bar__wrapper {
bottom: 0;
width: 100%;
align-self: auto;
display: flex;
border-top: 1px solid lighten($ui-base-color, 8%);
}
.tab-ber-bottom .navber .tabs-bar__wrapper a{
flex: 1;
text-align: center;
}
.columns-area__panels__pane-tab-ber{
display: none;
}
.tab-ber-bottom .navber .tabs-bar__wrapper a span {
display: none;
}
}
//ここまで下タブバーの実装

@ -0,0 +1 @@
Subproject commit a58c699a605784b3318f54717bfaaa804db4d2f3

@ -0,0 +1 @@
Subproject commit a5c22831445f5669315434f7df49c3c1e1f70d20

View File

@ -78,6 +78,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
@silenced_account_ids = []
@params = {}
process_quote
process_status_params
process_tags
process_audience
@ -130,6 +131,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
media_attachment_ids: attachment_ids,
ordered_media_attachment_ids: attachment_ids,
poll: process_poll,
quote: @quote,
}
end
@ -430,4 +432,20 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
poll.reload
retry
end
def process_quote
return unless (@quote = quote_from_url(@object['quoteUrl']))
%r{<br><br>RE:\s</span><a.*</a>}.match(@object['content']) do |m|
@object['content'] = @object['content'].sub(m[0], "</span><span class=\"quote-inline\">#{m[0].sub(%r{</span>}, '')}</span>")
end
end
def quote_from_url(url)
return nil if url.nil?
quote = ResolveURLService.new.call(url)
status_from_uri(quote.uri) if quote
rescue
nil
end
end

View File

@ -13,7 +13,8 @@ class TextFormatter
multiline: true,
}.freeze
attr_reader :text, :options
attr_accessor :text
attr_reader :options
# @param [String] text
# @param [Hash] options
@ -21,6 +22,7 @@ class TextFormatter
# @option options [Boolean] :with_domains
# @option options [Boolean] :with_rel_me
# @option options [Array<Account>] :preloaded_accounts
# @option options [Status] :quote
def initialize(text, options = {})
@text = text
@options = DEFAULT_OPTIONS.merge(options)
@ -31,7 +33,7 @@ class TextFormatter
end
def to_s
return ''.html_safe if text.blank?
return ''.html_safe if text.blank? & !quote?
html = rewrite do |entity|
if entity[:url]
@ -43,6 +45,8 @@ class TextFormatter
end
end
html += render_quote if quote?
html = simple_format(html, {}, sanitize: false).delete("\n") if multiline?
html.html_safe # rubocop:disable Rails/OutputSafety
@ -126,7 +130,7 @@ class TextFormatter
return "@#{h(entity[:screen_name])}" if account.nil?
url = ActivityPub::TagManager.instance.url_for(account)
url = ap_tag_manager.url_for(account)
display_username = same_username_hits&.positive? || with_domains? ? account.pretty_acct : account.username
<<~HTML.squish
@ -134,6 +138,13 @@ class TextFormatter
HTML
end
def render_quote
link = link_to_url({ url: ap_tag_manager.url_for(quote) })
<<~HTML.squish
<span class="quote-inline"><br>RE: #{link}</span>
HTML
end
def entity_cache
@entity_cache ||= EntityCache.instance
end
@ -142,6 +153,10 @@ class TextFormatter
@tag_manager ||= TagManager.instance
end
def ap_tag_manager
@ap_tag_manager ||= ActivityPub::TagManager.instance
end
delegate :local_domain?, to: :tag_manager
def multiline?
@ -163,4 +178,12 @@ class TextFormatter
def preloaded_accounts?
preloaded_accounts.present?
end
def quote
options[:quote]
end
def quote?
quote.present?
end
end

View File

@ -20,4 +20,10 @@ class ApplicationMailer < ActionMailer::Base
headers['X-Auto-Response-Suppress'] = 'All'
headers['Auto-Submitted'] = 'auto-generated'
end
def set_autoreply_headers!
headers['Precedence'] = 'list'
headers['X-Auto-Response-Suppress'] = 'All'
headers['Auto-Submitted'] = 'auto-generated'
end
end

View File

@ -95,6 +95,10 @@ module User::HasSettings
settings['web.disable_swiping']
end
def setting_place_tab_bar_at_bottom
settings['web.place_tab_bar_at_bottom']
end
def setting_always_send_emails
settings['always_send_emails']
end

View File

@ -22,6 +22,7 @@
# account_id :bigint(8) not null
# application_id :bigint(8)
# in_reply_to_account_id :bigint(8)
# quote_id :bigint(8)
# poll_id :bigint(8)
# deleted_at :datetime
# edited_at :datetime
@ -62,6 +63,7 @@ class Status < ApplicationRecord
with_options class_name: 'Status', optional: true do
belongs_to :thread, foreign_key: 'in_reply_to_id', inverse_of: :replies
belongs_to :reblog, foreign_key: 'reblog_of_id', inverse_of: :reblogs
belongs_to :quote, class_name: 'Status', inverse_of: :quoted, optional: true
end
has_many :favourites, inverse_of: :status, dependent: :destroy
@ -72,6 +74,7 @@ class Status < ApplicationRecord
has_many :mentions, dependent: :destroy, inverse_of: :status
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
has_many :media_attachments, dependent: :nullify
has_many :quoted, foreign_key: 'quote_id', class_name: 'Status', inverse_of: :quote, dependent: :nullify
# The `dependent` option is enabled by the initial `mentions` association declaration
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status # rubocop:disable Rails/HasManyOrHasOneDependent
@ -97,6 +100,7 @@ class Status < ApplicationRecord
validates_with DisallowedHashtagsValidator
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
validates :quote_visibility, inclusion: { in: %w(public unlisted) }, if: :quote?
accepts_nested_attributes_for :poll
@ -200,6 +204,14 @@ class Status < ApplicationRecord
!reblog_of_id.nil?
end
def quote?
!quote_id.nil? && quote
end
def quote_visibility
quote&.visibility
end
def within_realtime_window?
created_at >= REAL_TIME_WINDOW.ago
end
@ -272,7 +284,12 @@ class Status < ApplicationRecord
fields = [spoiler_text, text]
fields += preloadable_poll.options unless preloadable_poll.nil?
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain)
quote_fields = []
quote_fields += [quote.spoiler_text, quote.text] if quote?
quote_fields += quote.preloadable_poll.options unless quote&.preloadable_poll.nil?
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain) +
CustomEmoji.from_text(quote_fields.join(' '), quote&.account&.domain)
end
def ordered_media_attachments

View File

@ -32,6 +32,7 @@ class UserSettings
setting :expand_content_warnings, default: false
setting :display_media, default: 'default', in: %w(default show_all hide_all)
setting :auto_play, default: false
setting :place_tab_bar_at_bottom, default: false
end
namespace :notification_emails do

View File

@ -92,6 +92,14 @@ class Webhook < ApplicationRecord
end
end
def validate_permissions
errors.add(:events, :invalid_permissions) if defined?(@current_account) && required_permissions.any? { |permission| !@current_account.user_role.can?(permission) }
end
def strip_events
self.events = events.filter_map { |str| str.strip.presence } if events.present?
end
def generate_secret
self.secret = SecureRandom.hex(20) if secret.blank?
end

View File

@ -11,6 +11,9 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
:atom_uri, :in_reply_to_atom_uri,
:conversation
attribute :quote_url, if: -> { object.quote? }
attribute :misskey_quote, key: :_misskey_quote, if: -> { object.quote? }
attribute :misskey_content, key: :_misskey_content, if: -> { object.quote? }
attribute :content
attribute :content_map, if: :language?
attribute :updated, if: :edited?
@ -138,6 +141,16 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
end
end
def quote_url
ActivityPub::TagManager.instance.uri_for(object.quote) if object.quote?
end
alias misskey_quote quote_url
def misskey_content
object.text if object.quote?
end
def local?
object.account.local?
end

View File

@ -29,6 +29,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:use_blurhash] = object_account_user.setting_use_blurhash
store[:use_pending_items] = object_account_user.setting_use_pending_items
store[:show_trends] = Setting.trends && object_account_user.setting_trends
store[:place_tab_bar_at_bottom] = object.current_account.user.setting_place_tab_bar_at_bottom
else
store[:auto_play_gif] = Setting.auto_play_gif
store[:display_media] = Setting.display_media

View File

@ -11,7 +11,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
attributes :domain, :title, :version, :source_url, :description,
:usage, :thumbnail, :languages, :configuration,
:registrations
:registrations, :feature_quote
has_one :contact, serializer: ContactSerializer
has_many :rules, serializer: REST::RuleSerializer
@ -93,6 +93,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
}
end
def feature_quote
true
end
private
def registrations_enabled?

View File

@ -18,6 +18,8 @@ class REST::StatusSerializer < ActiveModel::Serializer
attribute :content, unless: :source_requested?
attribute :text, if: :source_requested?
attribute :quote_id, if: -> { object.quote? }
belongs_to :reblog, serializer: REST::StatusSerializer
belongs_to :application, if: :show_application?
belongs_to :account, serializer: REST::AccountSerializer
@ -42,6 +44,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
object.in_reply_to_account_id&.to_s
end
def quote_id
object.quote_id.to_s
end
def current_user?
!current_user.nil?
end
@ -196,3 +202,25 @@ class REST::StatusSerializer < ActiveModel::Serializer
end
end
end
class REST::NestedQuoteSerializer < REST::StatusSerializer
attribute :quote do
nil
end
attribute :quote_muted, if: :current_user?
def quote_muted
if instance_options && instance_options[:account_relationships]
!!instance_options[:account_relationships].muting[object.account_id] ||
instance_options[:account_relationships].blocking[object.account_id] ||
instance_options[:account_relationships].blocked_by[object.account_id] ||
instance_options[:account_relationships].domain_blocking[object.account_id]
else
current_user.account.muting?(object.account) || object.account.blocking?(current_user.account) || current_user.account.blocking?(object.account) || current_user.account.domain_blocking?(object.account.domain)
end
end
end
class REST::StatusSerializer < ActiveModel::Serializer
belongs_to :quote, serializer: REST::NestedQuoteSerializer
end

View File

@ -75,7 +75,7 @@ class FetchLinkCardService < BaseService
@status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
else
document = Nokogiri::HTML(@status.text)
links = document.css('a')
links = document.css(':not(.quote-inline) > a')
links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize)
end

View File

@ -5,6 +5,7 @@ class PostStatusService < BaseService
include LanguagesHelper
MIN_SCHEDULE_OFFSET = 5.minutes.freeze
QUOTE_LINK_PATTERN = /\n\[?(#{FetchLinkCardService::URL_PATTERN})\]?$/
class UnexpectedMentionsError < StandardError
attr_reader :accounts
@ -30,6 +31,7 @@ class PostStatusService < BaseService
# @option [Doorkeeper::Application] :application
# @option [String] :idempotency Optional idempotency key
# @option [Boolean] :with_rate_limit
# @option [Integer] :quote_id
# @option [Enumerable] :allowed_mentions Optional array of expected mentioned account IDs, raises `UnexpectedMentionsError` if unexpected accounts end up in mentions
# @return [Status]
def call(account, options = {})
@ -37,11 +39,13 @@ class PostStatusService < BaseService
@options = options
@text = @options[:text] || ''
@in_reply_to = @options[:thread]
@quote_id = @options[:quote_id]
return idempotency_duplicate if idempotency_given? && idempotency_duplicate?
validate_media!
preprocess_attributes!
preprocess_quote!
if scheduled?
schedule_status!
@ -61,6 +65,19 @@ class PostStatusService < BaseService
private
def status_from_uri(uri)
ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
end
def quote_from_url(url)
return nil if url.nil?
quote = ResolveURLService.new.call(url)
status_from_uri(quote.uri) if quote
rescue
nil
end
def preprocess_attributes!
@sensitive = (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
@ -68,10 +85,21 @@ class PostStatusService < BaseService
@visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced?
@scheduled_at = @options[:scheduled_at]&.to_datetime
@scheduled_at = nil if scheduled_in_the_past?
if @quote_id.nil? && (link = @text&.match(QUOTE_LINK_PATTERN)) && (@quote_id = quote_from_url(link[1])&.id)
@text = @text&.lines(chomp: true)&.[](0...-1)&.join("\n")
end
rescue ArgumentError
raise ActiveRecord::RecordInvalid
end
def preprocess_quote!
if @quote_id.present?
quote = Status.find(@quote_id)
@quote_id = quote.reblog_of_id.to_s if quote.reblog?
end
end
def process_status!
@status = @account.statuses.new(status_attributes)
process_mentions_service.call(@status, save_records: false)
@ -193,6 +221,7 @@ class PostStatusService < BaseService
language: valid_locale_cascade(@options[:language], @account.user&.preferred_posting_language, I18n.default_locale),
application: @options[:application],
rate_limit: @options[:with_rate_limit],
quote_id: @quote_id,
}.compact
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class StatusLengthValidator < ActiveModel::Validator
MAX_CHARS = 500
MAX_CHARS = 2048
URL_PLACEHOLDER_CHARS = 23
URL_PLACEHOLDER = 'x' * 23

View File

@ -2,7 +2,7 @@
%html{ lang: I18n.locale }
%head
%meta{ charset: 'utf-8' }/
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' }/
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, viewport-fit=cover'}/
- if cdn_host?
%link{ rel: 'dns-prefetch', href: cdn_host }/

View File

@ -57,6 +57,7 @@
= ff.input :'web.reduce_motion', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_reduce_motion')
= ff.input :'web.disable_swiping', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_disable_swiping')
= ff.input :'web.use_system_font', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_system_font_ui')
= ff.input :'web.place_tab_bar_at_bottom', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_place_tab_bar_at_bottom')
%h4= t 'appearance.discovery'

View File

@ -0,0 +1,20 @@
:ruby
detailed ||= false
inline ||= false
.p-author.h-card
= link_to ActivityPub::TagManager.instance.url_for(author), class: "#{detailed ? 'detailed-' : ''}status__display-name u-url", target: stream_link_target, rel: "noopener #{detailed || inline ? '' : 'noreferrer'}" do
%div{ class: "#{detailed ? 'detailed-' : ''}status__#{detailed ? 'display-' : ''}avatar" }
%div
- if prefers_autoplay?
= image_tag author.avatar_original_url, alt: '', class: 'u-photo account__avatar'
- else
= image_tag author.avatar_static_url, alt: '', class: 'u-photo account__avatar'
%span.display-name
%bdi
%strong.display-name__html.p-name.emojify= display_name(author, custom_emojify: true, autoplay: prefers_autoplay?)
%span.display-name__account
= acct(author)
- if !inline && author.locked?
= fa_icon('lock')

View File

@ -1,40 +1,14 @@
.detailed-status.detailed-status--flex{ class: "detailed-status-#{status.visibility}" }
.p-author.h-card
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'detailed-status__display-name u-url', target: stream_link_target, rel: 'noopener' do
.detailed-status__display-avatar
- if prefers_autoplay?
= image_tag status.account.avatar_original_url, alt: '', class: 'account__avatar u-photo'
- else
= image_tag status.account.avatar_static_url, alt: '', class: 'account__avatar u-photo'
%span.display-name
%bdi
%strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: prefers_autoplay?)
%span.display-name__account
= acct(status.account)
= fa_icon('lock') if status.account.locked?
= render 'statuses/author', author: status.account, detailed: true
= account_action_button(status.account)
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
- if status.spoiler_text?
%p<
%span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}&nbsp;
%button.status__content__spoiler-link= t('statuses.show_more')
.e-content{ lang: status.language }
= prerender_custom_emojis(status_content_format(status), status.emojis)
= render 'statuses/text', status: status
- if status.preloadable_poll
= render_poll_component(status)
- if status.quote?
= render 'statuses/quote', status: status.quote
- if !status.ordered_media_attachments.empty?
- if status.ordered_media_attachments.first.video?
= render_video_component(status, width: 670, height: 380, detailed: true)
- elsif status.ordered_media_attachments.first.audio?
= render_audio_component(status, width: 670, height: 380)
- else
= render_media_gallery_component(status, height: 380, standalone: true)
- elsif status.preview_card
= render_card_component(status)
= render 'statuses/media', status: status, detailed: true
.detailed-status__meta
%data.dt-published{ value: status.created_at.to_time.iso8601 }

View File

@ -0,0 +1,13 @@
:ruby
detailed ||= false
quote ||= false
- if !status.ordered_media_attachments.empty?
- if status.ordered_media_attachments.first.video?
= render_video_component(status, width: 670, height: 380, detailed: detailed)
- elsif status.ordered_media_attachments.first.audio?
= render_audio_component(status, width: 670, height: 380)
- else
= render_media_gallery_component(status, height: 380, standalone: true)
- elsif status.preview_card
= render_card_component(status, quote: quote)

View File

@ -0,0 +1,6 @@
.status.quote-status{ dataurl: ActivityPub::TagManager.instance.url_for(status) }
= render 'statuses/author', author: status.account, inline: true
= render 'statuses/text', status: status
= render 'statuses/media', status: status, quote: true

View File

@ -35,18 +35,12 @@
.e-content{ lang: status.language }
= prerender_custom_emojis(status_content_format(status), status.emojis)
- if status.preloadable_poll
= render_poll_component(status)
= render 'statuses/text', status: status
- if !status.ordered_media_attachments.empty?
- if status.ordered_media_attachments.first.video?
= render_video_component(status, width: 610, height: 343)
- elsif status.ordered_media_attachments.first.audio?
= render_audio_component(status, width: 610, height: 343)
- else
= render_media_gallery_component(status, height: 343)
- elsif status.preview_card
= render_card_component(status)
- if status.quote?
= render 'statuses/quote', status: status.quote
= render 'statuses/media', status: status
- if !status.in_reply_to_id.nil? && status.in_reply_to_account_id == status.account.id && !hide_show_thread
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__content__read-more-button', target: stream_link_target, rel: 'noopener noreferrer' do

View File

@ -0,0 +1,9 @@
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
- if status.spoiler_text?
%p<
%span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}&nbsp;
%button.status__content__spoiler-link= t('statuses.show_more')
.e-content{ lang: status.language }
= prerender_custom_emojis(status_content_format(status), status.emojis)
- if status.preloadable_poll
= render_poll_component(status)

View File

@ -138,7 +138,7 @@ ja:
push: プッシュ通知
reports: 通報
search: 検索
statuses: 投稿
statuses: トゥート
layouts:
admin:
nav:
@ -178,13 +178,13 @@ ja:
read:notifications: 通知の読み取り
read:reports: 通報の読み取り
read:search: あなたの代わりに検索
read:statuses: すべての投稿の読み取り
read:statuses: すべてのトゥートの読み取り
write: アカウントのすべてのデータの変更
write:accounts: プロフィールの変更
write:blocks: ユーザーのブロックやドメインの非表示
write:bookmarks: 投稿のブックマーク登録
write:bookmarks: トゥートのブックマーク登録
write:conversations: 会話のミュートと削除
write:favourites: お気に入りの投稿
write:favourites: お気に入りのトゥート
write:filters: フィルターの変更
write:follows: あなたの代わりにフォロー、アンフォロー
write:lists: リストの変更
@ -192,4 +192,4 @@ ja:
write:mutes: アカウントや会話のミュート
write:notifications: 通知の消去
write:reports: 通報の作成
write:statuses: 投稿の送信
write:statuses: トゥートの送信

View File

@ -18,8 +18,8 @@ ja:
pin_errors:
following: おすすめしたい人はあなたが既にフォローしている必要があります
posts:
other: 投稿
posts_tab_heading: 投稿
other: トゥート
posts_tab_heading: トゥート
admin:
account_actions:
action: アクションを実行
@ -143,7 +143,7 @@ ja:
targeted_reports: このアカウントについての通報
silence: サイレンス
silenced: サイレンス済み
statuses: 投稿
statuses: トゥート
strikes: 前回のストライク
subscribe: 購読する
suspend: サスペンド
@ -192,7 +192,7 @@ ja:
destroy_email_domain_block: メールドメインブロックを削除
destroy_instance: ドメインをブロックする
destroy_ip_block: IPルールを削除
destroy_status: 投稿を削除
destroy_status: トゥートを削除
destroy_unavailable_domain: 配送できないドメインを削除
destroy_user_role: ロールを削除
disable_2fa_user: 二要素認証を無効化
@ -223,7 +223,7 @@ ja:
update_custom_emoji: カスタム絵文字を更新
update_domain_block: ドメインブロックを更新
update_ip_block: IPルールを更新
update_status: 投稿を更新
update_status: トゥートを更新
update_user_role: ロールを更新
actions:
approve_appeal_html: "%{name}さんが%{target}さんからの抗議を承認しました"
@ -251,7 +251,7 @@ ja:
destroy_email_domain_block_html: "%{name}さんが%{target}をメールドメインブロックから外しました"
destroy_instance_html: "%{name}さんがドメイン %{target}をブロックしました"
destroy_ip_block_html: "%{name}さんが IP %{target}のルールを削除しました"
destroy_status_html: "%{name}さんが%{target}さんの投稿を削除しました"
destroy_status_html: "%{name}さんが%{target}さんのトゥートを削除しました"
destroy_unavailable_domain_html: "%{name}がドメイン %{target}への配送を再開しました"
destroy_user_role_html: "%{name}さんがロール『%{target}』を削除しました"
disable_2fa_user_html: "%{name}さんが%{target}さんの二要素認証を無効化しました"
@ -282,7 +282,7 @@ ja:
update_custom_emoji_html: "%{name}さんがカスタム絵文字 %{target}を更新しました"
update_domain_block_html: "%{name}さんが%{target}のドメインブロックを更新しました"
update_ip_block_html: "%{name} さんがIP %{target} のルールを更新しました"
update_status_html: "%{name}さんが%{target}さんの投稿を更新しました"
update_status_html: "%{name}さんが%{target}さんのトゥートを更新しました"
update_user_role_html: "%{name}さんがロール『%{target}』を変更しました"
deleted_account: 削除されたアカウント
empty: ログが見つかりませんでした
@ -397,7 +397,7 @@ ja:
create: ブロックを作成
hint: ドメインブロックはデータベース中のアカウント項目の作成を妨げませんが、遡って自動的に指定されたモデレーションをそれらのアカウントに適用します。
severity:
desc_html: "<strong>制限</strong>は、このドメイン上のアカウントからの投稿が、相手をフォローしている場合を除き非表示になります。<strong>停止</strong>は、このドメイン上のすべてのコンテンツ、メディア、およびプロフィールデータを受け付けなくなります。メディアファイルのみを拒否したい場合は<strong>なし</strong>を選択します。"
desc_html: "<strong>制限</strong>は、このドメイン上のアカウントからのトゥートが、相手をフォローしている場合を除き非表示になります。<strong>停止</strong>は、このドメイン上のすべてのコンテンツ、メディア、およびプロフィールデータを受け付けなくなります。メディアファイルのみを拒否したい場合は<strong>なし</strong>を選択します。"
noop: なし
silence: 制限
suspend: 停止
@ -494,7 +494,7 @@ ja:
instance_languages_dimension: 人気の言語
instance_media_attachments_measure: 保存されたメディア
instance_reports_measure: 通報
instance_statuses_measure: 保存された投稿
instance_statuses_measure: 保存されたトゥート
delivery:
all: すべて
clear: 配送エラーをクリア
@ -553,11 +553,11 @@ ja:
relays:
add_new: リレーを追加
delete: 削除
description_html: "<strong>連合リレー</strong>とは、登録しているサーバー間の公開投稿を仲介するサーバーです。<strong>中小規模のサーバーが連合のコンテンツを見つけるのを助けます。</strong>これを使用しない場合、ローカルユーザーがリモートユーザーを手動でフォローする必要があります。"
description_html: "<strong>連合リレー</strong>とは、登録しているサーバー間の公開トゥートを仲介するサーバーです。<strong>中小規模のサーバーが連合のコンテンツを見つけるのを助けます。</strong>これを使用しない場合、ローカルユーザーがリモートユーザーを手動でフォローする必要があります。"
disable: 無効化
disabled: 無効
enable: 有効化
enable_hint: 有効にすると、リレーから全ての公開投稿を受信するようになり、またこのサーバーの全ての公開投稿をリレーに送信するようになります。
enable_hint: 有効にすると、リレーから全ての公開トゥートを受信するようになり、またこのサーバーの全ての公開トゥートをリレーに送信するようになります。
enabled: 有効
inbox_url: リレーURL
pending: リレーサーバーの承認待ちです
@ -576,8 +576,8 @@ ja:
action_log: 監査ログ
action_taken_by: 通報処理者
actions:
delete_description_html: 報告された投稿は削除され、ストライクが記録されます。同じアカウントによる今後の違反行為のエスカレーションに役立てられます。
mark_as_sensitive_description_html: 報告された投稿のメディアは閲覧注意となり、ストライクが記録され、同じアカウントによる今後の違反行為のエスカレーションに役立てられます。
delete_description_html: 報告されたトゥートは削除され、ストライクが記録されます。同じアカウントによる今後の違反行為のエスカレーションに役立てられます。
mark_as_sensitive_description_html: 報告されたトゥートのメディアは閲覧注意となり、ストライクが記録され、同じアカウントによる今後の違反行為のエスカレーションに役立てられます。
other_description_html: アカウントの動作を制御するためのオプションや、報告されたアカウントへの通信をカスタマイズするためのオプションを確認してください。
resolve_description_html: 報告されたアカウントに対していかなる措置も取られず、ストライクも記録されず、報告は終了します。
silence_description_html: このアカウントは、すでにフォローしている人、または手動で検索した人にしか見えないため、リーチが極端に制限されます。いつでも元に戻すことができます。このアカウントに対するすべての通報をクローズします。
@ -598,7 +598,7 @@ ja:
confirm: 確認
confirm_action: "@%{acct} さんに対するアクション"
created_at: 通報日時
delete_and_resolve: 投稿を削除
delete_and_resolve: トゥートを削除
forwarded: 転送済み
forwarded_replies_explanation: これはリモートユーザーによる、リモートコンテンツについての報告です。問題のコンテンツはあなたのサーバー利用者への返信なので、こちらにも転送されて来ました。
forwarded_to: "%{domain}に転送されました"
@ -626,18 +626,18 @@ ja:
skip_to_actions: アクションに移動
status: ステータス
statuses: 通報内容
statuses_description_html: 問題の投稿は通報されたアカウントへの連絡時に引用されます
statuses_description_html: 問題のトゥートは通報されたアカウントへの連絡時に引用されます
summary:
action_preambles:
delete_html: "<strong>@%{acct}</strong>さんの投稿を<strong>削除</strong>します。この操作は:"
mark_as_sensitive_html: "<strong>@%{acct}</strong>さんの投稿を<strong>閲覧注意</strong>として<strong>マーク</strong>します。この操作は:"
delete_html: "<strong>@%{acct}</strong>さんのトゥートを<strong>削除</strong>します。この操作は:"
mark_as_sensitive_html: "<strong>@%{acct}</strong>さんのトゥートを<strong>閲覧注意</strong>として<strong>マーク</strong>します。この操作は:"
silence_html: "<strong>@%{acct}</strong>さんのアカウントを<strong>制限 (サイレンス) </strong>します。この操作は:"
suspend_html: "<strong>@%{acct}</strong>さんのアカウントを<strong>停止 (サスペンド) </strong>します。この操作は:"
actions:
delete_html: 当該の投稿を削除します
mark_as_sensitive_html: 当該の投稿に含まれるメディアを閲覧注意にします
delete_html: 当該のトゥートを削除します
mark_as_sensitive_html: 当該のトゥートに含まれるメディアを閲覧注意にします
silence_html: "<strong>@%{acct}</strong>さんのプロフィールとコンテンツの表示範囲をフォロー中の人や意図的にプロフィールにアクセスした人のみに制限することで、アカウントを発見されにくくします"
suspend_html: "<strong>@%{acct}</strong>さんのアカウントが凍結され、プロフィールとコンテンツへのアクセス、および投稿ができなくなります"
suspend_html: "<strong>@%{acct}</strong>さんのアカウントが凍結され、プロフィールとコンテンツへのアクセス、およびトゥートができなくなります"
close_report: '通報 #%{id} を解決済みにします'
close_reports_html: "<strong>@%{acct}</strong>さんに対する<strong>すべての</strong>通報を解決済みにします"
delete_data_html: 停止が解除されないまま30日経過すると、<strong>@%{acct}</strong>さんのプロフィールとコンテンツは削除されます
@ -762,7 +762,7 @@ ja:
open: 誰でも登録可
security:
authorized_fetch: 連合サーバーによる署名なしでの情報取得を拒否する
authorized_fetch_hint: ほかの連合サーバーから受け付けるリクエストに署名を必須にすることで、ユーザーによるブロックおよびドメインブロック両方の効果をより強力にします。ただし連合の処理コストが増えてパフォーマンス面で不利になるほか、このサーバーから送られた反応が届く範囲が狭まったり、連合における互換性の問題を招く可能性もあります。また、この機能は公開投稿やプロフィールへのアクセスをブロックした相手から完全に遮断できるものではありません。
authorized_fetch_hint: ほかの連合サーバーから受け付けるリクエストに署名を必須にすることで、ユーザーによるブロックおよびドメインブロック両方の効果をより強力にします。ただし連合の処理コストが増えてパフォーマンス面で不利になるほか、このサーバーから送られた反応が届く範囲が狭まったり、連合における互換性の問題を招く可能性もあります。また、この機能は公開トゥートやプロフィールへのアクセスをブロックした相手から完全に遮断できるものではありません。
authorized_fetch_overridden_hint: この設定は環境変数で指定されているため、ここでは変更できません。
federation_authentication: 連合に署名を必須にする
title: サーバー設定
@ -798,19 +798,19 @@ ja:
title: メディア
metadata: メタデータ
no_status_selected: 何も選択されていないため、変更されていません
open: 投稿を開く
original_status: オリジナルの投稿
open: トゥートを開く
original_status: オリジナルのトゥート
reblogs: ブースト
status_changed: 投稿を変更しました
title: 投稿一覧
status_changed: トゥートを変更しました
title: トゥート一覧
trending: トレンド
visibility: 公開範囲
with_media: メディアあり
strikes:
actions:
delete_statuses: "%{name}さんが%{target}さんの投稿を削除しました"
delete_statuses: "%{name}さんが%{target}さんのトゥートを削除しました"
disable: "%{name}さんが%{target}さんを凍結しました"
mark_statuses_as_sensitive: "%{name}さんが%{target}さんの投稿を閲覧注意としてマークしました"
mark_statuses_as_sensitive: "%{name}さんが%{target}さんのトゥートを閲覧注意としてマークしました"
none: "%{name}さんが%{target}さんに警告を送信しました"
sensitive: "%{name}さんが%{target}さんのアカウントを閲覧注意としてマークしました"
silence: "%{name}さんが%{target}さんを制限しました"
@ -889,15 +889,15 @@ ja:
rejected: 拒否
statuses:
allow: 掲載を許可
allow_account: 投稿者を許可
description_html: これらは、このサーバーが知っている、たくさんシェアされ、お気に入り登録されている投稿です。新しいユーザーや久しぶりにアクセスするユーザーがフォローする人を探すのに役立ちます。あなたが投稿者を承認し、投稿者が許可するまで、表示されることはありません。また、個別の投稿を許可または拒否することもできます。
allow_account: トゥート者を許可
description_html: これらは、このサーバーが知っている、たくさんシェアされ、お気に入り登録されているトゥートです。新しいユーザーや久しぶりにアクセスするユーザーがフォローする人を探すのに役立ちます。あなたがトゥート者を承認し、トゥート者が許可するまで、表示されることはありません。また、個別のトゥートを許可または拒否することもできます。
disallow: 掲載を拒否
disallow_account: 投稿者を拒否
disallow_account: トゥート者を拒否
no_status_selected: 何も選択されていないため、変更されていません
not_discoverable: 投稿者は発見可能であることに同意していません
not_discoverable: トゥート者は発見可能であることに同意していません
shared_by:
other: "%{friendly_count}回の共有、お気に入り"
title: トレンド投稿
title: トレンドトゥート
tags:
current_score: 現在のスコア %{score}
dashboard:
@ -906,7 +906,7 @@ ja:
tag_servers_dimension: 人気のサーバー
tag_servers_measure: その他のサーバー
tag_uses_measure: 合計利用数
description_html: これらは、多くの投稿に使用されているハッシュタグです。あなたのユーザーが、人々が今一番話題にしていることを知るのに役立ちます。あなたが承認するまで、ハッシュタグは一般に表示されません。
description_html: これらは、多くのトゥートに使用されているハッシュタグです。あなたのユーザーが、人々が今一番話題にしていることを知るのに役立ちます。あなたが承認するまで、ハッシュタグは一般に表示されません。
listable: おすすめに表示する
no_tag_selected: 何も選択されていないため、変更されていません
not_listable: おすすめに表示しない
@ -944,13 +944,13 @@ ja:
new: 新しいwebhook
rotate_secret: シークレットをローテートする
secret: シークレットに署名
status: 投稿
status: トゥート
title: Webhooks
webhook: Webhook
admin_mailer:
new_appeal:
actions:
delete_statuses: 投稿を削除する
delete_statuses: トゥートを削除する
disable: アカウントを無効にする
mark_statuses_as_sensitive: 閲覧注意としてマーク
none: 警告
@ -978,7 +978,7 @@ ja:
new_trending_links:
title: トレンドリンク
new_trending_statuses:
title: トレンド投稿
title: トレンドトゥート
new_trending_tags:
title: トレンドハッシュタグ
subject: "%{instance}で新しいトレンドが審査待ちです"
@ -1007,7 +1007,7 @@ ja:
unsubscribe: 購読解除
view: 'リンク:'
view_profile: プロフィールを表示
view_status: 投稿を表示
view_status: トゥートを表示
applications:
created: アプリが作成されました
destroyed: アプリが削除されました
@ -1132,7 +1132,7 @@ ja:
warning:
before: '続行する前に、次の点を再度確認してください:'
caches: 他のサーバーにコンテンツのキャッシュがずっと残る場合があります
data_removal: あなたの投稿やその他のデータはこのサーバーから完全に削除されます
data_removal: あなたのトゥートやその他のデータはこのサーバーから完全に削除されます
email_change_html: アカウントを削除しなくても<a href="%{path}">メールアドレスを変更</a>できます
email_contact_html: それでも届かない場合、<a href="mailto:%{email}">%{email}</a>までメールで問い合わせてください
email_reconfirmation_html: 確認のメールが届かない場合、<a href="%{path}">もう一度申請</a>できます。
@ -1156,13 +1156,13 @@ ja:
description_html: これらは、%{instance}のスタッフがあなたのアカウントに対して行った措置や、あなたに送られた警告です。
recipient: 送信元
reject_appeal: 抗議を却下
status: '投稿 #%{id}'
status: 'トゥート #%{id}'
status_removed: 既に削除されています
title: "%{date}に%{action}"
title_actions:
delete_statuses: 投稿の削除
delete_statuses: トゥートの削除
disable: アカウント凍結
mark_statuses_as_sensitive: 投稿を閲覧注意としてマーク
mark_statuses_as_sensitive: トゥートを閲覧注意としてマーク
none: 警告
sensitive: アカウントを閲覧注意としてマーク
silence: アカウントの制限
@ -1174,7 +1174,7 @@ ja:
invalid_domain: は無効なドメイン名です
edit_profile:
basic_information: 基本情報
hint_html: "<strong>アカウントのトップページや投稿の隣に表示される公開情報です。</strong>プロフィールとアイコンを設定することで、ほかのユーザーは親しみやすく、またフォローしやすくなります。"
hint_html: "<strong>アカウントのトップページやトゥートの隣に表示される公開情報です。</strong>プロフィールとアイコンを設定することで、ほかのユーザーは親しみやすく、またフォローしやすくなります。"
other: その他
errors:
'400': 送信されたリクエストは無効であるか、または不正なフォーマットです。
@ -1198,7 +1198,7 @@ ja:
archive_takeout:
date: 日時
download: ダウンロード
hint_html: "<strong>投稿本文とメディア</strong>のアーカイブをリクエストできます。 データはActivityPub形式で、対応しているソフトウェアで読み込むことができます。7日毎にアーカイブをリクエストできます。"
hint_html: "<strong>トゥート本文とメディア</strong>のアーカイブをリクエストできます。 データはActivityPub形式で、対応しているソフトウェアで読み込むことができます。7日毎にアーカイブをリクエストできます。"
in_progress: 準備中...
request: アーカイブをリクエスト
size: 容量
@ -1213,7 +1213,7 @@ ja:
add_new: 追加
errors:
limit: すでに注目のハッシュタグの上限数に達しています
hint_html: "<strong>注目のハッシュタグとは?</strong> プロフィールページに目立つ形で表示され、そのハッシュタグのついたあなたの公開投稿だけを抽出して閲覧できるようにします。クリエイティブな仕事や長期的なプロジェクトを追うのに優れた機能です。"
hint_html: "<strong>注目のハッシュタグとは?</strong> プロフィールページに目立つ形で表示され、そのハッシュタグのついたあなたの公開トゥートだけを抽出して閲覧できるようにします。クリエイティブな仕事や長期的なプロジェクトを追うのに優れた機能です。"
filters:
contexts:
account: プロフィール
@ -1224,8 +1224,8 @@ ja:
edit:
add_keyword: キーワードを追加
keywords: キーワード
statuses: 個別の投稿
statuses_hint_html: このフィルタは、以下のキーワードにマッチするかどうかに関わらず、個々の投稿を選択して適用されます。 <a href="%{path}">フィルターを確認または投稿を削除</a>。
statuses: 個別のトゥート
statuses_hint_html: このフィルタは、以下のキーワードにマッチするかどうかに関わらず、個々のトゥートを選択して適用されます。 <a href="%{path}">フィルターを確認またはトゥートを削除</a>。
title: フィルターを編集
errors:
deprecated_api_multiple_keywords: これらのパラメータは複数のフィルタキーワードに適用されるため、このアプリケーションから変更できません。 最新のアプリケーションまたはWebインターフェースを使用してください。
@ -1239,9 +1239,9 @@ ja:
keywords:
other: "%{count}件のキーワード"
statuses:
other: "%{count}件の投稿"
other: "%{count}件のトゥート"
statuses_long:
other: "%{count}件の投稿を非表示にしました"
other: "%{count}件のトゥートを非表示にしました"
title: フィルター
new:
save: 新規フィルターを保存
@ -1251,8 +1251,8 @@ ja:
batch:
remove: フィルターから削除する
index:
hint: このフィルターは、他の条件に関係なく個々の投稿を選択する場合に適用されます。Webインターフェースからこのフィルターにさらに投稿を追加できます。
title: フィルターされた投稿
hint: このフィルターは、他の条件に関係なく個々のトゥートを選択する場合に適用されます。Webインターフェースからこのフィルターにさらにトゥートを追加できます。
title: フィルターされたトゥート
generic:
all: すべて
all_items_on_page_selected_html:
@ -1290,14 +1290,14 @@ ja:
overwrite_long: 現在のレコードを新しいもので置き換えます
overwrite_preambles:
blocking_html: "<strong>%{filename}</strong>の<strong>%{total_items}個のアカウント</strong>で<strong>ブロックしたアカウントリストを置き換えます</strong>。"
bookmarks_html: "<strong>%{filename}</strong>の<strong>%{total_items}件の投稿</strong>で<strong>ブックマークの一覧を置き換えます</strong>。"
bookmarks_html: "<strong>%{filename}</strong>の<strong>%{total_items}件のトゥート</strong>で<strong>ブックマークの一覧を置き換えます</strong>。"
domain_blocking_html: "<strong>%{filename}</strong>の<strong>%{total_items}個のドメイン</strong>で<strong>非表示にしたドメインリストを置き換えます</strong>。"
following_html: "<strong>%{filename}</strong>の<strong>%{total_items}個のアカウント</strong>を<strong>フォローします</strong>。また、<strong>この中に含まれていないアカウントのフォローを解除します</strong>。"
lists_html: "<strong>作成済みのリスト</strong>を<strong>%{filename}の内容で置き換えます</strong>。<strong>%{total_items}個のアカウント</strong>が新しいリストに追加されます。"
muting_html: "<strong>%{filename}</strong>の<strong>%{total_items}個のアカウント</strong>で<strong>ミュートしたアカウントリストを置き換えます</strong>。"
preambles:
blocking_html: "<strong>%{filename}</strong>の<strong>%{total_items}個のアカウント</strong>を<strong>ブロックします</strong>。"
bookmarks_html: "<strong>%{filename}</strong>の<strong>%{total_items}件の投稿</strong>を<strong>ブックマークに追加します</strong>。"
bookmarks_html: "<strong>%{filename}</strong>の<strong>%{total_items}件のトゥート</strong>を<strong>ブックマークに追加します</strong>。"
domain_blocking_html: "<strong>%{filename}</strong>の<strong>%{total_items}個のドメイン</strong>を<strong>非表示にします</strong>。"
following_html: "<strong>%{filename}</strong>の<strong>%{total_items}個のアカウント</strong>を<strong>フォローします</strong>。"
lists_html: "<strong>%{filename}</strong>の<strong>%{total_items}個のアカウント</strong>を<strong>リストに追加します</strong>。追加先のリストがない場合は新しく作成されます。"
@ -1433,7 +1433,7 @@ ja:
sign_up:
subject: "%{name}さんがサインアップしました"
favourite:
body: "%{name}さんにお気に入り登録された、あなたの投稿があります:"
body: "%{name}さんにお気に入り登録された、あなたのトゥートがあります:"
subject: "%{name}さんにお気に入りに登録されました"
title: 新たなお気に入り登録
follow:
@ -1453,13 +1453,13 @@ ja:
poll:
subject: "%{name} さんの投票が終了しました"
reblog:
body: "%{name}さんにブーストされた、あなたの投稿があります:"
body: "%{name}さんにブーストされた、あなたのトゥートがあります:"
subject: "%{name}さんにブーストされました"
title: 新たなブースト
status:
subject: "%{name}さんが投稿しました"
subject: "%{name}さんがトゥートしました"
update:
subject: "%{name}さんが投稿を更新しました"
subject: "%{name}さんがトゥートを更新しました"
notifications:
administration_emails: 管理にかかわるメール通知
email_events: メールによる通知
@ -1484,9 +1484,9 @@ ja:
setup: セットアップ
wrong_code: コードが間違っています。サーバーとデバイスの時計にずれがあるかもしれません。
pagination:
newer: 新しい投稿
newer: 新しいトゥート
next:
older: 以前の投稿
older: 以前のトゥート
prev:
truncate: "&hellip;"
polls:
@ -1503,16 +1503,16 @@ ja:
too_many_options: は%{max}個までです
preferences:
other: その他
posting_defaults: デフォルトの投稿設定
posting_defaults: デフォルトのトゥート設定
public_timelines: 公開タイムライン
privacy:
hint_html: "<strong>プロフィールの見えかたや、ほかのユーザーからの見つかりやすさを設定します。</strong>Mastodonには自分のアカウントのことをより多くの人に知ってもらうためのさまざまな機能があり、有効・無効をそれぞれ切り換えられます。使いかたや好みに合わせて調節しましょう。"
privacy: プライバシー
privacy_hint_html: 自分に関する情報をどの程度開示するかについての設定項目です。ユーザーはほかのアカウントのフォロー一覧を見て興味のあるアカウントを探したり、投稿元のアプリ名を見て自分が使いたいアプリの参考にすることがあります。希望に応じて、これらを見られないようにできます。
privacy_hint_html: 自分に関する情報をどの程度開示するかについての設定項目です。ユーザーはほかのアカウントのフォロー一覧を見て興味のあるアカウントを探したり、トゥート元のアプリ名を見て自分が使いたいアプリの参考にすることがあります。希望に応じて、これらを見られないようにできます。
reach: つながりやすさ
reach_hint_html: ほかのユーザーからの見つかりやすさと、フォローされる方法についての設定項目です。「エクスプローラー」やおすすめのユーザーに掲載するか、また新しいフォロワーをどのように受け入れるかをここで変更できます。
search: 被検索性
search_hint_html: 検索での見つかりやすさに関する設定項目です。公開投稿を検索できるようにするかや、Mastodonの外からweb検索でたどり着けるようにするかをここで変更できます。ただし検索エンジンのなかには、この設定に従わずに公開されている情報を利用するものがあるかもしれません。
search_hint_html: 検索での見つかりやすさに関する設定項目です。公開トゥートを検索できるようにするかや、Mastodonの外からweb検索でたどり着けるようにするかをここで変更できます。ただし検索エンジンのなかには、この設定に従わずに公開されている情報を利用するものがあるかもしれません。
title: プライバシーとつながりやすさ
privacy_policy:
title: プライバシーポリシー
@ -1552,11 +1552,11 @@ ja:
rss:
content_warning: '閲覧注意:'
descriptions:
account: "@%{acct}からの公開投稿"
tag: "#%{hashtag}が付けられた公開投稿"
account: "@%{acct}からの公開トゥート"
tag: "#%{hashtag}が付けられた公開トゥート"
scheduled_statuses:
over_daily_limit: その日予約できる投稿数 %{limit}を超えています
over_total_limit: 予約できる投稿数 %{limit}を超えています
over_daily_limit: その日予約できるトゥート数 %{limit}を超えています
over_total_limit: 予約できるトゥート数 %{limit}を超えています
too_soon: より先の時間を指定してください
self_destruct:
lead_html: 残念ながら、<strong>%{domain}</strong> は恒久的に閉鎖されます。ここにお持ちだったアカウントを今後使うことはできませんが、これまでのデータのバックアップを要求することはまだ可能です。
@ -1626,7 +1626,7 @@ ja:
preferences: ユーザー設定
profile: プロフィール
relationships: フォロー・フォロワー
statuses_cleanup: 投稿の自動削除
statuses_cleanup: トゥートの自動削除
strikes: モデレーションストライク
two_factor_authentication: 二要素認証
webauthn_authentication: セキュリティキー
@ -1646,13 +1646,13 @@ ja:
other: '許可されていないハッシュタグが含まれています: %{tags}'
edited_at_html: "%{date} 編集済み"
errors:
in_reply_not_found: あなたが返信しようとしている投稿は存在しないようです。
in_reply_not_found: あなたが返信しようとしているトゥートは存在しないようです。
open_in_web: Webで開く
over_character_limit: 上限は%{max}文字です
pin_errors:
direct: 返信したユーザーのみに表示される投稿はピン留めできません
limit: 固定できる投稿数の上限に達しました
ownership: 他人の投稿を固定することはできません
direct: 返信したユーザーのみに表示されるトゥートはピン留めできません
limit: 固定できるトゥート数の上限に達しました
ownership: 他人のトゥートを固定することはできません
reblog: ブーストを固定することはできません
poll:
total_people:
@ -1674,26 +1674,26 @@ ja:
unlisted: 非収載
unlisted_long: 誰でも見ることができますが、公開タイムラインには表示されません
statuses_cleanup:
enabled: 古い投稿を自動的に削除する
enabled_hint: 設定した期間を過ぎた投稿は、以下の例外に該当しない限り、自動的に削除されます
enabled: 古いトゥートを自動的に削除する
enabled_hint: 設定した期間を過ぎたトゥートは、以下の例外に該当しない限り、自動的に削除されます
exceptions: 例外
explanation: 投稿の削除はサーバーに負荷がかかるため、サーバーが混み合っていないときに時間をかけて行われます。
explanation: トゥートの削除はサーバーに負荷がかかるため、サーバーが混み合っていないときに時間をかけて行われます。
ignore_favs: 設定しない
ignore_reblogs: 設定しない
interaction_exceptions: インタラクションに基づく例外
interaction_exceptions_explanation: お気に入りやブーストの基準値を一度超えてしまった投稿は、基準値を下回った後であっても、削除される保証はありません。
interaction_exceptions_explanation: お気に入りやブーストの基準値を一度超えてしまったトゥートは、基準値を下回った後であっても、削除される保証はありません。
keep_direct: ダイレクトメッセージを保持
keep_direct_hint: ダイレクトメッセージを削除せずに残します
keep_media: メディア付きの投稿を保持
keep_media_hint: メディア付きの投稿を削除せずに残します
keep_pinned: ピン留めされた投稿を保持
keep_pinned_hint: ピン留めされた投稿を削除せずに残します
keep_polls: アンケート付きの投稿を保持
keep_polls_hint: アンケート付きの投稿を削除せずに残します
keep_self_bookmark: ブックマークした投稿を保持
keep_self_bookmark_hint: 自分自身でブックマークした投稿を削除せずに残します
keep_self_fav: お気に入りに登録した投稿を保持
keep_self_fav_hint: 自分自身でお気に入りに登録した投稿を削除せずに残します
keep_media: メディア付きのトゥートを保持
keep_media_hint: メディア付きのトゥートを削除せずに残します
keep_pinned: ピン留めされたトゥートを保持
keep_pinned_hint: ピン留めされたトゥートを削除せずに残します
keep_polls: アンケート付きのトゥートを保持
keep_polls_hint: アンケート付きのトゥートを削除せずに残します
keep_self_bookmark: ブックマークしたトゥートを保持
keep_self_bookmark_hint: 自分自身でブックマークしたトゥートを削除せずに残します
keep_self_fav: お気に入りに登録したトゥートを保持
keep_self_fav_hint: 自分自身でお気に入りに登録したトゥートを削除せずに残します
min_age:
'1209600': 2週間
'15778476': 半年
@ -1703,11 +1703,11 @@ ja:
'604800': 1週間
'63113904': 2年
'7889238': 3ヶ月
min_age_label: 投稿を保持する期間
min_age_label: トゥートを保持する期間
min_favs: お気に入りの基準値
min_favs_hint: この数以上、お気に入り登録された投稿を削除せずに残します。空白にしておくと、お気に入りの数に関わらず投稿を削除します。
min_favs_hint: この数以上、お気に入り登録されたトゥートを削除せずに残します。空白にしておくと、お気に入りの数に関わらずトゥートを削除します。
min_reblogs: ブーストの基準値
min_reblogs_hint: この数以上、ブーストされた投稿を削除せずに残します。空白にしておくと、ブーストされた数に関わらず投稿を削除します。
min_reblogs_hint: この数以上、ブーストされたトゥートを削除せずに残します。空白にしておくと、ブーストされた数に関わらずトゥートを削除します。
stream_entries:
sensitive_content: 閲覧注意
strikes:
@ -1781,26 +1781,26 @@ ja:
spam: スパム
violation: コンテンツは以下のコミュニティガイドラインに違反しています
explanation:
delete_statuses: あなたの投稿のいくつかは、1つ以上のコミュニティガイドラインに違反していることが判明し、%{instance}のモデレータによって削除されました。
delete_statuses: あなたのトゥートのいくつかは、1つ以上のコミュニティガイドラインに違反していることが判明し、%{instance}のモデレータによって削除されました。
disable: アカウントは使用できませんが、プロフィールやその他のデータはそのまま残ります。 データのバックアップをリクエストしたり、アカウント設定を変更したり、アカウントを削除したりできます。
mark_statuses_as_sensitive: あなたのいくつかの投稿は、%{instance}のモデレータによって閲覧注意としてマークされています。これは、プレビューが表示される前にユーザが投稿内のメディアをタップする必要があることを意味します。あなたは将来投稿する際に自分自身でメディアを閲覧注意としてマークすることができます。
mark_statuses_as_sensitive: あなたのいくつかのトゥートは、%{instance}のモデレータによって閲覧注意としてマークされています。これは、プレビューが表示される前にユーザがトゥート内のメディアをタップする必要があることを意味します。あなたは将来トゥートする際に自分自身でメディアを閲覧注意としてマークすることができます。
sensitive: 今後、アップロードされたすべてのメディアファイルは閲覧注意としてマークされ、クリック解除式の警告で覆われるようになります。
silence: アカウントが制限されています。このサーバーでは既にフォローしている人だけがあなたの投稿を見ることができます。 様々な発見機能から除外されるかもしれません。他の人があなたを手動でフォローすることは可能です。
silence: アカウントが制限されています。このサーバーでは既にフォローしている人だけがあなたのトゥートを見ることができます。 様々な発見機能から除外されるかもしれません。他の人があなたを手動でフォローすることは可能です。
suspend: アカウントは使用できなくなり、プロフィールなどのデータにもアクセスできなくなります。約30日後にデータが完全に削除されるまでは、ログインしてデータのバックアップを要求することができますが、アカウントの停止回避を防ぐために一部の基本データを保持します。
reason: '理由:'
statuses: '投稿:'
statuses: 'トゥート:'
subject:
delete_statuses: "%{acct}さんの投稿が削除されました"
delete_statuses: "%{acct}さんのトゥートが削除されました"
disable: あなたのアカウント %{acct}は凍結されました
mark_statuses_as_sensitive: あなたの%{acct}の投稿は閲覧注意としてマークされました
mark_statuses_as_sensitive: あなたの%{acct}のトゥートは閲覧注意としてマークされました
none: "%{acct}に対する警告"
sensitive: あなたの%{acct}の投稿はこれから閲覧注意としてマークされます
sensitive: あなたの%{acct}のトゥートはこれから閲覧注意としてマークされます
silence: あなたのアカウント %{acct}はサイレンスにされました
suspend: あなたのアカウント %{acct}は停止されました
title:
delete_statuses: 投稿が削除されました
delete_statuses: トゥートが削除されました
disable: アカウントが凍結されました
mark_statuses_as_sensitive: 閲覧注意としてマークされた投稿
mark_statuses_as_sensitive: 閲覧注意としてマークされたトゥート
none: 警告
sensitive: 閲覧注意としてマークされたアカウント
silence: アカウントがサイレンスにされました
@ -1812,7 +1812,7 @@ ja:
新しいフォロワーからフォローリクエストを承認する前に、オプトインで確認できます。
explanation: 始めるにあたってのアドバイスです
final_action: 始めましょう
final_step: 'さあ、始めましょう! たとえフォロワーがまだいなくても、あなたの公開した投稿はローカルタイムラインやハッシュタグなどを通じて誰かの目にとまるはずです。自己紹介をしたいときには #introductions ハッシュタグが便利かもしれません。'
final_step: 'さあ、始めましょう! たとえフォロワーがまだいなくても、あなたの公開したトゥートはローカルタイムラインやハッシュタグなどを通じて誰かの目にとまるはずです。自己紹介をしたいときには #introductions ハッシュタグが便利かもしれません。'
full_handle: あなたの正式なユーザーID
full_handle_hint: 別のサーバーの友達とフォローやメッセージをやり取りする際には、これを伝えることになります。
subject: Mastodonへようこそ

View File

@ -215,7 +215,11 @@ en:
setting_display_media_show_all: Show all
setting_expand_spoilers: Always expand posts marked with content warnings
setting_hide_network: Hide your social graph
setting_noindex: Opt-out of search engine indexing
setting_place_tab_bar_at_bottom: Place the tab bar at the bottom
setting_reduce_motion: Reduce motion in animations
setting_show_application: Disclose application used to send posts
setting_show_status_reaction: Show emoji reactions
setting_system_font_ui: Use system's default font
setting_theme: Site theme
setting_trends: Show today's trends

View File

@ -15,18 +15,18 @@ ja:
account_migration:
acct: 引っ越し先のユーザー名@ドメインを指定してください
account_warning_preset:
text: URL、ハッシュタグ、メンションなど、投稿に用いる構文が使用できます
text: URL、ハッシュタグ、メンションなど、トゥートに用いる構文が使用できます
title: オプションです。受信者には表示されません。
admin_account_action:
include_statuses: ユーザーは取られた制限や警告の原因となった投稿を確認できるようになります
include_statuses: ユーザーは取られた制限や警告の原因となったトゥートを確認できるようになります
send_email_notification: ユーザーは自分のアカウントに何が起こったのか説明を受け取ります
text_html: オプションです。投稿に用いる構文を使うことができます。簡略化のため<a href="%{path}">プリセット警告文を追加</a>することができます
text_html: オプションです。トゥートに用いる構文を使うことができます。簡略化のため<a href="%{path}">プリセット警告文を追加</a>することができます
type_html: "<strong>%{acct}</strong>さんに対し、何を行うか選択してください"
types:
disable: ユーザーが自分のアカウントを使用できないようにします。コンテンツを削除したり非表示にすることはありません。
none: これを使用すると、他の操作をせずにユーザーに警告を送信できます。
sensitive: このユーザーが添付したメディアを強制的に閲覧注意にする
silence: ユーザーによる公開投稿を禁止し、フォローしていない人に投稿や通知が表示されないようにします。また、このアカウントに対するすべての通報をクローズします。
silence: ユーザーによる公開トゥートを禁止し、フォローしていない人にトゥートや通知が表示されないようにします。また、このアカウントに対するすべての通報をクローズします。
suspend: このアカウントによるすべての活動を禁止し、コンテンツを削除します。この操作は30日以内であれば取り消しが可能です。また、このアカウントに対するすべての通報をクローズします。
warning_preset_id: オプションです。プリセット警告文の末尾に任意の文字列を追加することができます
announcement:
@ -34,7 +34,7 @@ ja:
ends_at: オプションです。指定すると、お知らせの掲載はその日時で自動的に終了します
scheduled_at: お知らせを今すぐ掲載する場合は空欄にしてください
starts_at: オプションです。お知らせしたい事柄の期間が決まっている場合に使用します
text: 投稿と同じ構文を使用できます。アナウンスが占める画面のスペースに注意してください
text: トゥートと同じ構文を使用できます。アナウンスが占める画面のスペースに注意してください
appeal:
text: 一度だけ異議を申し立てることができます
defaults:
@ -48,17 +48,20 @@ ja:
email: 確認のメールが送信されます
header: "%{size}までのPNG、GIF、JPGが利用可能です。 %{dimensions}pxまで縮小されます"
inbox_url: 使用したいリレーサーバーのトップページからURLをコピーします
irreversible: フィルターが後で削除されても、除外された投稿は元に戻せなくなります
irreversible: フィルターが後で削除されても、除外されたトゥートは元に戻せなくなります
locale: ユーザーインターフェース、メールやプッシュ通知の言語
password: 少なくとも8文字は入力してください
phrase: 投稿内容の大文字小文字や閲覧注意に関係なく一致
phrase: トゥート内容の大文字小文字や閲覧注意に関係なく一致
scopes: アプリの API に許可するアクセス権を選択してください。最上位のスコープを選択する場合、個々のスコープを選択する必要はありません。
setting_aggregate_reblogs: 最近ブーストされた投稿が新たにブーストされても表示しません (設定後受信したものにのみ影響)
setting_aggregate_reblogs: 最近ブーストされたトゥートが新たにブーストされても表示しません (設定後受信したものにのみ影響)
setting_always_send_emails: 通常、Mastodon からメール通知は行われません。
setting_default_sensitive: 閲覧注意状態のメディアはデフォルトでは内容が伏せられ、クリックして初めて閲覧できるようになります
setting_display_media_default: 閲覧注意としてマークされたメディアは隠す
setting_display_media_hide_all: メディアを常に隠す
setting_display_media_show_all: メディアを常に表示する
setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします
setting_noindex: 公開プロフィールおよび各トゥートページに影響します
setting_show_application: トゥートするのに使用したアプリがトゥートの詳細ビューに表示されるようになります
setting_use_blurhash: ぼかしはメディアの色を元に生成されますが、細部は見えにくくなっています
setting_use_pending_items: 新着があってもタイムラインを自動的にスクロールしないようにします
username: アルファベット大文字と小文字、数字、アンダーバー「_」が使えます
@ -71,16 +74,16 @@ ja:
featured_tag:
name: 最近使用したハッシュタグ
filters:
action: 投稿がフィルタに一致したときに実行するアクションを選択
action: トゥートがフィルタに一致したときに実行するアクションを選択
actions:
hide: フィルタに一致した投稿を完全に非表示にします
warn: フィルタに一致した投稿を非表示にし、フィルタのタイトルを含む警告を表示します
hide: フィルタに一致したトゥートを完全に非表示にします
warn: フィルタに一致したトゥートを非表示にし、フィルタのタイトルを含む警告を表示します
form_admin_settings:
activity_api_enabled: 週単位でローカルで公開された投稿数、アクティブユーザー数、新規登録者数を表示します
activity_api_enabled: 週単位でローカルで公開されたトゥート数、アクティブユーザー数、新規登録者数を表示します
backups_retention_period: 生成されたユーザーのアーカイブを指定した日数の間保持します。
bootstrap_timeline_accounts: これらのアカウントは、新しいユーザー向けのおすすめユーザーの一番上にピン留めされます。
closed_registrations_message: アカウント作成を停止している時に表示されます
content_cache_retention_period: 指定した日数が経過した他のサーバーの投稿とブーストを削除します。削除された投稿は再取得できない場合があります。削除された投稿についたブックマークやお気に入り、ブーストも失われ、元に戻せません。
content_cache_retention_period: 指定した日数が経過した他のサーバーのトゥートとブーストを削除します。削除されたトゥートは再取得できない場合があります。削除されたトゥートについたブックマークやお気に入り、ブーストも失われ、元に戻せません。
custom_css: ウェブ版のMastodonでカスタムスタイルを適用できます。
mascot: 上級者向けWebインターフェースのイラストを上書きします。
media_cache_retention_period: 正の値に設定されている場合、ダウンロードされたメディアファイルは指定された日数の後に削除され、リクエストに応じて再ダウンロードされます。
@ -96,9 +99,9 @@ ja:
status_page_url: 障害発生時などにユーザーがサーバーの状態を確認できるページのURL
theme: ログインしていない人と新規ユーザーに表示されるテーマ。
thumbnail: サーバー情報と共に表示される、アスペクト比が約 2:1 の画像。
timeline_preview: ログインしていないユーザーがサーバー上の最新の公開投稿を閲覧できるようにします。
timeline_preview: ログインしていないユーザーがサーバー上の最新の公開トゥートを閲覧できるようにします。
trendable_by_default: トレンドの審査を省略します。トレンドは掲載後でも個別に除外できます。
trends: トレンドは、サーバー上で人気を集めている投稿、ハッシュタグ、ニュース記事などが表示されます。
trends: トレンドは、サーバー上で人気を集めているトゥート、ハッシュタグ、ニュース記事などが表示されます。
trends_as_landing_page: ログインしていないユーザーに対して、サーバーの説明の代わりにトレンドコンテンツを表示します。トレンドを有効にする必要があります。
form_challenge:
current_password: セキュリティ上重要なエリアにアクセスしています
@ -126,7 +129,7 @@ ja:
tag:
name: 視認性向上などのためにアルファベット大文字小文字の変更のみ行うことができます
user:
chosen_languages: 選択すると、選択した言語の投稿のみが公開タイムラインに表示されるようになります
chosen_languages: 選択すると、選択した言語のトゥートのみが公開タイムラインに表示されるようになります
role: このロールはユーザーが持つ権限を管理します
user_role:
color: UI 全体でロールの表示に使用される色16進数RGB形式
@ -155,7 +158,7 @@ ja:
text: プリセット警告文
title: タイトル
admin_account_action:
include_statuses: 通報された投稿をメールに含める
include_statuses: 通報されたトゥートをメールに含める
send_email_notification: メールでユーザーに通知
text: カスタム警告文
type: アクション
@ -204,17 +207,19 @@ ja:
setting_always_send_emails: 常にメール通知を送信する
setting_auto_play_gif: アニメーションGIFを自動再生する
setting_boost_modal: ブーストする前に確認ダイアログを表示する
setting_default_language: 投稿する言語
setting_default_privacy: 投稿の公開範囲
setting_default_language: トゥートする言語
setting_default_privacy: トゥートの公開範囲
setting_default_sensitive: メディアを常に閲覧注意としてマークする
setting_delete_modal: 投稿を削除する前に確認ダイアログを表示する
setting_delete_modal: トゥートを削除する前に確認ダイアログを表示する
setting_disable_swiping: スワイプでの切り替えを無効にする
setting_display_media: メディアの表示
setting_display_media_default: 標準
setting_display_media_hide_all: 非表示
setting_display_media_show_all: 表示
setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する
setting_expand_spoilers: 閲覧注意としてマークされたトゥートを常に展開する
setting_hide_network: 繋がりを隠す
setting_noindex: 検索エンジンによるインデックスを拒否する
setting_place_tab_bar_at_bottom: タブバーを下に配置する
setting_reduce_motion: アニメーションの動きを減らす
setting_system_font_ui: システムのデフォルトフォントを使う
setting_theme: サイトテーマ
@ -289,7 +294,7 @@ ja:
follow_request: フォローリクエストを受けた時
mention: 返信が来た時
pending_account: 新しいアカウントの承認が必要な時
reblog: 投稿がブーストされた時
reblog: トゥートがブーストされた時
report: 新しい通報が送信された時
software_updates:
all: すべてのアップデートを通知する
@ -307,7 +312,7 @@ ja:
listable: 検索とディレクトリへの使用を許可する
name: ハッシュタグ
trendable: トレンドへの表示を許可する
usable: 投稿への使用を許可する
usable: トゥートへの使用を許可する
user:
role: ロール
time_zone: タイムゾーン

View File

@ -1,3 +1,4 @@
default: styles/application.scss
contrast: styles/contrast.scss
mastodon-light: styles/mastodon-light.scss
light-pink: styles/light-pink.scss

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddQuoteIdToStatuses < ActiveRecord::Migration[5.1]
def change
add_column :statuses, :quote_id, :bigint, null: true, default: nil
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddIndexToStatusesQuoteId < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
add_index :statuses, :quote_id, algorithm: :concurrently
end
end

View File

@ -27,6 +27,7 @@ class MoveUserSettings < ActiveRecord::Migration[6.1]
use_blurhash: 'web.use_blurhash',
use_pending_items: 'web.use_pending_items',
crop_images: 'web.crop_images',
place_tab_bar_at_bottom: 'web.place_tab_bar_at_bottom',
notification_emails: {
follow: 'notification_emails.follow',
reblog: 'notification_emails.reblog',

View File

@ -999,6 +999,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_11_033014) do
t.bigint "account_id", null: false
t.bigint "application_id"
t.bigint "in_reply_to_account_id"
t.bigint "quote_id"
t.bigint "poll_id"
t.datetime "deleted_at", precision: nil
t.datetime "edited_at", precision: nil
@ -1011,6 +1012,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_11_033014) do
t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id", where: "(in_reply_to_account_id IS NOT NULL)"
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", where: "(in_reply_to_id IS NOT NULL)"
t.index ["quote_id"], name: "index_statuses_on_quote_id"
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
t.index ["uri"], name: "index_statuses_on_uri", unique: true, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)"
end

View File

@ -31,6 +31,7 @@ class Sanitize
next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes
next true if /^(mention|hashtag)$/.match?(e) # semantic classes
next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes
next true if /^quote-inline$/.match?(e) # quote inline classes
end
node['class'] = class_list.join(' ')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB