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 def index
cache_if_unauthenticated! cache_if_unauthenticated!
@statuses = load_statuses @statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, 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)
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 end
private private

View File

@ -6,8 +6,12 @@ class Api::V1::BookmarksController < Api::BaseController
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
@statuses = load_statuses @statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, 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)
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 end
private private

View File

@ -6,8 +6,12 @@ class Api::V1::FavouritesController < Api::BaseController
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
@statuses = load_statuses @statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, 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)
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 end
private private

View File

@ -47,10 +47,14 @@ class Api::V1::StatusesController < Api::BaseController
loaded_ancestors = cache_collection(ancestors_results, Status) loaded_ancestors = cache_collection(ancestors_results, Status)
loaded_descendants = cache_collection(descendants_results, Status) loaded_descendants = cache_collection(descendants_results, Status)
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants) @context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
statuses = [@status] + @context.ancestors + @context.descendants statuses = [@status] + @context.ancestors + @context.descendants
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 end
def create def create
@ -68,7 +72,8 @@ class Api::V1::StatusesController < Api::BaseController
poll: status_params[:poll], poll: status_params[:poll],
allowed_mentions: status_params[:allowed_mentions], allowed_mentions: status_params[:allowed_mentions],
idempotency: request.headers['Idempotency-Key'], idempotency: request.headers['Idempotency-Key'],
with_rate_limit: true with_rate_limit: true,
quote_id: status_params[:quote_id].presence
) )
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
@ -138,6 +143,7 @@ class Api::V1::StatusesController < Api::BaseController
:visibility, :visibility,
:language, :language,
:scheduled_at, :scheduled_at,
:quote_id,
allowed_mentions: [], allowed_mentions: [],
media_ids: [], media_ids: [],
media_attributes: [ media_attributes: [

View File

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

View File

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

View File

@ -7,8 +7,13 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
def show def show
cache_if_unauthenticated! cache_if_unauthenticated!
@statuses = load_statuses @statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, 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)
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 end
private private

View File

@ -8,8 +8,13 @@ class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController
def show def show
cache_if_unauthenticated! cache_if_unauthenticated!
@statuses = load_statuses @statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, 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)
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 end
private 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 }
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
skip_before_action :require_functional! skip_before_action :require_functional!
include Localized include Localized

View File

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

View File

@ -19,7 +19,12 @@ module FormattingHelper
module_function :extract_status_plain_text module_function :extract_status_plain_text
def status_content_format(status) def status_content_format(status)
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : [])) 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 end
def rss_status_content_format(status) 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 api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedStatuses } from './importer'; import { importFetchedStatuses } from './importer';
import { uniq } from '../utils/uniq';
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST'; export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS'; 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 => { api(getState).get('/api/v1/bookmarks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next'); const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data)); 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)); dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => { }).catch(error => {
dispatch(fetchBookmarkedStatusesFail(error)); dispatch(fetchBookmarkedStatusesFail(error));
@ -62,6 +65,7 @@ export function expandBookmarkedStatuses() {
api(getState).get(url).then(response => { api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next'); const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data)); 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)); dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => { }).catch(error => {
dispatch(expandBookmarkedStatusesFail(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_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY'; export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_MENTION = 'COMPOSE_MENTION';
export const COMPOSE_RESET = 'COMPOSE_RESET'; export const COMPOSE_RESET = 'COMPOSE_RESET';
@ -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() { export function resetCompose() {
return { return {
type: COMPOSE_RESET, type: COMPOSE_RESET,
@ -208,6 +227,7 @@ export function submitCompose(routerHistory) {
visibility: getState().getIn(['compose', 'privacy']), visibility: getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null), poll: getState().getIn(['compose', 'poll'], null),
language: getState().getIn(['compose', 'language']), language: getState().getIn(['compose', 'language']),
quote_id: getState().getIn(['compose', 'quote_from'], null),
}, },
headers: { headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),

View File

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

View File

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

View File

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

View File

@ -28,6 +28,9 @@ export const STATUS_REVEAL = 'STATUS_REVEAL';
export const STATUS_HIDE = 'STATUS_HIDE'; export const STATUS_HIDE = 'STATUS_HIDE';
export const STATUS_COLLAPSE = 'STATUS_COLLAPSE'; export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
export const QUOTE_REVEAL = 'QUOTE_REVEAL';
export const QUOTE_HIDE = 'QUOTE_HIDE';
export const REDRAFT = 'REDRAFT'; export const REDRAFT = 'REDRAFT';
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST'; export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
@ -348,3 +351,25 @@ export const undoStatusTranslation = (id, pollId) => ({
id, id,
pollId, 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 { compareId } from 'mastodon/compare_id';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import { uniq } from '../utils/uniq';
import { fetchRelationships } from './accounts';
import { importFetchedStatus, importFetchedStatuses } from './importer'; import { importFetchedStatus, importFetchedStatuses } from './importer';
import { submitMarkers } from './markers'; import { submitMarkers } from './markers';
@ -45,6 +48,7 @@ export function updateTimeline(timeline, status, accept) {
} }
dispatch(importFetchedStatus(status)); 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({ dispatch({
type: TIMELINE_UPDATE, 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'); const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data)); 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)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
if (timelineId === 'home' && !isLoadingMore && !isLoadingRecent) { 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 = () => ( export const WordmarkLogo: React.FC = () => (
<svg viewBox='0 0 261 66' className='logo logo--wordmark' role='img'> <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 = () => ( export const SymbolLogo: React.FC = () => (
<img src={logo} alt='Mastodon' className='logo logo--icon' /> <img src={logo} alt='Mastodon' className='logo logo--icon' />
); );

View File

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

View File

@ -35,6 +35,29 @@ import { VisibilityIcon } from './visibility_icon';
const domParser = new DOMParser(); 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) => { export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']); const displayName = status.getIn(['account', 'display_name']);
@ -76,6 +99,60 @@ const messages = defineMessages({
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, 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 { class Status extends ImmutablePureComponent {
static propTypes = { static propTypes = {
@ -102,7 +179,9 @@ class Status extends ImmutablePureComponent {
onToggleCollapsed: PropTypes.func, onToggleCollapsed: PropTypes.func,
onTranslate: PropTypes.func, onTranslate: PropTypes.func,
onInteractionModal: PropTypes.func, onInteractionModal: PropTypes.func,
onQuoteToggleHidden: PropTypes.func,
muted: PropTypes.bool, muted: PropTypes.bool,
quoteMuted: PropTypes.bool,
hidden: PropTypes.bool, hidden: PropTypes.bool,
unread: PropTypes.bool, unread: PropTypes.bool,
onMoveUp: PropTypes.func, onMoveUp: PropTypes.func,
@ -118,6 +197,7 @@ class Status extends ImmutablePureComponent {
inUse: PropTypes.bool, inUse: PropTypes.bool,
available: PropTypes.bool, available: PropTypes.bool,
}), }),
contextType: PropTypes.string,
...WithOptionalRouterPropTypes, ...WithOptionalRouterPropTypes,
}; };
@ -130,10 +210,12 @@ class Status extends ImmutablePureComponent {
'hidden', 'hidden',
'unread', 'unread',
'pictureInPicture', 'pictureInPicture',
'quoteMuted',
]; ];
state = { state = {
showMedia: defaultMediaVisibility(this.props.status), showMedia: defaultMediaVisibility(this.props.status),
showQuoteMedia: defaultMediaVisibility(this.props.status ? this.props.status.get('quote', null) : null),
statusId: undefined, statusId: undefined,
forceFilter: undefined, forceFilter: undefined,
}; };
@ -142,6 +224,7 @@ class Status extends ImmutablePureComponent {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) { if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return { return {
showMedia: defaultMediaVisibility(nextProps.status), showMedia: defaultMediaVisibility(nextProps.status),
showQuoteMedia: defaultMediaVisibility(nextProps.status ? nextProps.status.get('quote', null) : null),
statusId: nextProps.status.get('id'), statusId: nextProps.status.get('id'),
}; };
} else { } else {
@ -153,6 +236,10 @@ class Status extends ImmutablePureComponent {
this.setState({ showMedia: !this.state.showMedia }); this.setState({ showMedia: !this.state.showMedia });
}; };
handleToggleQuoteMediaVisibility = () => {
this.setState({ showQuoteMedia: !this.state.showQuoteMedia });
}
handleClick = e => { handleClick = e => {
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) { if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
return; return;
@ -182,6 +269,24 @@ class Status extends ImmutablePureComponent {
this._openProfile(proper); 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 = () => { handleExpandedToggle = () => {
this.props.onToggleHidden(this._properStatus()); this.props.onToggleHidden(this._properStatus());
}; };
@ -194,6 +299,10 @@ class Status extends ImmutablePureComponent {
this.props.onTranslate(this._properStatus()); this.props.onTranslate(this._properStatus());
}; };
handleExpandedQuoteToggle = () => {
this.props.onQuoteToggleHidden(this._properStatus());
}
getAttachmentAspectRatio () { getAttachmentAspectRatio () {
const attachments = this._properStatus().get('media_attachments'); const attachments = this._properStatus().get('media_attachments');
@ -348,7 +457,7 @@ class Status extends ImmutablePureComponent {
}; };
render () { 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; let { status, account, ...other } = this.props;
@ -370,7 +479,7 @@ class Status extends ImmutablePureComponent {
openMedia: this.handleHotkeyOpenMedia, openMedia: this.handleHotkeyOpenMedia,
}; };
let media, statusAvatar, prepend, rebloggedByText; let prepend, rebloggedByText;
if (hidden) { if (hidden) {
return ( return (
@ -445,99 +554,117 @@ class Status extends ImmutablePureComponent {
</div> </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')) { if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
media = <PictureInPicturePlaceholder aspectRatio={this.getAttachmentAspectRatio()} />; const attachment = status.getIn(['media_attachments', 0]);
} else if (status.get('media_attachments').size > 0) { const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
const language = status.getIn(['translation', 'language']) || status.get('language');
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { return (
const attachment = status.getIn(['media_attachments', 0]); <Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); {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 = ( return (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} > <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => ( {Component => (
<Component <Component
src={attachment.get('url')} preview={attachment.get('preview_url')}
alt={description} frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
lang={language} aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])} blurhash={attachment.get('blurhash')}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])} src={attachment.get('url')}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])} alt={description}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])} lang={language}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)} sensitive={status.get('sensitive')}
width={this.props.cachedMediaWidth} onOpenVideo={this.handleOpenVideo}
height={110} deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
cacheWidth={this.props.cacheMediaWidth} visible={this.state.showMedia}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined} onToggleVisibility={this.handleToggleMediaVisibility}
sensitive={status.get('sensitive')} quote={quote}
blurhash={attachment.get('blurhash')} />
visible={this.state.showMedia} )}
onToggleVisibility={this.handleToggleMediaVisibility} </Bundle>
/> );
)} } else {
</Bundle> return (
); <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { {Component => (
const attachment = status.getIn(['media_attachments', 0]); <Component
const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); media={status.get('media_attachments')}
lang={language}
media = ( sensitive={status.get('sensitive')}
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > height={110}
{Component => ( onOpenMedia={this.handleOpenMedia}
<Component cacheWidth={this.props.cacheMediaWidth}
preview={attachment.get('preview_url')} defaultWidth={this.props.cachedMediaWidth}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])} visible={this.state.showMedia}
aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`} onToggleVisibility={this.handleToggleMediaVisibility}
blurhash={attachment.get('blurhash')} quote={quote}
src={attachment.get('url')} />
alt={description} )}
lang={language} </Bundle>
sensitive={status.get('sensitive')} );
onOpenVideo={this.handleOpenVideo} }
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined} } else if (status.get('spoiler_text').length === 0 && status.get('card')) {
visible={this.state.showMedia} return (
onToggleVisibility={this.handleToggleMediaVisibility} <Card
/> onOpenMedia={this.handleOpenMedia}
)} card={status.get('card')}
</Bundle> compact
); sensitive={status.get('sensitive')}
} else { quote={quote}
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>
); );
} }
} else if (status.get('spoiler_text').length === 0 && status.get('card')) { return null;
media = ( };
<Card
onOpenMedia={this.handleOpenMedia}
card={status.get('card')}
compact
sensitive={status.get('sensitive')}
/>
);
}
if (account === undefined || account === null) { const statusAvatar = (status, account) => {
statusAvatar = <Avatar account={status.get('account')} size={46} />; if (account === undefined || account === null) {
} else { return <Avatar account={status.get('account')} size={46} />;
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />; } 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 {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0; 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>} <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>
<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'> <div className='status__avatar'>
{statusAvatar} {statusAvatar}
</div> </div>
<DisplayName account={status.get('account')} /> <DisplayName account={status.get('account')} />
</a> </a> */}
{identity(status, account, false)}
</div> </div>
<StatusContent <StatusContent
@ -577,8 +706,10 @@ class Status extends ImmutablePureComponent {
{...statusContentProps} {...statusContentProps}
/> />
{media} {media(status)}
{quote(status, this.props.muted, quoteMuted, this.handleQuoteClick, this.handleExpandedQuoteToggle, identity, media, this.props, contextType)}
{expanded && hashtagBar} {expanded && hashtagBar}
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} /> <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 ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.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 StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react'; import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.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' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' }, favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' }, removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
@ -85,6 +88,7 @@ class StatusActionBar extends ImmutablePureComponent {
onReply: PropTypes.func, onReply: PropTypes.func,
onFavourite: PropTypes.func, onFavourite: PropTypes.func,
onReblog: PropTypes.func, onReblog: PropTypes.func,
onQuote: PropTypes.func,
onDelete: PropTypes.func, onDelete: PropTypes.func,
onDirect: PropTypes.func, onDirect: PropTypes.func,
onMention: PropTypes.func, onMention: PropTypes.func,
@ -155,6 +159,10 @@ class StatusActionBar extends ImmutablePureComponent {
} }
}; };
handleQuoteClick = () => {
this.props.onQuote(this.props.status, this.props.history);
}
handleBookmarkClick = () => { handleBookmarkClick = () => {
this.props.onBookmark(this.props.status); this.props.onBookmark(this.props.status);
}; };
@ -248,6 +256,14 @@ class StatusActionBar extends ImmutablePureComponent {
this.props.onFilter(); this.props.onFilter();
}; };
static quoteTitle = (intl, messages, publicStatus) => {
if (publicStatus) {
return intl.formatMessage(messages.quote);
} else {
return intl.formatMessage(messages.cannot_quote);
}
}
render () { render () {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
const { signedIn, permissions } = this.context.identity; 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='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} /> <IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} /> <IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} 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} /> <IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
{filterButton} {filterButton}

View File

@ -15,6 +15,7 @@ import PollContainer from 'mastodon/containers/poll_container';
import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state'; import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) 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, onCollapsedToggle: PropTypes.func,
languages: ImmutablePropTypes.map, languages: ImmutablePropTypes.map,
intl: PropTypes.object, intl: PropTypes.object,
quote: PropTypes.bool,
// from react-router // from react-router
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,
location: PropTypes.object.isRequired, location: PropTypes.object.isRequired,
@ -130,12 +132,12 @@ class StatusContent extends PureComponent {
} }
if (status.get('collapsed', null) === null && onCollapsedToggle) { if (status.get('collapsed', null) === null && onCollapsedToggle) {
const { collapsible, onClick } = this.props; const { collapsible, onClick, quote } = this.props;
const collapsed = const collapsed =
collapsible collapsible
&& onClick && onClick
&& node.clientHeight > MAX_HEIGHT && node.clientHeight > (quote ? QUOTE_MAX_HEIGHT : MAX_HEIGHT)
&& status.get('spoiler_text').length === 0; && status.get('spoiler_text').length === 0;
onCollapsedToggle(collapsed); onCollapsedToggle(collapsed);
@ -239,7 +241,7 @@ class StatusContent extends PureComponent {
}; };
render () { 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 hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed'); const renderReadMore = this.props.onClick && status.get('collapsed');
@ -270,6 +272,12 @@ class StatusContent extends PureComponent {
<PollContainer pollId={status.get('poll')} lang={language} /> <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) { if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = ''; let mentionsPlaceholder = '';

View File

@ -11,6 +11,7 @@ import { initBlockModal } from '../actions/blocks';
import { initBoostModal } from '../actions/boosts'; import { initBoostModal } from '../actions/boosts';
import { import {
replyCompose, replyCompose,
quoteCompose,
mentionCompose, mentionCompose,
directCompose, directCompose,
} from '../actions/compose'; } from '../actions/compose';
@ -45,6 +46,8 @@ import {
editStatus, editStatus,
translateStatus, translateStatus,
undoStatusTranslation, undoStatusTranslation,
hideQuote,
revealQuote,
} from '../actions/statuses'; } from '../actions/statuses';
import Status from '../components/status'; import Status from '../components/status';
import { boostModal, deleteModal } from '../initial_state'; 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.' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
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' }, editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' }, 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) { onFavourite (status) {
if (status.get('favourited')) { if (status.get('favourited')) {
dispatch(unfavourite(status)); dispatch(unfavourite(status));
@ -253,6 +274,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed)); 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) { onBlockDomain (domain) {
dispatch(openModal({ dispatch(openModal({
modalType: 'CONFIRM', modalType: 'CONFIRM',

View File

@ -61,6 +61,7 @@ class Audio extends PureComponent {
volume: PropTypes.number, volume: PropTypes.number,
muted: PropTypes.bool, muted: PropTypes.bool,
deployPictureInPicture: PropTypes.func, deployPictureInPicture: PropTypes.func,
quote: PropTypes.bool,
}; };
state = { state = {
@ -105,7 +106,11 @@ class Audio extends PureComponent {
_setDimensions () { _setDimensions () {
const width = this.player.offsetWidth; 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) { if (this.props.cacheWidth) {
this.props.cacheWidth(width); this.props.cacheWidth(width);

View File

@ -39,6 +39,9 @@ const messages = defineMessages({
publish: { id: 'compose_form.publish', defaultMessage: 'Post' }, publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Update' }, saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Update' },
reply: { id: 'compose_form.reply', defaultMessage: 'Reply' }, 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 { class ComposeForm extends ImmutablePureComponent {
@ -71,6 +74,9 @@ class ComposeForm extends ImmutablePureComponent {
singleColumn: PropTypes.bool, singleColumn: PropTypes.bool,
lang: PropTypes.string, lang: PropTypes.string,
maxChars: PropTypes.number, maxChars: PropTypes.number,
onGojiSubmit: PropTypes.func.isRequired,
onHarukinSubmit: PropTypes.func.isRequired,
onRisaSubmit: PropTypes.func.isRequired,
...WithOptionalRouterPropTypes ...WithOptionalRouterPropTypes
}; };
@ -224,6 +230,10 @@ class ComposeForm extends ImmutablePureComponent {
this.props.onPickEmoji(position, data, needsSpace); 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 () { render () {
const { intl, onPaste, autoFocus, withoutNavigation, maxChars } = this.props; const { intl, onPaste, autoFocus, withoutNavigation, maxChars } = this.props;
const { highlighted } = this.state; const { highlighted } = this.state;
@ -311,6 +321,10 @@ class ComposeForm extends ImmutablePureComponent {
</div> </div>
</div> </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> </form>
); );
} }

View File

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

View File

@ -10,6 +10,11 @@ import {
insertEmojiCompose, insertEmojiCompose,
uploadCompose, uploadCompose,
} from '../../../actions/compose'; } from '../../../actions/compose';
import {
submitGoji,
submitHarukin,
submitRisa
} from '../../../actions/UtilBtns';
import ComposeForm from '../components/compose_form'; import ComposeForm from '../components/compose_form';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
@ -65,6 +70,18 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(insertEmojiCompose(position, data, needsSpace)); 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); 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 ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react'; import StarIcon from '@/material-icons/400-24px/star.svg?react';
import { initBoostModal } from 'mastodon/actions/boosts'; 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 { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import { IconButton } from 'mastodon/components/icon_button'; 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' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' }, favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, 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?' }, 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' }, 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 => { handleOpenClick = e => {
if (e.button !== 0 || !history) { if (e.button !== 0 || !history) {
return; 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 ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.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 StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.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 { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { IconButton } from '../../../components/icon_button'; import { IconButton } from '../../../components/icon_button';
import StatusActionBar from '../../../components/status_action_bar';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { me } from '../../../initial_state'; import { me } from '../../../initial_state';
@ -39,6 +41,8 @@ const messages = defineMessages({
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' }, favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
more: { id: 'status.more', defaultMessage: 'More' }, more: { id: 'status.more', defaultMessage: 'More' },
@ -77,6 +81,7 @@ class ActionBar extends PureComponent {
relationship: ImmutablePropTypes.map, relationship: ImmutablePropTypes.map,
onReply: PropTypes.func.isRequired, onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired,
onQuote: PropTypes.func,
onFavourite: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
@ -105,6 +110,10 @@ class ActionBar extends PureComponent {
this.props.onReblog(this.props.status, e); this.props.onReblog(this.props.status, e);
}; };
handleQuoteClick = () => {
this.props.onQuote(this.props.status, this.props.history);
}
handleFavouriteClick = () => { handleFavouriteClick = () => {
this.props.onFavourite(this.props.status); 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 title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div> <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'><IconButton disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='quote' iconComponent={QuoteIcon} onClick={this.handleQuoteClick} /></div>
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div>
<div className='detailed-status__action-bar-dropdown'> <div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer icon='ellipsis-h' iconComponent={MoreHorizIcon} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} /> <DropdownMenuContainer icon='ellipsis-h' iconComponent={MoreHorizIcon} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
</div> </div>

View File

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

View File

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

View File

@ -27,6 +27,8 @@ import {
deleteStatus, deleteStatus,
hideStatus, hideStatus,
revealStatus, revealStatus,
hideQuote,
revealQuote,
} from '../../../actions/statuses'; } from '../../../actions/statuses';
import { boostModal, deleteModal } from '../../../initial_state'; import { boostModal, deleteModal } from '../../../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../../../selectors'; 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)); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus));

View File

@ -30,6 +30,7 @@ import { initBlockModal } from '../../actions/blocks';
import { initBoostModal } from '../../actions/boosts'; import { initBoostModal } from '../../actions/boosts';
import { import {
replyCompose, replyCompose,
quoteCompose,
mentionCompose, mentionCompose,
directCompose, directCompose,
} from '../../actions/compose'; } from '../../actions/compose';
@ -60,6 +61,8 @@ import {
revealStatus, revealStatus,
translateStatus, translateStatus,
undoStatusTranslation, undoStatusTranslation,
hideQuote,
revealQuote,
} from '../../actions/statuses'; } from '../../actions/statuses';
import ColumnHeader from '../../components/column_header'; import ColumnHeader from '../../components/column_header';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
@ -84,6 +87,8 @@ const messages = defineMessages({
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, 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?' }, 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' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
}); });
@ -216,6 +221,7 @@ class Status extends ImmutablePureComponent {
state = { state = {
fullscreen: false, fullscreen: false,
showMedia: defaultMediaVisibility(this.props.status), showMedia: defaultMediaVisibility(this.props.status),
showQuoteMedia: defaultMediaVisibility(this.props.status ? this.props.status.get('quote', null) : null),
loadedStatusId: undefined, loadedStatusId: undefined,
}; };
@ -235,7 +241,11 @@ class Status extends ImmutablePureComponent {
} }
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) { 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 }); this.setState({ showMedia: !this.state.showMedia });
}; };
handleToggleQuoteMediaVisibility = () => {
this.setState({ showQuoteMedia: !this.state.showQuoteMedia });
}
handleFavouriteClick = (status) => { handleFavouriteClick = (status) => {
const { dispatch } = this.props; const { dispatch } = this.props;
const { signedIn } = this.context.identity; 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) => { handleModalReblog = (status, privacy) => {
this.props.dispatch(reblog(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 = () => { handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS()); const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
@ -707,9 +742,12 @@ class Status extends ImmutablePureComponent {
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}
onToggleHidden={this.handleToggleHidden} onToggleHidden={this.handleToggleHidden}
onTranslate={this.handleTranslate} onTranslate={this.handleTranslate}
onQuoteToggleHidden={this.handleQuoteToggleHidden}
domain={domain} domain={domain}
showMedia={this.state.showMedia} showMedia={this.state.showMedia}
showQuoteMedia={this.state.showQuoteMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility} onToggleMediaVisibility={this.handleToggleMediaVisibility}
onToggleQuoteMediaVisibility={this.handleToggleQuoteMediaVisibility}
pictureInPicture={pictureInPicture} pictureInPicture={pictureInPicture}
/> />
@ -719,6 +757,7 @@ class Status extends ImmutablePureComponent {
onReply={this.handleReplyClick} onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick} onFavourite={this.handleFavouriteClick}
onReblog={this.handleReblogClick} onReblog={this.handleReblogClick}
onQuote={this.handleQuoteClick}
onBookmark={this.handleBookmarkClick} onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick} onDelete={this.handleDeleteClick}
onEdit={this.handleEditClick} onEdit={this.handleEditClick}

View File

@ -29,6 +29,13 @@ import ComposePanel from './compose_panel';
import DrawerLoading from './drawer_loading'; import DrawerLoading from './drawer_loading';
import NavigationPanel from './navigation_panel'; 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 = { const componentMap = {
'COMPOSE': Compose, 'COMPOSE': Compose,
'HOME': HomeTimeline, 'HOME': HomeTimeline,
@ -149,13 +156,40 @@ export default class ColumnsArea extends ImmutablePureComponent {
const { renderComposePanel } = this.state; const { renderComposePanel } = this.state;
if (singleColumn) { if (singleColumn) {
return ( if (place_tab_bar_at_bottom) {
<div className='columns-area__panels'> return (
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'> <div className='columns-area__panels tab-ber-bottom'>
<div className='columns-area__panels__pane__inner'> <div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
{renderComposePanel && <ComposePanel />} <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>
</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='columns-area__panels__main'>
<div className='tabs-bar__wrapper'><TabsBarPortal /></div> <div className='tabs-bar__wrapper'><TabsBarPortal /></div>
@ -168,7 +202,8 @@ export default class ColumnsArea extends ImmutablePureComponent {
</div> </div>
</div> </div>
</div> </div>
); );
}
} }
return ( 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, autoPlay: PropTypes.bool,
volume: PropTypes.number, volume: PropTypes.number,
muted: PropTypes.bool, muted: PropTypes.bool,
quote: PropTypes.bool,
componentIndex: PropTypes.number, componentIndex: PropTypes.number,
autoFocus: PropTypes.bool, autoFocus: PropTypes.bool,
}; };

View File

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

View File

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

View File

@ -6,6 +6,8 @@ import {
COMPOSE_CHANGE, COMPOSE_CHANGE,
COMPOSE_REPLY, COMPOSE_REPLY,
COMPOSE_REPLY_CANCEL, COMPOSE_REPLY_CANCEL,
COMPOSE_QUOTE,
COMPOSE_QUOTE_CANCEL,
COMPOSE_DIRECT, COMPOSE_DIRECT,
COMPOSE_MENTION, COMPOSE_MENTION,
COMPOSE_SUBMIT_REQUEST, COMPOSE_SUBMIT_REQUEST,
@ -67,6 +69,8 @@ const initialState = ImmutableMap({
caretPosition: null, caretPosition: null,
preselectDate: null, preselectDate: null,
in_reply_to: null, in_reply_to: null,
quote_from: null,
quote_from_url: null,
is_composing: false, is_composing: false,
is_submitting: false, is_submitting: false,
is_changing_upload: false, is_changing_upload: false,
@ -119,6 +123,8 @@ function clearAll(state) {
map.set('is_submitting', false); map.set('is_submitting', false);
map.set('is_changing_upload', false); map.set('is_changing_upload', false);
map.set('in_reply_to', null); map.set('in_reply_to', null);
map.set('quote_from', null);
map.set('quote_from_url', null);
map.set('privacy', state.get('default_privacy')); map.set('privacy', state.get('default_privacy'));
map.set('sensitive', state.get('default_sensitive')); map.set('sensitive', state.get('default_sensitive'));
map.set('language', state.get('default_language')); 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; 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) => { const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
prefix = prefix.toLowerCase(); prefix = prefix.toLowerCase();
if (suggestions.length < 4) { if (suggestions.length < 4) {
@ -335,10 +352,20 @@ export default function compose(state = initialState, action) {
case COMPOSE_COMPOSING_CHANGE: case COMPOSE_COMPOSING_CHANGE:
return state.set('is_composing', action.value); return state.set('is_composing', action.value);
case COMPOSE_REPLY: case COMPOSE_REPLY:
case COMPOSE_QUOTE:
return state.withMutations(map => { return state.withMutations(map => {
map.set('id', null); map.set('id', null);
map.set('in_reply_to', action.status.get('id')); if (action.type === COMPOSE_REPLY) {
map.set('text', statusToTextMentions(state, action.status)); 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('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
map.set('focusDate', new Date()); map.set('focusDate', new Date());
map.set('caretPosition', null); map.set('caretPosition', null);
@ -370,6 +397,7 @@ export default function compose(state = initialState, action) {
case COMPOSE_UPLOAD_CHANGE_REQUEST: case COMPOSE_UPLOAD_CHANGE_REQUEST:
return state.set('is_changing_upload', true); return state.set('is_changing_upload', true);
case COMPOSE_REPLY_CANCEL: case COMPOSE_REPLY_CANCEL:
case COMPOSE_QUOTE_CANCEL:
case COMPOSE_RESET: case COMPOSE_RESET:
case COMPOSE_SUBMIT_SUCCESS: case COMPOSE_SUBMIT_SUCCESS:
return clearAll(state); return clearAll(state);
@ -468,8 +496,10 @@ export default function compose(state = initialState, action) {
})); }));
case REDRAFT: case REDRAFT:
return state.withMutations(map => { 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('in_reply_to', action.status.get('in_reply_to_id'));
map.set('quote_from', action.status.getIn(['quote', 'id']));
map.set('quote_from_url', action.status.getIn(['quote', 'url']));
map.set('privacy', action.status.get('visibility')); map.set('privacy', action.status.get('visibility'));
map.set('media_attachments', action.status.get('media_attachments').map((media) => media.set('unattached', true))); map.set('media_attachments', action.status.get('media_attachments').map((media) => media.set('unattached', true)));
map.set('focusDate', new Date()); map.set('focusDate', new Date());

View File

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

View File

@ -26,6 +26,8 @@ import {
STATUS_TRANSLATE_UNDO, STATUS_TRANSLATE_UNDO,
STATUS_FETCH_REQUEST, STATUS_FETCH_REQUEST,
STATUS_FETCH_FAIL, STATUS_FETCH_FAIL,
QUOTE_REVEAL,
QUOTE_HIDE,
} from '../actions/statuses'; } from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines'; import { TIMELINE_DELETE } from '../actions/timelines';
@ -121,6 +123,14 @@ export default function statuses(state = initialState, action) {
}); });
case STATUS_COLLAPSE: case STATUS_COLLAPSE:
return state.setIn([action.id, 'collapsed'], action.isCollapsed); 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: case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.references); return deleteStatus(state, action.id, action.references);
case STATUS_TRANSLATE_SUCCESS: 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 { toServerSideType } from 'mastodon/utils/filters';
import { me } from '../initial_state'; import { me } from '../initial_state';
import {reblogRequest} from '../actions/interactions';
export { makeGetAccount } from "./accounts"; export { makeGetAccount } from "./accounts";
@ -21,22 +22,54 @@ export const makeGetStatus = () => {
[ [
(state, { id }) => state.getIn(['statuses', id]), (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, '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', 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, '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, 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')) { if (!statusBase || statusBase.get('isLoading')) {
return null; return null;
} }
accountBase = accountBase.withMutations(map => {
map.set('relationship', relationshipBase);
map.set('moved', movedBase);
});
if (statusReblog) { if (statusReblog) {
accountReblog = accountReblog.withMutations(map => {
map.set('relationship', relationshipReblog);
map.set('moved', movedReblog);
});
statusReblog = statusReblog.set('account', accountReblog); statusReblog = statusReblog.set('account', accountReblog);
} else { } else {
statusReblog = null; 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; let filtered = false;
if ((accountReblog || accountBase).get('id') !== me && filters) { if ((accountReblog || accountBase).get('id') !== me && filters) {
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList(); let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
@ -51,6 +84,7 @@ export const makeGetStatus = () => {
return statusBase.withMutations(map => { return statusBase.withMutations(map => {
map.set('reblog', statusReblog); map.set('reblog', statusReblog);
map.set('quote', statusQuote);
map.set('account', accountBase); map.set('account', accountBase);
map.set('matched_filters', filtered); 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'); 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(); 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 }) => { Rails.delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {

View File

@ -23,3 +23,5 @@
@import 'mastodon/rtl'; @import 'mastodon/rtl';
@import 'mastodon/accessibility'; @import 'mastodon/accessibility';
@import 'mastodon/rich_text'; @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; background: $ui-highlight-color;
border-color: $ui-highlight-color; border-color: $ui-highlight-color;
color: $primary-text-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 { .status__content--with-action {
cursor: pointer; cursor: pointer;
} }
@ -1250,6 +1312,10 @@ body > [data-popper-placement] {
.status__content.status__content--collapsed { .status__content.status__content--collapsed {
max-height: 22px * 15; // 15 lines is roughly above 500 characters max-height: 22px * 15; // 15 lines is roughly above 500 characters
.quote-status & {
max-height: 22px * 5;
}
} }
.status__content__read-more-button, .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 { .focusable {
&:focus { &:focus {
outline: 0; outline: 0;
@ -1344,9 +1474,12 @@ body > [data-popper-placement] {
.status { .status {
padding: 16px; padding: 16px;
min-height: 54px; min-height: 54px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: auto; cursor: auto;
&:not(.quote-status) {
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
@keyframes fade { @keyframes fade {
0% { 0% {
opacity: 0; opacity: 0;

View File

@ -40,7 +40,7 @@
&:last-child { &:last-child {
.detailed-status, .detailed-status,
.status, .status:not(.quote-status),
.load-more { .load-more {
border-bottom: 0; border-bottom: 0;
border-radius: 0 0 4px 4px; 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) { @media screen and (width <= 740px) {
.detailed-status, .detailed-status,
.status, .status:not(.quote-status),
.load-more { .load-more {
border-radius: 0 !important; border-radius: 0 !important;
} }
@ -77,6 +86,10 @@
} }
} }
.standalone-timeline .quote-status {
cursor: pointer;
}
.button.logo-button svg { .button.logo-button svg {
width: 20px; width: 20px;
height: auto; 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 = [] @silenced_account_ids = []
@params = {} @params = {}
process_quote
process_status_params process_status_params
process_tags process_tags
process_audience process_audience
@ -130,6 +131,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
media_attachment_ids: attachment_ids, media_attachment_ids: attachment_ids,
ordered_media_attachment_ids: attachment_ids, ordered_media_attachment_ids: attachment_ids,
poll: process_poll, poll: process_poll,
quote: @quote,
} }
end end
@ -430,4 +432,20 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
poll.reload poll.reload
retry retry
end 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 end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -92,6 +92,14 @@ class Webhook < ApplicationRecord
end end
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 def generate_secret
self.secret = SecureRandom.hex(20) if secret.blank? self.secret = SecureRandom.hex(20) if secret.blank?
end end

View File

@ -11,6 +11,9 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
:atom_uri, :in_reply_to_atom_uri, :atom_uri, :in_reply_to_atom_uri,
:conversation :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
attribute :content_map, if: :language? attribute :content_map, if: :language?
attribute :updated, if: :edited? attribute :updated, if: :edited?
@ -138,6 +141,16 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
end end
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? def local?
object.account.local? object.account.local?
end end

View File

@ -29,6 +29,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:use_blurhash] = object_account_user.setting_use_blurhash store[:use_blurhash] = object_account_user.setting_use_blurhash
store[:use_pending_items] = object_account_user.setting_use_pending_items store[:use_pending_items] = object_account_user.setting_use_pending_items
store[:show_trends] = Setting.trends && object_account_user.setting_trends 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 else
store[:auto_play_gif] = Setting.auto_play_gif store[:auto_play_gif] = Setting.auto_play_gif
store[:display_media] = Setting.display_media store[:display_media] = Setting.display_media

View File

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

View File

@ -18,6 +18,8 @@ class REST::StatusSerializer < ActiveModel::Serializer
attribute :content, unless: :source_requested? attribute :content, unless: :source_requested?
attribute :text, if: :source_requested? attribute :text, if: :source_requested?
attribute :quote_id, if: -> { object.quote? }
belongs_to :reblog, serializer: REST::StatusSerializer belongs_to :reblog, serializer: REST::StatusSerializer
belongs_to :application, if: :show_application? belongs_to :application, if: :show_application?
belongs_to :account, serializer: REST::AccountSerializer belongs_to :account, serializer: REST::AccountSerializer
@ -42,6 +44,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
object.in_reply_to_account_id&.to_s object.in_reply_to_account_id&.to_s
end end
def quote_id
object.quote_id.to_s
end
def current_user? def current_user?
!current_user.nil? !current_user.nil?
end end
@ -196,3 +202,25 @@ class REST::StatusSerializer < ActiveModel::Serializer
end end
end 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 } @status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
else else
document = Nokogiri::HTML(@status.text) document = Nokogiri::HTML(@status.text)
links = document.css('a') links = document.css(':not(.quote-inline) > a')
links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize) links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize)
end end

View File

@ -5,6 +5,7 @@ class PostStatusService < BaseService
include LanguagesHelper include LanguagesHelper
MIN_SCHEDULE_OFFSET = 5.minutes.freeze MIN_SCHEDULE_OFFSET = 5.minutes.freeze
QUOTE_LINK_PATTERN = /\n\[?(#{FetchLinkCardService::URL_PATTERN})\]?$/
class UnexpectedMentionsError < StandardError class UnexpectedMentionsError < StandardError
attr_reader :accounts attr_reader :accounts
@ -30,6 +31,7 @@ class PostStatusService < BaseService
# @option [Doorkeeper::Application] :application # @option [Doorkeeper::Application] :application
# @option [String] :idempotency Optional idempotency key # @option [String] :idempotency Optional idempotency key
# @option [Boolean] :with_rate_limit # @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 # @option [Enumerable] :allowed_mentions Optional array of expected mentioned account IDs, raises `UnexpectedMentionsError` if unexpected accounts end up in mentions
# @return [Status] # @return [Status]
def call(account, options = {}) def call(account, options = {})
@ -37,11 +39,13 @@ class PostStatusService < BaseService
@options = options @options = options
@text = @options[:text] || '' @text = @options[:text] || ''
@in_reply_to = @options[:thread] @in_reply_to = @options[:thread]
@quote_id = @options[:quote_id]
return idempotency_duplicate if idempotency_given? && idempotency_duplicate? return idempotency_duplicate if idempotency_given? && idempotency_duplicate?
validate_media! validate_media!
preprocess_attributes! preprocess_attributes!
preprocess_quote!
if scheduled? if scheduled?
schedule_status! schedule_status!
@ -61,6 +65,19 @@ class PostStatusService < BaseService
private 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! def preprocess_attributes!
@sensitive = (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present? @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? @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? @visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced?
@scheduled_at = @options[:scheduled_at]&.to_datetime @scheduled_at = @options[:scheduled_at]&.to_datetime
@scheduled_at = nil if scheduled_in_the_past? @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 rescue ArgumentError
raise ActiveRecord::RecordInvalid raise ActiveRecord::RecordInvalid
end 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! def process_status!
@status = @account.statuses.new(status_attributes) @status = @account.statuses.new(status_attributes)
process_mentions_service.call(@status, save_records: false) 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), language: valid_locale_cascade(@options[:language], @account.user&.preferred_posting_language, I18n.default_locale),
application: @options[:application], application: @options[:application],
rate_limit: @options[:with_rate_limit], rate_limit: @options[:with_rate_limit],
quote_id: @quote_id,
}.compact }.compact
end end

View File

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

View File

@ -2,7 +2,7 @@
%html{ lang: I18n.locale } %html{ lang: I18n.locale }
%head %head
%meta{ charset: 'utf-8' }/ %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? - if cdn_host?
%link{ rel: 'dns-prefetch', href: 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.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.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.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' %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}" } .detailed-status.detailed-status--flex{ class: "detailed-status-#{status.visibility}" }
.p-author.h-card = render 'statuses/author', author: status.account, detailed: true
= 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?
= account_action_button(status.account) = account_action_button(status.account)
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< = render 'statuses/text', status: status
- 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 - if status.quote?
= render_poll_component(status) = render 'statuses/quote', status: status.quote
- if !status.ordered_media_attachments.empty? = render 'statuses/media', status: status, detailed: true
- 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)
.detailed-status__meta .detailed-status__meta
%data.dt-published{ value: status.created_at.to_time.iso8601 } %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 } .e-content{ lang: status.language }
= prerender_custom_emojis(status_content_format(status), status.emojis) = prerender_custom_emojis(status_content_format(status), status.emojis)
- if status.preloadable_poll = render 'statuses/text', status: status
= render_poll_component(status)
- if !status.ordered_media_attachments.empty? - if status.quote?
- if status.ordered_media_attachments.first.video? = render 'statuses/quote', status: status.quote
= render_video_component(status, width: 610, height: 343)
- elsif status.ordered_media_attachments.first.audio? = render 'statuses/media', status: status
= 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.in_reply_to_id.nil? && status.in_reply_to_account_id == status.account.id && !hide_show_thread - 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 = 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: プッシュ通知 push: プッシュ通知
reports: 通報 reports: 通報
search: 検索 search: 検索
statuses: 投稿 statuses: トゥート
layouts: layouts:
admin: admin:
nav: nav:
@ -178,13 +178,13 @@ ja:
read:notifications: 通知の読み取り read:notifications: 通知の読み取り
read:reports: 通報の読み取り read:reports: 通報の読み取り
read:search: あなたの代わりに検索 read:search: あなたの代わりに検索
read:statuses: すべての投稿の読み取り read:statuses: すべてのトゥートの読み取り
write: アカウントのすべてのデータの変更 write: アカウントのすべてのデータの変更
write:accounts: プロフィールの変更 write:accounts: プロフィールの変更
write:blocks: ユーザーのブロックやドメインの非表示 write:blocks: ユーザーのブロックやドメインの非表示
write:bookmarks: 投稿のブックマーク登録 write:bookmarks: トゥートのブックマーク登録
write:conversations: 会話のミュートと削除 write:conversations: 会話のミュートと削除
write:favourites: お気に入りの投稿 write:favourites: お気に入りのトゥート
write:filters: フィルターの変更 write:filters: フィルターの変更
write:follows: あなたの代わりにフォロー、アンフォロー write:follows: あなたの代わりにフォロー、アンフォロー
write:lists: リストの変更 write:lists: リストの変更
@ -192,4 +192,4 @@ ja:
write:mutes: アカウントや会話のミュート write:mutes: アカウントや会話のミュート
write:notifications: 通知の消去 write:notifications: 通知の消去
write:reports: 通報の作成 write:reports: 通報の作成
write:statuses: 投稿の送信 write:statuses: トゥートの送信

View File

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

View File

@ -215,7 +215,11 @@ en:
setting_display_media_show_all: Show all setting_display_media_show_all: Show all
setting_expand_spoilers: Always expand posts marked with content warnings setting_expand_spoilers: Always expand posts marked with content warnings
setting_hide_network: Hide your social graph 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_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_system_font_ui: Use system's default font
setting_theme: Site theme setting_theme: Site theme
setting_trends: Show today's trends setting_trends: Show today's trends

View File

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

View File

@ -1,3 +1,4 @@
default: styles/application.scss default: styles/application.scss
contrast: styles/contrast.scss contrast: styles/contrast.scss
mastodon-light: styles/mastodon-light.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_blurhash: 'web.use_blurhash',
use_pending_items: 'web.use_pending_items', use_pending_items: 'web.use_pending_items',
crop_images: 'web.crop_images', crop_images: 'web.crop_images',
place_tab_bar_at_bottom: 'web.place_tab_bar_at_bottom',
notification_emails: { notification_emails: {
follow: 'notification_emails.follow', follow: 'notification_emails.follow',
reblog: 'notification_emails.reblog', 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 "account_id", null: false
t.bigint "application_id" t.bigint "application_id"
t.bigint "in_reply_to_account_id" t.bigint "in_reply_to_account_id"
t.bigint "quote_id"
t.bigint "poll_id" t.bigint "poll_id"
t.datetime "deleted_at", precision: nil t.datetime "deleted_at", precision: nil
t.datetime "edited_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 ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id", where: "(in_reply_to_account_id IS NOT NULL)" t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id", where: "(in_reply_to_account_id IS NOT NULL)"
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", where: "(in_reply_to_id IS NOT NULL)" t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", where: "(in_reply_to_id IS NOT NULL)"
t.index ["quote_id"], name: "index_statuses_on_quote_id"
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id" t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
t.index ["uri"], name: "index_statuses_on_uri", unique: true, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)" t.index ["uri"], name: "index_statuses_on_uri", unique: true, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)"
end end

View File

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

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