Compare commits
60 Commits
Author | SHA1 | Date | |
---|---|---|---|
c92b5559a9 | |||
fbef81ab51 | |||
0eb421cc64 | |||
c2e185162d | |||
93cd53398a | |||
0ca146a155 | |||
af41ff0e2b | |||
|
0cd5faaa9d | ||
|
c2f59a2848 | ||
|
6e76cbb0e4 | ||
1b06c5befc | |||
ec77396ddd | |||
5e2bc7aa95 | |||
2ab80bc511 | |||
393ae412db | |||
45b7276b9f | |||
70cf68fc6e | |||
2c3b5a9d0c | |||
d646011a17 | |||
090f82f070 | |||
ff17262aff | |||
ca0dbf2c50 | |||
0f7e94a055 | |||
9667505b46 | |||
c5e6544f6a | |||
7a78300faf | |||
338733ac10 | |||
af554529f5 | |||
6ee650bd26 | |||
9f9e3234dd | |||
4fe20bae9c | |||
169b80234e | |||
d05a0c8fa3 | |||
d5ef4dff60 | |||
0923806964 | |||
1b02b4bfde | |||
a221c8e874 | |||
fbd972e447 | |||
90d36dd2a0 | |||
1e32a46edd | |||
eb6f1f0826 | |||
cce2fb6b96 | |||
158f9604ea | |||
0770a48d0a | |||
|
fa7943f5e5 | ||
|
8559edcee5 | ||
|
ce62e633c8 | ||
|
5d59901b8d | ||
|
ae2190b9a5 | ||
|
4be8ece78d | ||
|
bef7f9a21c | ||
|
ac517abef2 | ||
|
0c206d8711 | ||
|
faf1aadad2 | ||
|
c15001381b | ||
|
725569e8ab | ||
|
71530857a5 | ||
|
e7eab1a6ae | ||
|
043ab77449 | ||
|
2aa6ec14fe |
30
.github/dependabot.yml
vendored
@ -1,30 +0,0 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 99
|
||||
allow:
|
||||
- dependency-type: direct
|
||||
|
||||
- package-ecosystem: bundler
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 99
|
||||
allow:
|
||||
- dependency-type: direct
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 99
|
||||
allow:
|
||||
- dependency-type: direct
|
@ -7,8 +7,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||
after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
|
||||
|
||||
def index
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||
@statuses = load_statuses
|
||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer,
|
||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -6,8 +6,11 @@ class Api::V1::BookmarksController < Api::BaseController
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
def index
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||
@statuses = load_statuses
|
||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer,
|
||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -6,8 +6,11 @@ class Api::V1::FavouritesController < Api::BaseController
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
def index
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||
@statuses = load_statuses
|
||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer,
|
||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -44,10 +44,13 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
loaded_ancestors = cache_collection(ancestors_results, Status)
|
||||
loaded_descendants = cache_collection(descendants_results, Status)
|
||||
|
||||
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
|
||||
statuses = [@status] + @context.ancestors + @context.descendants
|
||||
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
|
||||
statuses = [@status] + @context.ancestors + @context.descendants
|
||||
account_ids = statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
||||
|
||||
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(account_ids, current_user&.account_id)
|
||||
end
|
||||
|
||||
def create
|
||||
@ -64,7 +67,8 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
application: doorkeeper_token.application,
|
||||
poll: status_params[:poll],
|
||||
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
|
||||
@ -128,6 +132,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
:visibility,
|
||||
:language,
|
||||
:scheduled_at,
|
||||
:quote_id,
|
||||
media_ids: [],
|
||||
media_attributes: [
|
||||
:id,
|
||||
|
@ -6,11 +6,13 @@ class Api::V1::Timelines::HomeController < Api::BaseController
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
def show
|
||||
@statuses = load_statuses
|
||||
@statuses = load_statuses
|
||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
||||
|
||||
render json: @statuses,
|
||||
each_serializer: REST::StatusSerializer,
|
||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id),
|
||||
status: account_home_feed.regenerating? ? 206 : 200
|
||||
end
|
||||
|
||||
|
@ -9,9 +9,12 @@ class Api::V1::Timelines::ListController < Api::BaseController
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
def show
|
||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
||||
|
||||
render json: @statuses,
|
||||
each_serializer: REST::StatusSerializer,
|
||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id)
|
||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id),
|
||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -6,8 +6,12 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
def show
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||
@statuses = load_statuses
|
||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
||||
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer,
|
||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -7,8 +7,12 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
def show
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||
@statuses = load_statuses
|
||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
||||
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer,
|
||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -55,6 +55,7 @@ class Settings::PreferencesController < Settings::BaseController
|
||||
:setting_trends,
|
||||
:setting_crop_images,
|
||||
:setting_always_send_emails,
|
||||
:setting_place_tab_bar_at_bottom,
|
||||
notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag appeal),
|
||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||
)
|
||||
|
@ -23,6 +23,7 @@ module ContextHelper
|
||||
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
||||
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
|
||||
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
||||
quoteUrl: { 'quoteUrl' => 'as:quoteUrl' },
|
||||
}.freeze
|
||||
|
||||
def full_context
|
||||
|
@ -15,7 +15,12 @@ module FormattingHelper
|
||||
module_function :extract_status_plain_text
|
||||
|
||||
def status_content_format(status)
|
||||
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []))
|
||||
html_aware_format(
|
||||
status.text,
|
||||
status.local?,
|
||||
preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []),
|
||||
quote: status.respond_to?(:quote) && status.quote,
|
||||
)
|
||||
end
|
||||
|
||||
def rss_status_content_format(status)
|
||||
|
Before Width: | Height: | Size: 588 B After Width: | Height: | Size: 444 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 735 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.0 KiB |
89
app/javascript/mastodon/actions/UtilBtns.js
Normal 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
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import { fetchRelationships } from './accounts';
|
||||
import api, { getLinks } from '../api';
|
||||
import { importFetchedStatuses } from './importer';
|
||||
import { uniq } from '../utils/uniq';
|
||||
|
||||
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
|
||||
export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
|
||||
@ -20,6 +22,7 @@ export function fetchBookmarkedStatuses() {
|
||||
api(getState).get('/api/v1/bookmarks').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
|
||||
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(fetchBookmarkedStatusesFail(error));
|
||||
@ -61,6 +64,7 @@ export function expandBookmarkedStatuses() {
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
|
||||
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandBookmarkedStatusesFail(error));
|
||||
|
@ -22,6 +22,8 @@ export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
|
||||
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
|
||||
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
|
||||
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
|
||||
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
|
||||
export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
|
||||
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
|
||||
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
|
||||
export const COMPOSE_RESET = 'COMPOSE_RESET';
|
||||
@ -120,6 +122,23 @@ export function cancelReplyCompose() {
|
||||
};
|
||||
}
|
||||
|
||||
export function quoteCompose(status, routerHistory) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: COMPOSE_QUOTE,
|
||||
status: status,
|
||||
});
|
||||
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
};
|
||||
};
|
||||
|
||||
export function cancelQuoteCompose() {
|
||||
return {
|
||||
type: COMPOSE_QUOTE_CANCEL,
|
||||
};
|
||||
};
|
||||
|
||||
export function resetCompose() {
|
||||
return {
|
||||
type: COMPOSE_RESET,
|
||||
@ -193,6 +212,7 @@ export function submitCompose(routerHistory) {
|
||||
visibility: getState().getIn(['compose', 'privacy']),
|
||||
poll: getState().getIn(['compose', 'poll'], null),
|
||||
language: getState().getIn(['compose', 'language']),
|
||||
quote_id: getState().getIn(['compose', 'quote_from'], null),
|
||||
},
|
||||
headers: {
|
||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { fetchRelationships } from './accounts';
|
||||
import api, { getLinks } from '../api';
|
||||
import { importFetchedStatuses } from './importer';
|
||||
import { uniq } from '../utils/uniq';
|
||||
|
||||
export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
|
||||
export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
|
||||
@ -20,6 +22,7 @@ export function fetchFavouritedStatuses() {
|
||||
api(getState).get('/api/v1/favourites').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
|
||||
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(fetchFavouritedStatusesFail(error));
|
||||
@ -64,6 +67,7 @@ export function expandFavouritedStatuses() {
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
|
||||
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandFavouritedStatusesFail(error));
|
||||
|
@ -80,6 +80,10 @@ export function importFetchedStatuses(statuses) {
|
||||
processStatus(status.reblog);
|
||||
}
|
||||
|
||||
if (status.quote && status.quote.id) {
|
||||
processStatus(status.quote);
|
||||
}
|
||||
|
||||
if (status.poll && status.poll.id) {
|
||||
pushUnique(polls, normalizePoll(status.poll));
|
||||
}
|
||||
|
@ -75,6 +75,8 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
||||
normalStatus.spoiler_text = normalOldStatus.get('spoiler_text');
|
||||
normalStatus.hidden = normalOldStatus.get('hidden');
|
||||
normalStatus.quote = normalOldStatus.get('quote');
|
||||
normalStatus.quote_hidden = normalOldStatus.get('quote_hidden');
|
||||
} else {
|
||||
// If the status has a CW but no contents, treat the CW as if it were the
|
||||
// status' contents, to avoid having a CW toggle with seemingly no effect.
|
||||
@ -91,6 +93,11 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
||||
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
||||
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
|
||||
|
||||
if (status.quote) {
|
||||
normalStatus.quote = normalizeStatus(status.quote, null);
|
||||
normalStatus.quote_hidden = normalStatus.quote.hidden;
|
||||
}
|
||||
}
|
||||
|
||||
return normalStatus;
|
||||
|
@ -28,6 +28,9 @@ export const STATUS_REVEAL = 'STATUS_REVEAL';
|
||||
export const STATUS_HIDE = 'STATUS_HIDE';
|
||||
export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
|
||||
|
||||
export const QUOTE_REVEAL = 'QUOTE_REVEAL';
|
||||
export const QUOTE_HIDE = 'QUOTE_HIDE';
|
||||
|
||||
export const REDRAFT = 'REDRAFT';
|
||||
|
||||
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
|
||||
@ -347,3 +350,25 @@ export const undoStatusTranslation = id => ({
|
||||
type: STATUS_TRANSLATE_UNDO,
|
||||
id,
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import { submitMarkers } from './markers';
|
||||
import api, { getLinks } from 'mastodon/api';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import compareId from 'mastodon/compare_id';
|
||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
||||
import { uniq } from '../utils/uniq';
|
||||
|
||||
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
||||
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
||||
@ -39,6 +41,7 @@ export function updateTimeline(timeline, status, accept) {
|
||||
}
|
||||
|
||||
dispatch(importFetchedStatus(status));
|
||||
dispatch(fetchRelationships([status.reblog ? status.reblog.account.id : status.account.id, status.quote ? status.quote.account.id : null].filter(x => x)));
|
||||
|
||||
dispatch({
|
||||
type: TIMELINE_UPDATE,
|
||||
@ -111,6 +114,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
||||
api(getState).get(path, { params }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchRelationships(uniq(response.data.map(item => [item.reblog ? item.reblog.account.id : item.account.id, item.quote ? item.quote.account.id : null]).flat().filter(x => x))));
|
||||
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
|
||||
|
||||
if (timelineId === 'home') {
|
||||
|
@ -1,10 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const Logo = () => (
|
||||
<svg viewBox='0 0 261 66' className='logo' role='img'>
|
||||
<title>Mastodon</title>
|
||||
<use xlinkHref='#logo-symbol-wordmark' />
|
||||
</svg>
|
||||
<img src="/y-zu-logo.svg" height="32px" />
|
||||
);
|
||||
|
||||
export default Logo;
|
||||
|
@ -236,10 +236,12 @@ class MediaGallery extends React.PureComponent {
|
||||
visible: PropTypes.bool,
|
||||
autoplay: PropTypes.bool,
|
||||
onToggleVisibility: PropTypes.func,
|
||||
quote: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
standalone: false,
|
||||
quote: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -310,7 +312,7 @@ class MediaGallery extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props;
|
||||
const { media, intl, sensitive, height, defaultWidth, standalone, autoplay, quote } = this.props;
|
||||
const { visible } = this.state;
|
||||
|
||||
const width = this.state.width || defaultWidth;
|
||||
@ -329,6 +331,10 @@ class MediaGallery extends React.PureComponent {
|
||||
style.height = height;
|
||||
}
|
||||
|
||||
if (quote && style.height) {
|
||||
style.height /= 2;
|
||||
}
|
||||
|
||||
const size = media.take(4).size;
|
||||
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import Avatar from './avatar';
|
||||
@ -22,6 +23,29 @@ import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_
|
||||
// to use the progress bar to show download progress
|
||||
import Bundle from '../features/ui/components/bundle';
|
||||
|
||||
export const mapStateToProps = (state, props) => {
|
||||
let status = props.status;
|
||||
|
||||
if (status === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
||||
status = status.get('reblog');
|
||||
}
|
||||
|
||||
if (status.get('quote', null) === null) {
|
||||
return {
|
||||
quoteMuted: !!status.get('quote_id', null),
|
||||
};
|
||||
}
|
||||
|
||||
const id = status.getIn(['quote', 'account', 'id'], null);
|
||||
return {
|
||||
quoteMuted: id !== null && (state.getIn(['relationships', id, 'muting']) || state.getIn(['relationships', id, 'blocking']) || state.getIn(['relationships', id, 'blocked_by']) || state.getIn(['relationships', id, 'domain_blocking']) || status.getIn(['quote', 'muted'])),
|
||||
};
|
||||
};
|
||||
|
||||
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
|
||||
const displayName = status.getIn(['account', 'display_name']);
|
||||
|
||||
@ -59,7 +83,61 @@ const messages = defineMessages({
|
||||
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps) @injectIntl
|
||||
class Status extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
@ -87,7 +165,9 @@ class Status extends ImmutablePureComponent {
|
||||
onToggleCollapsed: PropTypes.func,
|
||||
onTranslate: PropTypes.func,
|
||||
onInteractionModal: PropTypes.func,
|
||||
onQuoteToggleHidden: PropTypes.func,
|
||||
muted: PropTypes.bool,
|
||||
quoteMuted: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
unread: PropTypes.bool,
|
||||
onMoveUp: PropTypes.func,
|
||||
@ -103,6 +183,7 @@ class Status extends ImmutablePureComponent {
|
||||
inUse: PropTypes.bool,
|
||||
available: PropTypes.bool,
|
||||
}),
|
||||
contextType: PropTypes.string,
|
||||
};
|
||||
|
||||
// Avoid checking props that are functions (and whose equality will always
|
||||
@ -114,10 +195,12 @@ class Status extends ImmutablePureComponent {
|
||||
'hidden',
|
||||
'unread',
|
||||
'pictureInPicture',
|
||||
'quoteMuted',
|
||||
];
|
||||
|
||||
state = {
|
||||
showMedia: defaultMediaVisibility(this.props.status),
|
||||
showQuoteMedia: defaultMediaVisibility(this.props.status ? this.props.status.get('quote', null) : null),
|
||||
statusId: undefined,
|
||||
forceFilter: undefined,
|
||||
};
|
||||
@ -126,6 +209,7 @@ class Status extends ImmutablePureComponent {
|
||||
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
|
||||
return {
|
||||
showMedia: defaultMediaVisibility(nextProps.status),
|
||||
showQuoteMedia: defaultMediaVisibility(nextProps.status ? nextProps.status.get('quote', null) : null),
|
||||
statusId: nextProps.status.get('id'),
|
||||
};
|
||||
} else {
|
||||
@ -137,6 +221,10 @@ class Status extends ImmutablePureComponent {
|
||||
this.setState({ showMedia: !this.state.showMedia });
|
||||
};
|
||||
|
||||
handleToggleQuoteMediaVisibility = () => {
|
||||
this.setState({ showQuoteMedia: !this.state.showQuoteMedia });
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
|
||||
return;
|
||||
@ -165,6 +253,15 @@ class Status extends ImmutablePureComponent {
|
||||
this._openProfile(proper);
|
||||
};
|
||||
|
||||
handleQuoteClick = () => {
|
||||
if (!this.context.router) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { status } = this.props;
|
||||
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'quote', 'id'], status.getIn(['quote', 'id']))}`);
|
||||
}
|
||||
|
||||
handleExpandedToggle = () => {
|
||||
this.props.onToggleHidden(this._properStatus());
|
||||
};
|
||||
@ -177,6 +274,10 @@ class Status extends ImmutablePureComponent {
|
||||
this.props.onTranslate(this._properStatus());
|
||||
};
|
||||
|
||||
handleExpandedQuoteToggle = () => {
|
||||
this.props.onQuoteToggleHidden(this._properStatus());
|
||||
}
|
||||
|
||||
renderLoadingMediaGallery () {
|
||||
return <div className='media-gallery' style={{ height: '110px' }} />;
|
||||
}
|
||||
@ -309,10 +410,9 @@ class Status extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
let media = null;
|
||||
let statusAvatar, prepend, rebloggedByText;
|
||||
let prepend, rebloggedByText;
|
||||
|
||||
const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture } = this.props;
|
||||
const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture, quoteMuted, contextType } = this.props;
|
||||
|
||||
let { status, account, ...other } = this.props;
|
||||
|
||||
@ -397,103 +497,123 @@ class Status extends ImmutablePureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (pictureInPicture.get('inUse')) {
|
||||
media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
if (this.props.muted) {
|
||||
media = (
|
||||
<AttachmentList
|
||||
const media = (status, quote = false) => {
|
||||
if (pictureInPicture.get('inUse')) {
|
||||
return <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
if (this.props.muted) {
|
||||
return (
|
||||
<AttachmentList
|
||||
compact
|
||||
media={status.get('media_attachments')}
|
||||
/>
|
||||
);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
|
||||
return (
|
||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer}>
|
||||
{Component => (
|
||||
<Component
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
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]);
|
||||
|
||||
return (
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer}>
|
||||
{Component => (
|
||||
<Component
|
||||
preview={attachment.get('preview_url')}
|
||||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||
blurhash={attachment.get('blurhash')}
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
width={this.props.cachedMediaWidth}
|
||||
height={110}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
||||
visible={quote ? this.state.showQuoteMedia : this.state.showMedia}
|
||||
onToggleVisibility={quote ? this.handleToggleQuoteMediaVisibility : this.handleToggleMediaVisibility}
|
||||
quote={quote}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||
{Component => (
|
||||
<Component
|
||||
media={status.get('media_attachments')}
|
||||
sensitive={status.get('sensitive')}
|
||||
height={110}
|
||||
onOpenMedia={this.handleOpenMedia}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
visible={quote ? this.state.showQuoteMedia : this.state.showMedia}
|
||||
onToggleVisibility={quote ? this.handleToggleQuoteMediaVisibility : this.handleToggleMediaVisibility}
|
||||
quote={quote}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
}
|
||||
} else if (status.get('spoiler_text').length === 0 && status.get('card') && !this.props.muted) {
|
||||
return (
|
||||
<Card
|
||||
onOpenMedia={this.handleOpenMedia}
|
||||
card={status.get('card')}
|
||||
compact
|
||||
media={status.get('media_attachments')}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
sensitive={status.get('sensitive')}
|
||||
quote={quote}
|
||||
/>
|
||||
);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
|
||||
media = (
|
||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
||||
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
|
||||
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||
width={this.props.cachedMediaWidth}
|
||||
height={110}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
||||
sensitive={status.get('sensitive')}
|
||||
blurhash={attachment.get('blurhash')}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
|
||||
media = (
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
preview={attachment.get('preview_url')}
|
||||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||
blurhash={attachment.get('blurhash')}
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
width={this.props.cachedMediaWidth}
|
||||
height={110}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else {
|
||||
media = (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||
{Component => (
|
||||
<Component
|
||||
media={status.get('media_attachments')}
|
||||
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') && !this.props.muted) {
|
||||
media = (
|
||||
<Card
|
||||
onOpenMedia={this.handleOpenMedia}
|
||||
card={status.get('card')}
|
||||
compact
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
sensitive={status.get('sensitive')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (account === undefined || account === null) {
|
||||
statusAvatar = <Avatar account={status.get('account')} size={46} />;
|
||||
} else {
|
||||
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const statusAvatar = (status, account) => {
|
||||
if (account === undefined || account === null) {
|
||||
return <Avatar account={status.get('account')} size={46} />;
|
||||
} else {
|
||||
return <AvatarOverlay account={status.get('account')} friend={account} />;
|
||||
}
|
||||
};
|
||||
|
||||
const identity = (status, account) => (
|
||||
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
||||
<div className='status__avatar'>
|
||||
{statusAvatar(status, account)}
|
||||
</div>
|
||||
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
);
|
||||
|
||||
const visibilityIconInfo = {
|
||||
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
|
||||
@ -516,13 +636,7 @@ class Status extends ImmutablePureComponent {
|
||||
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
||||
</a>
|
||||
|
||||
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
||||
<div className='status__avatar'>
|
||||
{statusAvatar}
|
||||
</div>
|
||||
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
{identity(status, account, false)}
|
||||
</div>
|
||||
|
||||
<StatusContent
|
||||
@ -535,7 +649,9 @@ class Status extends ImmutablePureComponent {
|
||||
onCollapsedToggle={this.handleCollapsedToggle}
|
||||
/>
|
||||
|
||||
{media}
|
||||
{media(status)}
|
||||
|
||||
{quote(status, this.props.muted, quoteMuted, this.handleQuoteClick, this.handleExpandedQuoteToggle, identity, media, this.context.router, contextType)}
|
||||
|
||||
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
|
||||
</div>
|
||||
|
@ -26,6 +26,8 @@ const messages = defineMessages({
|
||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||
quote: { id: 'status.quote', defaultMessage: 'Quote' },
|
||||
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
|
||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
|
||||
@ -68,6 +70,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
onReply: PropTypes.func,
|
||||
onFavourite: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onQuote: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onDirect: PropTypes.func,
|
||||
onMention: PropTypes.func,
|
||||
@ -138,6 +141,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
handleQuoteClick = () => {
|
||||
this.props.onQuote(this.props.status, this.context.router.history);
|
||||
}
|
||||
|
||||
handleBookmarkClick = () => {
|
||||
this.props.onBookmark(this.props.status);
|
||||
};
|
||||
@ -231,6 +238,14 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
this.props.onFilter();
|
||||
};
|
||||
|
||||
static quoteTitle = (intl, messages, publicStatus) => {
|
||||
if (publicStatus) {
|
||||
return intl.formatMessage(messages.quote);
|
||||
} else {
|
||||
return intl.formatMessage(messages.cannot_quote);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
|
||||
const { signedIn, permissions } = this.context.identity;
|
||||
@ -361,7 +376,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
<div className='status__action-bar'>
|
||||
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' 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' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' 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-right' onClick={this.handleQuoteClick} />
|
||||
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
||||
|
||||
{shareButton}
|
||||
|
@ -9,6 +9,7 @@ import Icon from 'mastodon/components/icon';
|
||||
import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'mastodon/initial_state';
|
||||
|
||||
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
||||
const QUOTE_MAX_HEIGHT = 112; // 22px * 5 (+ 2px padding at the top)
|
||||
|
||||
class TranslateButton extends React.PureComponent {
|
||||
|
||||
@ -64,6 +65,7 @@ class StatusContent extends React.PureComponent {
|
||||
collapsable: PropTypes.bool,
|
||||
onCollapsedToggle: PropTypes.func,
|
||||
intl: PropTypes.object,
|
||||
quote: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -107,12 +109,12 @@ class StatusContent extends React.PureComponent {
|
||||
}
|
||||
|
||||
if (status.get('collapsed', null) === null && onCollapsedToggle) {
|
||||
const { collapsable, onClick } = this.props;
|
||||
const { collapsable, onClick, quote } = this.props;
|
||||
|
||||
const collapsed =
|
||||
collapsable
|
||||
&& onClick
|
||||
&& node.clientHeight > MAX_HEIGHT
|
||||
&& node.clientHeight > (quote ? QUOTE_MAX_HEIGHT : MAX_HEIGHT)
|
||||
&& status.get('spoiler_text').length === 0;
|
||||
|
||||
onCollapsedToggle(collapsed);
|
||||
@ -216,7 +218,7 @@ class StatusContent extends React.PureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { status, intl } = this.props;
|
||||
const { status, intl, quote } = this.props;
|
||||
|
||||
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
||||
const renderReadMore = this.props.onClick && status.get('collapsed');
|
||||
@ -245,6 +247,12 @@ class StatusContent extends React.PureComponent {
|
||||
<PollContainer pollId={status.get('poll')} />
|
||||
);
|
||||
|
||||
if (quote) {
|
||||
const doc = new DOMParser().parseFromString(content.__html, 'text/html').documentElement;
|
||||
Array.from(doc.querySelectorAll('br')).forEach(nl => nl.replaceWith(' '));
|
||||
content.__html = doc.outerHTML;
|
||||
}
|
||||
|
||||
if (status.get('spoiler_text').length > 0) {
|
||||
let mentionsPlaceholder = '';
|
||||
|
||||
|
@ -4,6 +4,7 @@ import Status from '../components/status';
|
||||
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
|
||||
import {
|
||||
replyCompose,
|
||||
quoteCompose,
|
||||
mentionCompose,
|
||||
directCompose,
|
||||
} from '../actions/compose';
|
||||
@ -27,6 +28,8 @@ import {
|
||||
editStatus,
|
||||
translateStatus,
|
||||
undoStatusTranslation,
|
||||
hideQuote,
|
||||
revealQuote,
|
||||
} from '../actions/statuses';
|
||||
import {
|
||||
unmuteAccount,
|
||||
@ -58,6 +61,8 @@ const messages = defineMessages({
|
||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
|
||||
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
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: 'Hide entire domain' },
|
||||
});
|
||||
|
||||
@ -107,6 +112,22 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||
}
|
||||
},
|
||||
|
||||
onQuote (status, router) {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.quoteMessage),
|
||||
confirm: intl.formatMessage(messages.quoteConfirm),
|
||||
onConfirm: () => dispatch(quoteCompose(status, router)),
|
||||
}));
|
||||
} else {
|
||||
dispatch(quoteCompose(status, router));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onFavourite (status) {
|
||||
if (status.get('favourited')) {
|
||||
dispatch(unfavourite(status));
|
||||
@ -234,6 +255,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
|
||||
},
|
||||
|
||||
onQuoteToggleHidden (status) {
|
||||
if (status.get('quote_hidden')) {
|
||||
dispatch(revealQuote(status.get('id')));
|
||||
} else {
|
||||
dispatch(hideQuote(status.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onBlockDomain (domain) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
|
||||
|
@ -50,6 +50,7 @@ class Audio extends React.PureComponent {
|
||||
volume: PropTypes.number,
|
||||
muted: PropTypes.bool,
|
||||
deployPictureInPicture: PropTypes.func,
|
||||
quote: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -94,7 +95,11 @@ class Audio extends React.PureComponent {
|
||||
|
||||
_setDimensions () {
|
||||
const width = this.player.offsetWidth;
|
||||
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
|
||||
let height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
|
||||
|
||||
if (this.props.quote) {
|
||||
height /= 2;
|
||||
}
|
||||
|
||||
if (this.props.cacheWidth) {
|
||||
this.props.cacheWidth(width);
|
||||
|
@ -29,6 +29,9 @@ const messages = defineMessages({
|
||||
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' },
|
||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||
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' }
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
@ -65,6 +68,9 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
isInReply: PropTypes.bool,
|
||||
singleColumn: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
onGojiSubmit: PropTypes.func.isRequired,
|
||||
onHarukinSubmit: PropTypes.func.isRequired,
|
||||
onRisaSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -90,7 +96,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
const fulltext = this.getFulltextForCharacterCounting();
|
||||
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
|
||||
|
||||
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia));
|
||||
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 2048 || (isOnlyWhitespace && !anyMedia));
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
@ -206,6 +212,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
this.props.onPickEmoji(position, data, needsSpace);
|
||||
};
|
||||
|
||||
handleOnGojiSubmit = () => this.props.onGojiSubmit(this.autosuggestTextarea.textarea);
|
||||
handleOnHarukinSubmit = () => this.props.onHarukinSubmit(this.autosuggestTextarea.textarea);
|
||||
handleOnRisaSubmit = () => this.props.onRisaSubmit(this.autosuggestTextarea.textarea);
|
||||
|
||||
render () {
|
||||
const { intl, onPaste, autoFocus } = this.props;
|
||||
const disabled = this.props.isSubmitting;
|
||||
@ -225,6 +235,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
<WarningContainer />
|
||||
|
||||
<ReplyIndicatorContainer />
|
||||
<ReplyIndicatorContainer quote />
|
||||
|
||||
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
|
||||
<AutosuggestInput
|
||||
@ -280,12 +291,13 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
</div>
|
||||
|
||||
<div className='character-counter__wrapper'>
|
||||
<CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} />
|
||||
<CharacterCounter max={2048} text={this.getFulltextForCharacterCounting()} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='compose-form__publish'>
|
||||
<div className='compose-form__publish-button-wrapper'>
|
||||
<Button className="compose-form__utilBtns-risa" text={intl.formatMessage(messages.utilBtns_risa)} onClick={this.handleOnRisaSubmit} block />
|
||||
<Button
|
||||
type='submit'
|
||||
text={publishText}
|
||||
@ -294,6 +306,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import IconButton from '../../../components/icon_button';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import classNames from 'classnames';
|
||||
import AttachmentList from 'mastodon/components/attachment_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -23,6 +24,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
||||
status: ImmutablePropTypes.map,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
quote: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
@ -37,7 +39,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { status, intl } = this.props;
|
||||
const { status, intl, quote } = this.props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
@ -46,7 +48,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
|
||||
return (
|
||||
<div className='reply-indicator'>
|
||||
<div className={classNames('reply-indicator', { 'quote-indicator': quote })}>
|
||||
<div className='reply-indicator__header'>
|
||||
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>
|
||||
|
||||
|
@ -10,6 +10,11 @@ import {
|
||||
insertEmojiCompose,
|
||||
uploadCompose,
|
||||
} from '../../../actions/compose';
|
||||
import {
|
||||
submitGoji,
|
||||
submitHarukin,
|
||||
submitRisa
|
||||
} from '../../../actions/UtilBtns';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
text: state.getIn(['compose', 'text']),
|
||||
@ -63,6 +68,18 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(insertEmojiCompose(position, data, needsSpace));
|
||||
},
|
||||
|
||||
onRisaSubmit (textarea) {
|
||||
dispatch(submitRisa(textarea));
|
||||
},
|
||||
|
||||
onGojiSubmit (textarea) {
|
||||
dispatch(submitGoji(textarea));
|
||||
},
|
||||
|
||||
onHarukinSubmit (textarea) {
|
||||
dispatch(submitHarukin(textarea));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm);
|
||||
|
@ -1,22 +1,23 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { cancelReplyCompose } from '../../../actions/compose';
|
||||
import { cancelReplyCompose, cancelQuoteCompose } from '../../../actions/compose';
|
||||
import { makeGetStatus } from '../../../selectors';
|
||||
import ReplyIndicator from '../components/reply_indicator';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const mapStateToProps = (state, props) => {
|
||||
let statusId = state.getIn(['compose', 'id'], null);
|
||||
let editing = true;
|
||||
|
||||
if (statusId === null) {
|
||||
statusId = state.getIn(['compose', 'in_reply_to']);
|
||||
statusId = state.getIn(['compose', props.quote ? 'quote_from' : 'in_reply_to']);
|
||||
editing = false;
|
||||
}
|
||||
|
||||
return {
|
||||
status: getStatus(state, { id: statusId }),
|
||||
quote: props.quote,
|
||||
editing,
|
||||
};
|
||||
};
|
||||
@ -26,8 +27,8 @@ const makeMapStateToProps = () => {
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onCancel () {
|
||||
dispatch(cancelReplyCompose());
|
||||
onCancel (quote) {
|
||||
dispatch(quote ? cancelQuoteCompose() : cancelReplyCompose());
|
||||
},
|
||||
|
||||
});
|
||||
|
@ -4,10 +4,11 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import StatusActionBar from 'mastodon/components/status_action_bar';
|
||||
import classNames from 'classnames';
|
||||
import { me, boostModal } from 'mastodon/initial_state';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { replyCompose } from 'mastodon/actions/compose';
|
||||
import { replyCompose, quoteCompose } from 'mastodon/actions/compose';
|
||||
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
|
||||
import { makeGetStatus } from 'mastodon/selectors';
|
||||
import { initBoostModal } from 'mastodon/actions/boosts';
|
||||
@ -20,9 +21,13 @@ const messages = defineMessages({
|
||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||
quote: { id: 'status.quote', defaultMessage: 'Quote' },
|
||||
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
|
||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
|
||||
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
open: { id: 'status.open', defaultMessage: 'Expand this status' },
|
||||
});
|
||||
|
||||
@ -135,6 +140,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 => {
|
||||
const { router } = this.context;
|
||||
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import StatusActionBar from '../../../components/status_action_bar';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
@ -20,6 +21,8 @@ const messages = defineMessages({
|
||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||
quote: { id: 'status.quote', defaultMessage: 'Quote' },
|
||||
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
|
||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
@ -61,6 +64,7 @@ class ActionBar extends React.PureComponent {
|
||||
relationship: ImmutablePropTypes.map,
|
||||
onReply: PropTypes.func.isRequired,
|
||||
onReblog: PropTypes.func.isRequired,
|
||||
onQuote: PropTypes.func,
|
||||
onFavourite: PropTypes.func.isRequired,
|
||||
onBookmark: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
@ -88,6 +92,10 @@ class ActionBar extends React.PureComponent {
|
||||
this.props.onReblog(this.props.status, e);
|
||||
};
|
||||
|
||||
handleQuoteClick = () => {
|
||||
this.props.onQuote(this.props.status, this.context.router.history);
|
||||
}
|
||||
|
||||
handleFavouriteClick = () => {
|
||||
this.props.onFavourite(this.props.status);
|
||||
};
|
||||
@ -286,6 +294,7 @@ class ActionBar extends React.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} onClick={this.handleReplyClick} /></div>
|
||||
<div className='detailed-status__button' ><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' 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' onClick={this.handleFavouriteClick} /></div>
|
||||
<div className='detailed-status__button'><IconButton disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='quote-right' 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' onClick={this.handleBookmarkClick} /></div>
|
||||
|
||||
{shareButton}
|
||||
|
@ -68,6 +68,7 @@ export default class Card extends React.PureComponent {
|
||||
defaultWidth: PropTypes.number,
|
||||
cacheWidth: PropTypes.func,
|
||||
sensitive: PropTypes.bool,
|
||||
quote: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -184,7 +185,7 @@ export default class Card extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { card, maxDescription, compact } = this.props;
|
||||
const { card, maxDescription, compact, quote } = this.props;
|
||||
const { width, embedded, revealed } = this.state;
|
||||
|
||||
if (card === null) {
|
||||
@ -197,7 +198,11 @@ export default class Card extends React.PureComponent {
|
||||
const className = classnames('status-card', { horizontal, compact, interactive });
|
||||
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
|
||||
const ratio = card.get('width') / card.get('height');
|
||||
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
|
||||
let height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
|
||||
|
||||
if (quote && height) {
|
||||
height /= 2;
|
||||
}
|
||||
|
||||
const description = (
|
||||
<div className='status-card__content'>
|
||||
|
@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import { mapStateToProps, quote } from '../../../components/status';
|
||||
import StatusContent from '../../../components/status_content';
|
||||
import MediaGallery from '../../../components/media_gallery';
|
||||
import { Link } from 'react-router-dom';
|
||||
@ -25,7 +27,7 @@ const messages = defineMessages({
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
export default @connect(mapStateToProps) @injectIntl
|
||||
class DetailedStatus extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
@ -37,17 +39,21 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||
onOpenMedia: PropTypes.func.isRequired,
|
||||
onOpenVideo: PropTypes.func.isRequired,
|
||||
onToggleHidden: PropTypes.func.isRequired,
|
||||
onQuoteToggleHidden: PropTypes.func.isRequired,
|
||||
onTranslate: PropTypes.func.isRequired,
|
||||
measureHeight: PropTypes.bool,
|
||||
onHeightChange: PropTypes.func,
|
||||
domain: PropTypes.string.isRequired,
|
||||
compact: PropTypes.bool,
|
||||
quoteMuted: PropTypes.bool,
|
||||
showMedia: PropTypes.bool,
|
||||
showQuoteMedia: PropTypes.bool,
|
||||
pictureInPicture: ImmutablePropTypes.contains({
|
||||
inUse: PropTypes.bool,
|
||||
available: PropTypes.bool,
|
||||
}),
|
||||
onToggleMediaVisibility: PropTypes.func,
|
||||
onQuoteToggleMediaVisibility: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -56,8 +62,9 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||
|
||||
handleAccountClick = (e) => {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) {
|
||||
const acct = e.currentTarget.getAttribute('data-acct');
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
||||
this.context.router.history.push(`/@${acct}`);
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
@ -71,6 +78,19 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||
this.props.onToggleHidden(this.props.status);
|
||||
};
|
||||
|
||||
handleExpandedQuoteToggle = () => {
|
||||
this.props.onQuoteToggleHidden(this.props.status);
|
||||
}
|
||||
|
||||
handleQuoteClick = () => {
|
||||
if (!this.context.router) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { status } = this.props;
|
||||
this.context.router.history.push(`/statuses/${status.getIn(['quote', 'id'])}`);
|
||||
}
|
||||
|
||||
_measureHeight (heightJustChanged) {
|
||||
if (this.props.measureHeight && this.node) {
|
||||
scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
|
||||
@ -112,13 +132,12 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||
render () {
|
||||
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||
const outerStyle = { boxSizing: 'border-box' };
|
||||
const { intl, compact, pictureInPicture } = this.props;
|
||||
const { intl, compact, pictureInPicture, quoteMuted } = this.props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let media = '';
|
||||
let applicationLink = '';
|
||||
let reblogLink = '';
|
||||
let reblogIcon = 'retweet';
|
||||
@ -129,63 +148,84 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||
outerStyle.height = `${this.state.height}px`;
|
||||
}
|
||||
|
||||
if (pictureInPicture.get('inUse')) {
|
||||
media = <PictureInPicturePlaceholder />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
const identity = (status, _0, _1, quote = false) => (
|
||||
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} data-acct={status.getIn(['account', 'acct'])} className='detailed-status__display-name'>
|
||||
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={quote ? 18 : 46} /></div>
|
||||
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
|
||||
</a>
|
||||
);
|
||||
|
||||
media = (
|
||||
<Audio
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
||||
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
|
||||
sensitive={status.get('sensitive')}
|
||||
visible={this.props.showMedia}
|
||||
blurhash={attachment.get('blurhash')}
|
||||
height={150}
|
||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
||||
/>
|
||||
);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
const media = (status, quote = false) => {
|
||||
if (pictureInPicture.get('inUse')) {
|
||||
return <PictureInPicturePlaceholder />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
|
||||
media = (
|
||||
<Video
|
||||
preview={attachment.get('preview_url')}
|
||||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||
blurhash={attachment.get('blurhash')}
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
width={300}
|
||||
height={150}
|
||||
inline
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
return (
|
||||
<Audio
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
||||
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
|
||||
sensitive={status.get('sensitive')}
|
||||
visible={this.props.showMedia}
|
||||
blurhash={attachment.get('blurhash')}
|
||||
height={150}
|
||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
||||
quote={quote}
|
||||
/>
|
||||
);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
|
||||
return (
|
||||
<Video
|
||||
preview={attachment.get('preview_url')}
|
||||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||
blurhash={attachment.get('blurhash')}
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
width={300}
|
||||
height={150}
|
||||
inline
|
||||
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')}
|
||||
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')}
|
||||
visible={this.props.showMedia}
|
||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
media = (
|
||||
<MediaGallery
|
||||
standalone
|
||||
sensitive={status.get('sensitive')}
|
||||
media={status.get('media_attachments')}
|
||||
height={300}
|
||||
onOpenMedia={this.props.onOpenMedia}
|
||||
visible={this.props.showMedia}
|
||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
||||
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)} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
if (status.get('application')) {
|
||||
applicationLink = <React.Fragment> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></React.Fragment>;
|
||||
@ -261,10 +301,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||
return (
|
||||
<div style={outerStyle}>
|
||||
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
|
||||
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='detailed-status__display-name'>
|
||||
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={46} /></div>
|
||||
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
|
||||
</a>
|
||||
{identity(status, null, null, false)}
|
||||
|
||||
<StatusContent
|
||||
status={status}
|
||||
@ -273,7 +310,9 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||
onTranslate={this.handleTranslate}
|
||||
/>
|
||||
|
||||
{media}
|
||||
{media(status, false)}
|
||||
|
||||
{quote(status, false, quoteMuted, this.handleQuoteClick, this.handleExpandedQuoteToggle, identity, media, this.context.router)}
|
||||
|
||||
<div className='detailed-status__meta'>
|
||||
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
||||
|
@ -20,6 +20,8 @@ import {
|
||||
deleteStatus,
|
||||
hideStatus,
|
||||
revealStatus,
|
||||
hideQuote,
|
||||
revealQuote,
|
||||
} from '../../../actions/statuses';
|
||||
import { initMuteModal } from '../../../actions/mutes';
|
||||
import { initBlockModal } from '../../../actions/blocks';
|
||||
@ -165,6 +167,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));
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
} from '../../actions/interactions';
|
||||
import {
|
||||
replyCompose,
|
||||
quoteCompose,
|
||||
mentionCompose,
|
||||
directCompose,
|
||||
} from '../../actions/compose';
|
||||
@ -35,6 +36,8 @@ import {
|
||||
revealStatus,
|
||||
translateStatus,
|
||||
undoStatusTranslation,
|
||||
hideQuote,
|
||||
revealQuote,
|
||||
} from '../../actions/statuses';
|
||||
import {
|
||||
unblockAccount,
|
||||
@ -73,6 +76,8 @@ const messages = defineMessages({
|
||||
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
|
||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
|
||||
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||
});
|
||||
|
||||
@ -205,6 +210,7 @@ class Status extends ImmutablePureComponent {
|
||||
state = {
|
||||
fullscreen: false,
|
||||
showMedia: defaultMediaVisibility(this.props.status),
|
||||
showQuoteMedia: defaultMediaVisibility(this.props.status ? this.props.status.get('quote', null) : null),
|
||||
loadedStatusId: undefined,
|
||||
};
|
||||
|
||||
@ -227,7 +233,11 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
|
||||
this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') });
|
||||
this.setState({
|
||||
showMedia: defaultMediaVisibility(nextProps.status),
|
||||
showQuoteMedia: defaultMediaVisibility(nextProps.status.get('quote', null)),
|
||||
loadedStatusId: nextProps.status.get('id'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -235,6 +245,10 @@ class Status extends ImmutablePureComponent {
|
||||
this.setState({ showMedia: !this.state.showMedia });
|
||||
};
|
||||
|
||||
handleToggleQuoteMediaVisibility = () => {
|
||||
this.setState({ showQuoteMedia: !this.state.showQuoteMedia });
|
||||
}
|
||||
|
||||
handleFavouriteClick = (status) => {
|
||||
const { dispatch } = this.props;
|
||||
const { signedIn } = this.context.identity;
|
||||
@ -285,6 +299,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.context.router.history)),
|
||||
}));
|
||||
} else {
|
||||
dispatch(quoteCompose(status, this.context.router.history));
|
||||
}
|
||||
}
|
||||
|
||||
handleModalReblog = (status, privacy) => {
|
||||
this.props.dispatch(reblog(status, privacy));
|
||||
};
|
||||
@ -388,6 +415,14 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
handleQuoteToggleHidden = (status) => {
|
||||
if (status.get('quote_hidden')) {
|
||||
this.props.dispatch(revealQuote(status.get('id')));
|
||||
} else {
|
||||
this.props.dispatch(hideQuote(status.get('id')));
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleAll = () => {
|
||||
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
|
||||
@ -640,9 +675,12 @@ class Status extends ImmutablePureComponent {
|
||||
onOpenMedia={this.handleOpenMedia}
|
||||
onToggleHidden={this.handleToggleHidden}
|
||||
onTranslate={this.handleTranslate}
|
||||
onQuoteToggleHidden={this.handleQuoteToggleHidden}
|
||||
domain={domain}
|
||||
showMedia={this.state.showMedia}
|
||||
showQuoteMedia={this.state.showQuoteMedia}
|
||||
onToggleMediaVisibility={this.handleToggleMediaVisibility}
|
||||
onToggleQuoteMediaVisibility={this.handleToggleQuoteMediaVisibility}
|
||||
pictureInPicture={pictureInPicture}
|
||||
/>
|
||||
|
||||
@ -652,6 +690,7 @@ class Status extends ImmutablePureComponent {
|
||||
onReply={this.handleReplyClick}
|
||||
onFavourite={this.handleFavouriteClick}
|
||||
onReblog={this.handleReblogClick}
|
||||
onQuote={this.handleQuoteClick}
|
||||
onBookmark={this.handleBookmarkClick}
|
||||
onDelete={this.handleDeleteClick}
|
||||
onEdit={this.handleEditClick}
|
||||
|
@ -21,8 +21,15 @@ import {
|
||||
} from '../../ui/util/async-components';
|
||||
import ComposePanel from './compose_panel';
|
||||
import NavigationPanel from './navigation_panel';
|
||||
import TabsBar from './tabs_bar';
|
||||
import { place_tab_bar_at_bottom } from 'mastodon/initial_state';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import { scrollRight } from '../../../scroll';
|
||||
import { Link } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started/);
|
||||
|
||||
const componentMap = {
|
||||
'COMPOSE': Compose,
|
||||
@ -138,26 +145,55 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
||||
const { renderComposePanel } = this.state;
|
||||
|
||||
if (singleColumn) {
|
||||
return (
|
||||
<div className='columns-area__panels'>
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
{renderComposePanel && <ComposePanel />}
|
||||
if (place_tab_bar_at_bottom) {
|
||||
return (
|
||||
<div className='columns-area__panels tab-ber-bottom'>
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
{renderComposePanel && <ComposePanel />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__main timeline'>
|
||||
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div>
|
||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__main navber'>
|
||||
{location.pathname !== '/publish' && <Link to='/publish' className='button bottom_right'><Icon id='pencil' fixedWidth /></Link>}
|
||||
<TabsBar key='tabs' />
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational columns-area__panels__pane-tab-ber'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
<NavigationPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className='columns-area__panels'>
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
{renderComposePanel && <ComposePanel />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__main'>
|
||||
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div>
|
||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
<NavigationPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__main'>
|
||||
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div>
|
||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
<NavigationPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
96
app/javascript/mastodon/features/ui/components/tabs_bar.js
Normal file
@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||
import { place_tab_bar_at_bottom, show_tab_bar_label } from 'mastodon/initial_state';
|
||||
import classNames from 'classnames';
|
||||
import { debounce } from 'lodash';
|
||||
import { isUserTouching } from '../../../is_mobile';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
export const links = [
|
||||
<NavLink className='tabs-bar__link' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /></NavLink>,
|
||||
<NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /></NavLink>,
|
||||
<NavLink className='tabs-bar__link' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /></NavLink>,
|
||||
<NavLink className='tabs-bar__link' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /></NavLink>,
|
||||
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /></NavLink>,
|
||||
<NavLink className='tabs-bar__link hamburger' to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
|
||||
];
|
||||
|
||||
export function getIndex (path) {
|
||||
return links.findIndex(link => link.props.to === path);
|
||||
}
|
||||
|
||||
export function getLink (index) {
|
||||
return links[index].props.to;
|
||||
}
|
||||
|
||||
export default @injectIntl
|
||||
@withRouter
|
||||
class TabsBar extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
setRef = ref => {
|
||||
this.node = ref;
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
// Only apply optimization for touch devices, which we assume are slower
|
||||
// We thus avoid the 250ms delay for non-touch devices and the lag for touch devices
|
||||
if (isUserTouching()) {
|
||||
e.preventDefault();
|
||||
e.persist();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link'));
|
||||
const currentTab = tabs.find(tab => tab.classList.contains('active'));
|
||||
const nextTab = tabs.find(tab => tab.contains(e.target));
|
||||
const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)];
|
||||
|
||||
|
||||
if (currentTab !== nextTab) {
|
||||
if (currentTab) {
|
||||
currentTab.classList.remove('active');
|
||||
}
|
||||
|
||||
const listener = debounce(() => {
|
||||
nextTab.removeEventListener('transitionend', listener);
|
||||
this.props.history.push(to);
|
||||
}, 50);
|
||||
|
||||
nextTab.addEventListener('transitionend', listener);
|
||||
nextTab.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
identity: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl: { formatMessage } } = this.props;
|
||||
return (
|
||||
<div className='tabs-bar__wrapper'>
|
||||
<nav className={classNames('tabs-bar', { 'bottom-bar': place_tab_bar_at_bottom })} ref={this.setRef}>
|
||||
{links.map(link => React.cloneElement(link, { key: link.props.to, className: classNames(link.props.className, { 'short-label': show_tab_bar_label }), onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
|
||||
</nav>
|
||||
|
||||
<div id='tabs-bar__portal' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -121,6 +121,7 @@ class Video extends React.PureComponent {
|
||||
autoPlay: PropTypes.bool,
|
||||
volume: PropTypes.number,
|
||||
muted: PropTypes.bool,
|
||||
quote: PropTypes.bool,
|
||||
componentIndex: PropTypes.number,
|
||||
autoFocus: PropTypes.bool,
|
||||
};
|
||||
@ -524,7 +525,7 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
|
||||
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, editable, blurhash, autoFocus, quote } = this.props;
|
||||
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||
const playerStyle = {};
|
||||
@ -538,6 +539,11 @@ class Video extends React.PureComponent {
|
||||
playerStyle.height = height;
|
||||
}
|
||||
|
||||
if (quote && height) {
|
||||
height /= 2;
|
||||
playerStyle.height = height;
|
||||
}
|
||||
|
||||
let preload;
|
||||
|
||||
if (this.props.currentTime || fullscreen || dragging) {
|
||||
|
@ -135,5 +135,7 @@ export const version = getMeta('version');
|
||||
export const translationEnabled = getMeta('translation_enabled');
|
||||
export const languages = initialState?.languages;
|
||||
export const statusPageUrl = getMeta('status_page_url');
|
||||
export const place_tab_bar_at_bottom = getMeta('place_tab_bar_at_bottom');
|
||||
export const show_tab_bar_label = getMeta('show_tab_bar_label');
|
||||
|
||||
export default initialState;
|
||||
|
@ -607,6 +607,14 @@
|
||||
"defaultMessage": "This post cannot be boosted",
|
||||
"id": "status.cannot_reblog"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Quote",
|
||||
"id": "status.quote"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "This post cannot be quoted",
|
||||
"id": "status.cannot_quote"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Favourite",
|
||||
"id": "status.favourite"
|
||||
@ -764,6 +772,14 @@
|
||||
{
|
||||
"defaultMessage": "Replied to {name}",
|
||||
"id": "status.replied_to"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Muted quote",
|
||||
"id": "status.muted_quote"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Unlisted quote",
|
||||
"id": "status.unlisted_quote"
|
||||
}
|
||||
],
|
||||
"path": "app/javascript/mastodon/components/status.json"
|
||||
@ -3716,6 +3732,14 @@
|
||||
"defaultMessage": "Detailed conversation view",
|
||||
"id": "status.detailed_status"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Quote",
|
||||
"id": "confirmations.quote.confirm"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
||||
"id": "confirmations.quote.message"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Reply",
|
||||
"id": "confirmations.reply.confirm"
|
||||
@ -4335,4 +4359,4 @@
|
||||
],
|
||||
"path": "app/javascript/mastodon/features/video/index.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
@ -169,6 +169,8 @@
|
||||
"confirmations.mute.confirm": "Mute",
|
||||
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
|
||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||
"confirmations.quote.confirm": "Quote",
|
||||
"confirmations.quote.message": "Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
||||
"confirmations.redraft.confirm": "Delete & redraft",
|
||||
"confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
|
||||
"confirmations.reply.confirm": "Reply",
|
||||
@ -553,6 +555,7 @@
|
||||
"status.block": "Block @{name}",
|
||||
"status.bookmark": "Bookmark",
|
||||
"status.cancel_reblog_private": "Unboost",
|
||||
"status.cannot_quote": "This post cannot be quoted",
|
||||
"status.cannot_reblog": "This post cannot be boosted",
|
||||
"status.copy": "Copy link to post",
|
||||
"status.delete": "Delete",
|
||||
@ -574,9 +577,11 @@
|
||||
"status.more": "More",
|
||||
"status.mute": "Mute @{name}",
|
||||
"status.mute_conversation": "Mute conversation",
|
||||
"status.muted_quote": "Muted quote",
|
||||
"status.open": "Expand this post",
|
||||
"status.pin": "Pin on profile",
|
||||
"status.pinned": "Pinned post",
|
||||
"status.quote": "Quote",
|
||||
"status.read_more": "Read more",
|
||||
"status.reblog": "Boost",
|
||||
"status.reblog_private": "Boost with original visibility",
|
||||
@ -599,6 +604,7 @@
|
||||
"status.translate": "Translate",
|
||||
"status.translated_from_with": "Translated from {lang} using {provider}",
|
||||
"status.uncached_media_warning": "Not available",
|
||||
"status.unlisted_quote": "Unlisted quote",
|
||||
"status.unmute_conversation": "Unmute conversation",
|
||||
"status.unpin": "Unpin from profile",
|
||||
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
|
||||
|
@ -21,13 +21,13 @@
|
||||
"account.browse_more_on_origin_server": "リモートで表示",
|
||||
"account.cancel_follow_request": "フォローリクエストの取り消し",
|
||||
"account.direct": "@{name}さんにダイレクトメッセージ",
|
||||
"account.disable_notifications": "@{name}さんの投稿時の通知を停止",
|
||||
"account.disable_notifications": "@{name}さんのトゥート時の通知を停止",
|
||||
"account.domain_blocked": "ドメインブロック中",
|
||||
"account.edit_profile": "プロフィール編集",
|
||||
"account.enable_notifications": "@{name}さんの投稿時に通知",
|
||||
"account.enable_notifications": "@{name}さんのトゥート時に通知",
|
||||
"account.endorse": "プロフィールで紹介する",
|
||||
"account.featured_tags.last_status_at": "最終投稿 {date}",
|
||||
"account.featured_tags.last_status_never": "投稿がありません",
|
||||
"account.featured_tags.last_status_at": "最終トゥート {date}",
|
||||
"account.featured_tags.last_status_never": "トゥートがありません",
|
||||
"account.featured_tags.title": "{name}の注目ハッシュタグ",
|
||||
"account.follow": "フォロー",
|
||||
"account.followers": "フォロワー",
|
||||
@ -50,14 +50,14 @@
|
||||
"account.mute_notifications": "@{name}さんからの通知を受け取らない",
|
||||
"account.muted": "ミュート済み",
|
||||
"account.open_original_page": "元のページを開く",
|
||||
"account.posts": "投稿",
|
||||
"account.posts_with_replies": "投稿と返信",
|
||||
"account.posts": "トゥート",
|
||||
"account.posts_with_replies": "トゥートと返信",
|
||||
"account.report": "@{name}さんを通報",
|
||||
"account.requested": "フォロー承認待ちです。クリックしてキャンセル",
|
||||
"account.requested_follow": "{name}さんがあなたにフォローリクエストしました",
|
||||
"account.share": "@{name}さんのプロフィールを共有する",
|
||||
"account.show_reblogs": "@{name}さんからのブーストを表示",
|
||||
"account.statuses_counter": "{counter} 投稿",
|
||||
"account.statuses_counter": "{counter} トゥート",
|
||||
"account.unblock": "@{name}さんのブロックを解除",
|
||||
"account.unblock_domain": "{domain}のブロックを解除",
|
||||
"account.unblock_short": "ブロック解除",
|
||||
@ -111,7 +111,7 @@
|
||||
"column.lists": "リスト",
|
||||
"column.mutes": "ミュートしたユーザー",
|
||||
"column.notifications": "通知",
|
||||
"column.pins": "固定された投稿",
|
||||
"column.pins": "固定されたトゥート",
|
||||
"column.public": "連合タイムライン",
|
||||
"column_back_button.label": "戻る",
|
||||
"column_header.hide_settings": "設定を隠す",
|
||||
@ -127,9 +127,9 @@
|
||||
"compose.language.change": "言語を変更",
|
||||
"compose.language.search": "言語を検索...",
|
||||
"compose_form.direct_message_warning_learn_more": "もっと詳しく",
|
||||
"compose_form.encryption_warning": "Mastodonの投稿はエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。",
|
||||
"compose_form.hashtag_warning": "この投稿は公開設定ではないのでハッシュタグの一覧に表示されません。公開投稿だけがハッシュタグで検索できます。",
|
||||
"compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。",
|
||||
"compose_form.encryption_warning": "Mastodonのトゥートはエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。",
|
||||
"compose_form.hashtag_warning": "このトゥートは公開設定ではないのでハッシュタグの一覧に表示されません。公開トゥートだけがハッシュタグで検索できます。",
|
||||
"compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定のトゥートを見ることができます。",
|
||||
"compose_form.lock_disclaimer.lock": "承認制",
|
||||
"compose_form.placeholder": "今なにしてる?",
|
||||
"compose_form.poll.add_option": "追加",
|
||||
@ -138,8 +138,8 @@
|
||||
"compose_form.poll.remove_option": "この項目を削除",
|
||||
"compose_form.poll.switch_to_multiple": "複数選択に変更",
|
||||
"compose_form.poll.switch_to_single": "単一選択に変更",
|
||||
"compose_form.publish": "投稿",
|
||||
"compose_form.publish_form": "投稿",
|
||||
"compose_form.publish": "トゥート",
|
||||
"compose_form.publish_form": "トゥート",
|
||||
"compose_form.publish_loud": "{publish}!",
|
||||
"compose_form.save_changes": "変更を保存",
|
||||
"compose_form.sensitive.hide": "メディアを閲覧注意にする",
|
||||
@ -148,6 +148,9 @@
|
||||
"compose_form.spoiler.marked": "本文は警告の後ろに隠されます",
|
||||
"compose_form.spoiler.unmarked": "本文は隠されていません",
|
||||
"compose_form.spoiler_placeholder": "ここに警告を書いてください",
|
||||
"compose_form.utilBtns_goji": "誤字盛!",
|
||||
"compose_form.utilBtns_harukin": "はるきん焼却",
|
||||
"compose_form.utilBtns_risa": "りさ姉",
|
||||
"confirmation_modal.cancel": "キャンセル",
|
||||
"confirmations.block.block_and_report": "ブロックし通報",
|
||||
"confirmations.block.confirm": "ブロック",
|
||||
@ -165,10 +168,12 @@
|
||||
"confirmations.logout.confirm": "ログアウト",
|
||||
"confirmations.logout.message": "本当にログアウトしますか?",
|
||||
"confirmations.mute.confirm": "ミュート",
|
||||
"confirmations.mute.explanation": "これにより相手の投稿と返信は見えなくなりますが、相手はあなたをフォローし続け投稿を見ることができます。",
|
||||
"confirmations.mute.explanation": "これにより相手のトゥートと返信は見えなくなりますが、相手はあなたをフォローし続けトゥートを見ることができます。",
|
||||
"confirmations.mute.message": "本当に{name}さんをミュートしますか?",
|
||||
"confirmations.quote.confirm": "引用",
|
||||
"confirmations.quote.message": "今引用すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
|
||||
"confirmations.redraft.confirm": "削除して下書きに戻す",
|
||||
"confirmations.redraft.message": "本当にこの投稿を削除して下書きに戻しますか? この投稿へのお気に入り登録やブーストは失われ、返信は孤立することになります。",
|
||||
"confirmations.redraft.message": "本当にこのトゥートを削除して下書きに戻しますか? このトゥートへのお気に入り登録やブーストは失われ、返信は孤立することになります。",
|
||||
"confirmations.reply.confirm": "返信",
|
||||
"confirmations.reply.message": "今返信すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
|
||||
"confirmations.unfollow.confirm": "フォロー解除",
|
||||
@ -185,12 +190,12 @@
|
||||
"directory.recently_active": "最近の活動順",
|
||||
"disabled_account_banner.account_settings": "アカウント設定",
|
||||
"disabled_account_banner.text": "あなたのアカウント『{disabledAccount}』は現在無効になっています。",
|
||||
"dismissable_banner.community_timeline": "これらは{domain}がホストしている人たちの最新の公開投稿です。",
|
||||
"dismissable_banner.community_timeline": "これらは{domain}がホストしている人たちの最新の公開トゥートです。",
|
||||
"dismissable_banner.dismiss": "閉じる",
|
||||
"dismissable_banner.explore_links": "これらのニュース記事は現在分散型ネットワークの他のサーバーの人たちに話されています。",
|
||||
"dismissable_banner.explore_statuses": "分散型ネットワーク内の他のサーバーのこれらの投稿は現在このサーバー上で注目されています。",
|
||||
"dismissable_banner.explore_statuses": "分散型ネットワーク内の他のサーバーのこれらのトゥートは現在このサーバー上で注目されています。",
|
||||
"dismissable_banner.explore_tags": "これらのハッシュタグは現在分散型ネットワークの他のサーバーの人たちに話されています。",
|
||||
"dismissable_banner.public_timeline": "これらの投稿はこのサーバーが知っている分散型ネットワークの他のサーバーの人たちの最新の公開投稿です。",
|
||||
"dismissable_banner.public_timeline": "これらのトゥートはこのサーバーが知っている分散型ネットワークの他のサーバーの人たちの最新の公開トゥートです。",
|
||||
"embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。",
|
||||
"embed.preview": "表示例:",
|
||||
"emoji_button.activity": "活動",
|
||||
@ -209,7 +214,7 @@
|
||||
"emoji_button.symbols": "記号",
|
||||
"emoji_button.travel": "旅行と場所",
|
||||
"empty_column.account_suspended": "アカウントは停止されています",
|
||||
"empty_column.account_timeline": "投稿がありません!",
|
||||
"empty_column.account_timeline": "トゥートがありません!",
|
||||
"empty_column.account_unavailable": "プロフィールは利用できません",
|
||||
"empty_column.blocks": "まだ誰もブロックしていません。",
|
||||
"empty_column.bookmarked_statuses": "まだ何もブックマーク登録していません。ブックマーク登録するとここに表示されます。",
|
||||
@ -225,11 +230,11 @@
|
||||
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
|
||||
"empty_column.home": "ホームタイムラインはまだ空っぽです。誰かフォローして埋めてみましょう。 {suggestions}",
|
||||
"empty_column.home.suggestions": "おすすめを見る",
|
||||
"empty_column.list": "このリストにはまだなにもありません。このリストのメンバーが新しい投稿をするとここに表示されます。",
|
||||
"empty_column.list": "このリストにはまだなにもありません。このリストのメンバーが新しいトゥートをするとここに表示されます。",
|
||||
"empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。",
|
||||
"empty_column.mutes": "まだ誰もミュートしていません。",
|
||||
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
|
||||
"empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう",
|
||||
"empty_column.public": "ここにはまだ何もありません! 公開で何かをトゥートしたり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう",
|
||||
"error.unexpected_crash.explanation": "不具合かブラウザの互換性問題のため、このページを正しく表示できませんでした。",
|
||||
"error.unexpected_crash.explanation_addons": "このページは正しく表示できませんでした。このエラーはブラウザのアドオンや自動翻訳ツールによって引き起こされることがあります。",
|
||||
"error.unexpected_crash.next_steps": "ページの再読み込みをお試しください。それでも解決しない場合、別のブラウザかアプリを使えば使用できることがあります。",
|
||||
@ -240,27 +245,27 @@
|
||||
"explore.suggested_follows": "おすすめ",
|
||||
"explore.title": "エクスプローラー",
|
||||
"explore.trending_links": "ニュース",
|
||||
"explore.trending_statuses": "投稿",
|
||||
"explore.trending_statuses": "トゥート",
|
||||
"explore.trending_tags": "ハッシュタグ",
|
||||
"filter_modal.added.context_mismatch_explanation": "このフィルターカテゴリーはあなたがアクセスした投稿のコンテキストには適用されません。この投稿のコンテキストでもフィルターを適用するにはフィルターを編集する必要があります。",
|
||||
"filter_modal.added.context_mismatch_explanation": "このフィルターカテゴリーはあなたがアクセスしたトゥートのコンテキストには適用されません。このトゥートのコンテキストでもフィルターを適用するにはフィルターを編集する必要があります。",
|
||||
"filter_modal.added.context_mismatch_title": "コンテキストが一致しません!",
|
||||
"filter_modal.added.expired_explanation": "このフィルターカテゴリは有効期限が切れています。適用するには有効期限を更新してください。",
|
||||
"filter_modal.added.expired_title": "フィルターの有効期限が切れています!",
|
||||
"filter_modal.added.review_and_configure": "このフィルターカテゴリーを確認して設定するには、{settings_link}に移動します。",
|
||||
"filter_modal.added.review_and_configure_title": "フィルター設定",
|
||||
"filter_modal.added.settings_link": "設定",
|
||||
"filter_modal.added.short_explanation": "この投稿はフィルターカテゴリー『{title}』に追加されました。",
|
||||
"filter_modal.added.short_explanation": "このトゥートはフィルターカテゴリー『{title}』に追加されました。",
|
||||
"filter_modal.added.title": "フィルターを追加しました!",
|
||||
"filter_modal.select_filter.context_mismatch": "このコンテキストには当てはまりません",
|
||||
"filter_modal.select_filter.expired": "期限切れ",
|
||||
"filter_modal.select_filter.prompt_new": "新しいカテゴリー: {name}",
|
||||
"filter_modal.select_filter.search": "検索または新規作成",
|
||||
"filter_modal.select_filter.subtitle": "既存のカテゴリーを使用するか新規作成します",
|
||||
"filter_modal.select_filter.title": "この投稿をフィルターする",
|
||||
"filter_modal.title.status": "投稿をフィルターする",
|
||||
"filter_modal.select_filter.title": "このトゥートをフィルターする",
|
||||
"filter_modal.title.status": "トゥートをフィルターする",
|
||||
"follow_recommendations.done": "完了",
|
||||
"follow_recommendations.heading": "投稿を見たい人をフォローしてください!ここにおすすめがあります。",
|
||||
"follow_recommendations.lead": "あなたがフォローしている人の投稿は、ホームフィードに時系列で表示されます。いつでも簡単に解除できるので、気軽にフォローしてみてください!",
|
||||
"follow_recommendations.heading": "トゥートを見たい人をフォローしてください!ここにおすすめがあります。",
|
||||
"follow_recommendations.lead": "あなたがフォローしている人のトゥートは、ホームフィードに時系列で表示されます。いつでも簡単に解除できるので、気軽にフォローしてみてください!",
|
||||
"follow_request.authorize": "許可",
|
||||
"follow_request.reject": "拒否",
|
||||
"follow_requests.unlocked_explanation": "あなたのアカウントは承認制ではありませんが、{domain}のスタッフはこれらのアカウントからのフォローリクエストの確認が必要であると判断しました。",
|
||||
@ -291,18 +296,18 @@
|
||||
"home.column_settings.show_replies": "返信表示",
|
||||
"home.hide_announcements": "お知らせを隠す",
|
||||
"home.show_announcements": "お知らせを表示",
|
||||
"interaction_modal.description.favourite": "Mastodonのアカウントでこの投稿をお気に入りに入れて投稿者に感謝を知らせたり保存することができます。",
|
||||
"interaction_modal.description.follow": "Mastodonのアカウントで{name}さんをフォローしてホームフィードで投稿を受け取れます。",
|
||||
"interaction_modal.description.reblog": "Mastodonのアカウントでこの投稿をブーストして自分のフォロワーに共有できます。",
|
||||
"interaction_modal.description.reply": "Mastodonのアカウントでこの投稿に反応できます。",
|
||||
"interaction_modal.description.favourite": "Mastodonのアカウントでこのトゥートをお気に入りに入れてトゥート者に感謝を知らせたり保存することができます。",
|
||||
"interaction_modal.description.follow": "Mastodonのアカウントで{name}さんをフォローしてホームフィードでトゥートを受け取れます。",
|
||||
"interaction_modal.description.reblog": "Mastodonのアカウントでこのトゥートをブーストして自分のフォロワーに共有できます。",
|
||||
"interaction_modal.description.reply": "Mastodonのアカウントでこのトゥートに反応できます。",
|
||||
"interaction_modal.on_another_server": "別のサーバー",
|
||||
"interaction_modal.on_this_server": "このサーバー",
|
||||
"interaction_modal.other_server_instructions": "このURLをお気に入りのMastodonアプリやMastodonサーバーのWebインターフェースの検索フィールドにコピーして貼り付けます。",
|
||||
"interaction_modal.preamble": "Mastodonは分散化されているためアカウントを持っていなくても別のMastodonサーバーまたは互換性のあるプラットフォームでホストされているアカウントを使用できます。",
|
||||
"interaction_modal.title.favourite": "{name}さんの投稿をお気に入り",
|
||||
"interaction_modal.title.favourite": "{name}さんのトゥートをお気に入り",
|
||||
"interaction_modal.title.follow": "{name}さんをフォロー",
|
||||
"interaction_modal.title.reblog": "{name}さんの投稿をブースト",
|
||||
"interaction_modal.title.reply": "{name}さんの投稿にリプライ",
|
||||
"interaction_modal.title.reblog": "{name}さんのトゥートをブースト",
|
||||
"interaction_modal.title.reply": "{name}さんのトゥートにリプライ",
|
||||
"intervals.full.days": "{number}日",
|
||||
"intervals.full.hours": "{number}時間",
|
||||
"intervals.full.minutes": "{number}分",
|
||||
@ -310,11 +315,11 @@
|
||||
"keyboard_shortcuts.blocked": "ブロックしたユーザーのリストを開く",
|
||||
"keyboard_shortcuts.boost": "ブースト",
|
||||
"keyboard_shortcuts.column": "左からn番目のカラムの最新に移動",
|
||||
"keyboard_shortcuts.compose": "投稿の入力欄に移動",
|
||||
"keyboard_shortcuts.compose": "トゥートの入力欄に移動",
|
||||
"keyboard_shortcuts.description": "説明",
|
||||
"keyboard_shortcuts.direct": "ダイレクトメッセージのカラムを開く",
|
||||
"keyboard_shortcuts.down": "カラム内一つ下に移動",
|
||||
"keyboard_shortcuts.enter": "投稿の詳細を表示",
|
||||
"keyboard_shortcuts.enter": "トゥートの詳細を表示",
|
||||
"keyboard_shortcuts.favourite": "お気に入り",
|
||||
"keyboard_shortcuts.favourites": "お気に入り登録のリストを開く",
|
||||
"keyboard_shortcuts.federated": "連合タイムラインを開く",
|
||||
@ -328,7 +333,7 @@
|
||||
"keyboard_shortcuts.my_profile": "自分のプロフィールを開く",
|
||||
"keyboard_shortcuts.notifications": "通知カラムを開く",
|
||||
"keyboard_shortcuts.open_media": "メディアを開く",
|
||||
"keyboard_shortcuts.pinned": "固定した投稿のリストを開く",
|
||||
"keyboard_shortcuts.pinned": "固定したトゥートのリストを開く",
|
||||
"keyboard_shortcuts.profile": "プロフィールを開く",
|
||||
"keyboard_shortcuts.reply": "返信",
|
||||
"keyboard_shortcuts.requests": "フォローリクエストのリストを開く",
|
||||
@ -337,8 +342,8 @@
|
||||
"keyboard_shortcuts.start": "\"スタート\" カラムを開く",
|
||||
"keyboard_shortcuts.toggle_hidden": "CWで隠れた文を見る/隠す",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "非表示のメディアを見る/隠す",
|
||||
"keyboard_shortcuts.toot": "新規投稿",
|
||||
"keyboard_shortcuts.unfocus": "投稿の入力欄・検索欄から離れる",
|
||||
"keyboard_shortcuts.toot": "新規トゥート",
|
||||
"keyboard_shortcuts.unfocus": "トゥートの入力欄・検索欄から離れる",
|
||||
"keyboard_shortcuts.up": "カラム内一つ上に移動",
|
||||
"lightbox.close": "閉じる",
|
||||
"lightbox.compress": "画像ビューボックスを閉じる",
|
||||
@ -373,7 +378,7 @@
|
||||
"navigation_bar.blocks": "ブロックしたユーザー",
|
||||
"navigation_bar.bookmarks": "ブックマーク",
|
||||
"navigation_bar.community_timeline": "ローカルタイムライン",
|
||||
"navigation_bar.compose": "投稿の新規作成",
|
||||
"navigation_bar.compose": "トゥートの新規作成",
|
||||
"navigation_bar.direct": "ダイレクトメッセージ",
|
||||
"navigation_bar.discover": "見つける",
|
||||
"navigation_bar.domain_blocks": "ブロックしたドメイン",
|
||||
@ -388,7 +393,7 @@
|
||||
"navigation_bar.logout": "ログアウト",
|
||||
"navigation_bar.mutes": "ミュートしたユーザー",
|
||||
"navigation_bar.personal": "個人用",
|
||||
"navigation_bar.pins": "固定した投稿",
|
||||
"navigation_bar.pins": "固定したトゥート",
|
||||
"navigation_bar.preferences": "ユーザー設定",
|
||||
"navigation_bar.public_timeline": "連合タイムライン",
|
||||
"navigation_bar.search": "検索",
|
||||
@ -396,15 +401,15 @@
|
||||
"not_signed_in_indicator.not_signed_in": "この機能を使うにはログインする必要があります。",
|
||||
"notification.admin.report": "{name}さんが{target}さんを通報しました",
|
||||
"notification.admin.sign_up": "{name}さんがサインアップしました",
|
||||
"notification.favourite": "{name}さんがあなたの投稿をお気に入りに登録しました",
|
||||
"notification.favourite": "{name}さんがあなたのトゥートに╰( ^o^)╮-=ニ=一=三★しました",
|
||||
"notification.follow": "{name}さんにフォローされました",
|
||||
"notification.follow_request": "{name}さんがあなたにフォローリクエストしました",
|
||||
"notification.mention": "{name}さんがあなたに返信しました",
|
||||
"notification.own_poll": "アンケートが終了しました",
|
||||
"notification.poll": "アンケートが終了しました",
|
||||
"notification.reblog": "{name}さんがあなたの投稿をブーストしました",
|
||||
"notification.status": "{name}さんが投稿しました",
|
||||
"notification.update": "{name}さんが投稿を編集しました",
|
||||
"notification.reblog": "{name}さんがあなたのトゥートをブーストしました",
|
||||
"notification.status": "{name}さんがトゥートしました",
|
||||
"notification.update": "{name}さんがトゥートを編集しました",
|
||||
"notifications.clear": "通知を消去",
|
||||
"notifications.clear_confirmation": "本当に通知を消去しますか?",
|
||||
"notifications.column_settings.admin.report": "新しい通報:",
|
||||
@ -422,7 +427,7 @@
|
||||
"notifications.column_settings.reblog": "ブースト:",
|
||||
"notifications.column_settings.show": "カラムに表示",
|
||||
"notifications.column_settings.sound": "通知音を再生",
|
||||
"notifications.column_settings.status": "新しい投稿:",
|
||||
"notifications.column_settings.status": "新しいトゥート:",
|
||||
"notifications.column_settings.unread_notifications.category": "未読の通知:",
|
||||
"notifications.column_settings.unread_notifications.highlight": "未読の通知を強調表示",
|
||||
"notifications.column_settings.update": "編集:",
|
||||
@ -479,20 +484,20 @@
|
||||
"relative_time.today": "今日",
|
||||
"reply_indicator.cancel": "キャンセル",
|
||||
"report.block": "ブロック",
|
||||
"report.block_explanation": "相手の投稿が表示されなくなります。相手はあなたの投稿を見ることやフォローすることができません。相手はブロックされていることがわかります。",
|
||||
"report.block_explanation": "相手のトゥートが表示されなくなります。相手はあなたのトゥートを見ることやフォローすることができません。相手はブロックされていることがわかります。",
|
||||
"report.categories.other": "その他",
|
||||
"report.categories.spam": "スパム",
|
||||
"report.categories.violation": "サーバーのルールに違反",
|
||||
"report.category.subtitle": "近いものを選択してください",
|
||||
"report.category.title": "この{type}について教えてください",
|
||||
"report.category.title_account": "プロフィール",
|
||||
"report.category.title_status": "投稿",
|
||||
"report.category.title_status": "トゥート",
|
||||
"report.close": "完了",
|
||||
"report.comment.title": "その他に私たちに伝えておくべき事はありますか?",
|
||||
"report.forward": "{target}に転送する",
|
||||
"report.forward_hint": "このアカウントは別のサーバーに所属しています。通報内容を匿名で転送しますか?",
|
||||
"report.mute": "ミュート",
|
||||
"report.mute_explanation": "相手の投稿は表示されなくなります。相手は引き続きあなたをフォローして、あなたの投稿を表示することができますが、ミュートされていることはわかりません。",
|
||||
"report.mute_explanation": "相手のトゥートは表示されなくなります。相手は引き続きあなたをフォローして、あなたのトゥートを表示することができますが、ミュートされていることはわかりません。",
|
||||
"report.next": "次へ",
|
||||
"report.placeholder": "追加コメント",
|
||||
"report.reasons.dislike": "興味がありません",
|
||||
@ -506,7 +511,7 @@
|
||||
"report.rules.subtitle": "当てはまるものをすべて選んでください:",
|
||||
"report.rules.title": "どのルールに違反していますか?",
|
||||
"report.statuses.subtitle": "当てはまるものをすべて選んでください:",
|
||||
"report.statuses.title": "この通報を裏付けるような投稿はありますか?",
|
||||
"report.statuses.title": "この通報を裏付けるようなトゥートはありますか?",
|
||||
"report.submit": "通報する",
|
||||
"report.target": "{target}さんを通報する",
|
||||
"report.thanks.take_action": "次のような方法はいかがでしょうか?",
|
||||
@ -514,8 +519,8 @@
|
||||
"report.thanks.title": "見えないようにしたいですか?",
|
||||
"report.thanks.title_actionable": "ご報告ありがとうございます、追って確認します。",
|
||||
"report.unfollow": "@{name}さんのフォローを解除",
|
||||
"report.unfollow_explanation": "このアカウントをフォローしています。ホームフィードに彼らの投稿を表示しないようにするには、彼らのフォローを外してください。",
|
||||
"report_notification.attached_statuses": "{count, plural, one {{count}件の投稿} other {{count}件の投稿}}が添付されました。",
|
||||
"report.unfollow_explanation": "このアカウントをフォローしています。ホームフィードに彼らのトゥートを表示しないようにするには、彼らのフォローを外してください。",
|
||||
"report_notification.attached_statuses": "{count, plural, one {{count}件のトゥート} other {{count}件のトゥート}}が添付されました。",
|
||||
"report_notification.categories.other": "その他",
|
||||
"report_notification.categories.spam": "スパム",
|
||||
"report_notification.categories.violation": "ルール違反",
|
||||
@ -523,17 +528,17 @@
|
||||
"search.placeholder": "検索",
|
||||
"search.search_or_paste": "検索またはURLを入力",
|
||||
"search_popout.search_format": "高度な検索フォーマット",
|
||||
"search_popout.tips.full_text": "表示名やユーザー名、ハッシュタグのほか、あなたの投稿やお気に入り、ブーストした投稿、返信に一致する単純なテキスト。",
|
||||
"search_popout.tips.full_text": "表示名やユーザー名、ハッシュタグのほか、あなたのトゥートやお気に入り、ブーストしたトゥート、返信に一致する単純なテキスト。",
|
||||
"search_popout.tips.hashtag": "ハッシュタグ",
|
||||
"search_popout.tips.status": "投稿",
|
||||
"search_popout.tips.status": "トゥート",
|
||||
"search_popout.tips.text": "表示名やユーザー名、ハッシュタグに一致する単純なテキスト",
|
||||
"search_popout.tips.user": "ユーザー",
|
||||
"search_results.accounts": "人々",
|
||||
"search_results.all": "すべて",
|
||||
"search_results.hashtags": "ハッシュタグ",
|
||||
"search_results.nothing_found": "この検索条件では何も見つかりませんでした",
|
||||
"search_results.statuses": "投稿",
|
||||
"search_results.statuses_fts_disabled": "このサーバーでは投稿本文の検索は利用できません。",
|
||||
"search_results.statuses": "トゥート",
|
||||
"search_results.statuses_fts_disabled": "このサーバーではトゥート本文の検索は利用できません。",
|
||||
"search_results.title": "『{q}』の検索結果",
|
||||
"search_results.total": "{count, number}件の結果",
|
||||
"server_banner.about_active_users": "過去30日間にこのサーバーを使用している人 (月間アクティブユーザー)",
|
||||
@ -544,15 +549,16 @@
|
||||
"server_banner.server_stats": "サーバーの情報",
|
||||
"sign_in_banner.create_account": "アカウント作成",
|
||||
"sign_in_banner.sign_in": "ログイン",
|
||||
"sign_in_banner.text": "ログインしてプロファイルやハッシュタグ、お気に入りをフォローしたり、投稿を共有したり、返信したり、別のサーバーのアカウントと交流したりできます。",
|
||||
"sign_in_banner.text": "ログインしてプロファイルやハッシュタグ、お気に入りをフォローしたり、トゥートを共有したり、返信したり、別のサーバーのアカウントと交流したりできます。",
|
||||
"status.admin_account": "@{name}さんのモデレーション画面を開く",
|
||||
"status.admin_domain": "{domain}のモデレーション画面を開く",
|
||||
"status.admin_status": "この投稿をモデレーション画面で開く",
|
||||
"status.admin_status": "このトゥートをモデレーション画面で開く",
|
||||
"status.block": "@{name}さんをブロック",
|
||||
"status.bookmark": "ブックマーク",
|
||||
"status.cancel_reblog_private": "ブースト解除",
|
||||
"status.cannot_reblog": "この投稿はブーストできません",
|
||||
"status.copy": "投稿へのリンクをコピー",
|
||||
"status.cannot_reblog": "このトゥートはブーストできません",
|
||||
"status.copy": "トゥートへのリンクをコピー",
|
||||
"status.cannot_quote": "このトゥートは引用できません",
|
||||
"status.delete": "削除",
|
||||
"status.detailed_status": "詳細な会話ビュー",
|
||||
"status.direct": "@{name}さんにダイレクトメッセージ",
|
||||
@ -561,20 +567,22 @@
|
||||
"status.edited_x_times": "{count}回編集",
|
||||
"status.embed": "埋め込み",
|
||||
"status.favourite": "お気に入り",
|
||||
"status.filter": "この投稿をフィルターする",
|
||||
"status.filter": "このトゥートをフィルターする",
|
||||
"status.filtered": "フィルターされました",
|
||||
"status.hide": "投稿を非表示",
|
||||
"status.hide": "トゥートを非表示",
|
||||
"status.history.created": "{name}さんが{date}に作成",
|
||||
"status.history.edited": "{name}さんが{date}に編集",
|
||||
"status.load_more": "もっと見る",
|
||||
"status.media_hidden": "非表示のメディア",
|
||||
"status.mention": "@{name}さんに投稿",
|
||||
"status.mention": "@{name}さんにトゥート",
|
||||
"status.more": "もっと見る",
|
||||
"status.mute": "@{name}さんをミュート",
|
||||
"status.mute_conversation": "会話をミュート",
|
||||
"status.muted_quote": "ミュートされた引用",
|
||||
"status.open": "詳細を表示",
|
||||
"status.pin": "プロフィールに固定表示",
|
||||
"status.pinned": "固定された投稿",
|
||||
"status.pinned": "固定されたトゥート",
|
||||
"status.quote": "引用",
|
||||
"status.read_more": "もっと見る",
|
||||
"status.reblog": "ブースト",
|
||||
"status.reblog_private": "ブースト",
|
||||
@ -597,9 +605,10 @@
|
||||
"status.translate": "翻訳",
|
||||
"status.translated_from_with": "{provider}を使って{lang}から翻訳",
|
||||
"status.uncached_media_warning": "利用できません",
|
||||
"status.unlisted_quote": "未収載の引用",
|
||||
"status.unmute_conversation": "会話のミュートを解除",
|
||||
"status.unpin": "プロフィールへの固定を解除",
|
||||
"subscribed_languages.lead": "選択した言語の投稿だけがホームとリストのタイムラインに表示されます。全ての言語の投稿を受け取る場合は全てのチェックを外して下さい。",
|
||||
"subscribed_languages.lead": "選択した言語のトゥートだけがホームとリストのタイムラインに表示されます。全ての言語のトゥートを受け取る場合は全てのチェックを外して下さい。",
|
||||
"subscribed_languages.save": "変更を保存",
|
||||
"subscribed_languages.target": "{target}さんの購読言語を変更します",
|
||||
"suggestions.dismiss": "隠す",
|
||||
@ -616,10 +625,10 @@
|
||||
"timeline_hint.remote_resource_not_displayed": "他のサーバーの{resource}は表示されません。",
|
||||
"timeline_hint.resources.followers": "フォロワー",
|
||||
"timeline_hint.resources.follows": "フォロー",
|
||||
"timeline_hint.resources.statuses": "以前の投稿",
|
||||
"timeline_hint.resources.statuses": "以前のトゥート",
|
||||
"trends.counter_by_accounts": "過去{days, plural, one {{days}日} other {{days}日}}に{count, plural, one {{counter}人} other {{counter} 人}}",
|
||||
"trends.trending_now": "トレンドタグ",
|
||||
"ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
|
||||
"ui.beforeunload": "Mastodonから離れると送信前のトゥートは失われます。",
|
||||
"units.short.billion": "{count}B",
|
||||
"units.short.million": "{count}M",
|
||||
"units.short.thousand": "{count}K",
|
||||
|
@ -4,6 +4,8 @@ import {
|
||||
COMPOSE_CHANGE,
|
||||
COMPOSE_REPLY,
|
||||
COMPOSE_REPLY_CANCEL,
|
||||
COMPOSE_QUOTE,
|
||||
COMPOSE_QUOTE_CANCEL,
|
||||
COMPOSE_DIRECT,
|
||||
COMPOSE_MENTION,
|
||||
COMPOSE_SUBMIT_REQUEST,
|
||||
@ -67,6 +69,8 @@ const initialState = ImmutableMap({
|
||||
caretPosition: null,
|
||||
preselectDate: null,
|
||||
in_reply_to: null,
|
||||
quote_from: null,
|
||||
quote_from_url: null,
|
||||
is_composing: false,
|
||||
is_submitting: false,
|
||||
is_changing_upload: false,
|
||||
@ -119,6 +123,8 @@ function clearAll(state) {
|
||||
map.set('is_submitting', false);
|
||||
map.set('is_changing_upload', false);
|
||||
map.set('in_reply_to', null);
|
||||
map.set('quote_from', null);
|
||||
map.set('quote_from_url', null);
|
||||
map.set('privacy', state.get('default_privacy'));
|
||||
map.set('sensitive', state.get('default_sensitive'));
|
||||
map.set('language', state.get('default_language'));
|
||||
@ -250,6 +256,17 @@ const expiresInFromExpiresAt = expires_at => {
|
||||
return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600;
|
||||
};
|
||||
|
||||
const rejectQuoteAltText = html => {
|
||||
const fragment = domParser.parseFromString(html, 'text/html').documentElement;
|
||||
|
||||
const quote_inline = fragment.querySelector('span.quote-inline');
|
||||
if (quote_inline) {
|
||||
quote_inline.remove();
|
||||
}
|
||||
|
||||
return fragment.innerHTML;
|
||||
};
|
||||
|
||||
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
|
||||
prefix = prefix.toLowerCase();
|
||||
if (suggestions.length < 4) {
|
||||
@ -323,10 +340,20 @@ export default function compose(state = initialState, action) {
|
||||
case COMPOSE_COMPOSING_CHANGE:
|
||||
return state.set('is_composing', action.value);
|
||||
case COMPOSE_REPLY:
|
||||
case COMPOSE_QUOTE:
|
||||
return state.withMutations(map => {
|
||||
map.set('id', null);
|
||||
map.set('in_reply_to', action.status.get('id'));
|
||||
map.set('text', statusToTextMentions(state, action.status));
|
||||
if (action.type === COMPOSE_REPLY) {
|
||||
map.set('in_reply_to', action.status.get('id'));
|
||||
map.set('quote_from', null);
|
||||
map.set('quote_from_url', null);
|
||||
map.set('text', statusToTextMentions(state, action.status));
|
||||
} else {
|
||||
map.set('in_reply_to', null);
|
||||
map.set('quote_from', action.status.get('id'));
|
||||
map.set('quote_from_url', action.status.get('url'));
|
||||
map.set('text', '');
|
||||
}
|
||||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
@ -358,6 +385,7 @@ export default function compose(state = initialState, action) {
|
||||
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
||||
return state.set('is_changing_upload', true);
|
||||
case COMPOSE_REPLY_CANCEL:
|
||||
case COMPOSE_QUOTE_CANCEL:
|
||||
case COMPOSE_RESET:
|
||||
case COMPOSE_SUBMIT_SUCCESS:
|
||||
return clearAll(state);
|
||||
@ -456,8 +484,10 @@ export default function compose(state = initialState, action) {
|
||||
}));
|
||||
case REDRAFT:
|
||||
return state.withMutations(map => {
|
||||
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
|
||||
map.set('text', action.raw_text || unescapeHTML(rejectQuoteAltText(expandMentions(action.status))));
|
||||
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
||||
map.set('quote_from', action.status.getIn(['quote', 'id']));
|
||||
map.set('quote_from_url', action.status.getIn(['quote', 'url']));
|
||||
map.set('privacy', action.status.get('visibility'));
|
||||
map.set('media_attachments', action.status.get('media_attachments').map((media) => media.set('unattached', true)));
|
||||
map.set('focusDate', new Date());
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
import {
|
||||
COMPOSE_MENTION,
|
||||
COMPOSE_REPLY,
|
||||
COMPOSE_QUOTE,
|
||||
COMPOSE_DIRECT,
|
||||
} from '../actions/compose';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
@ -37,6 +38,7 @@ export default function search(state = initialState, action) {
|
||||
case SEARCH_SHOW:
|
||||
return state.set('hidden', false);
|
||||
case COMPOSE_REPLY:
|
||||
case COMPOSE_QUOTE:
|
||||
case COMPOSE_MENTION:
|
||||
case COMPOSE_DIRECT:
|
||||
return state.set('hidden', true);
|
||||
|
@ -17,6 +17,8 @@ import {
|
||||
STATUS_TRANSLATE_UNDO,
|
||||
STATUS_FETCH_REQUEST,
|
||||
STATUS_FETCH_FAIL,
|
||||
QUOTE_REVEAL,
|
||||
QUOTE_HIDE,
|
||||
} from '../actions/statuses';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
|
||||
@ -83,6 +85,14 @@ export default function statuses(state = initialState, action) {
|
||||
});
|
||||
case STATUS_COLLAPSE:
|
||||
return state.setIn([action.id, 'collapsed'], action.isCollapsed);
|
||||
case QUOTE_REVEAL:
|
||||
return state.withMutations(map => {
|
||||
action.ids.forEach(id => map.setIn([id, 'quote_hidden'], false));
|
||||
});
|
||||
case QUOTE_HIDE:
|
||||
return state.withMutations(map => {
|
||||
action.ids.forEach(id => map.setIn([id, 'quote_hidden'], true));
|
||||
});
|
||||
case TIMELINE_DELETE:
|
||||
return deleteStatus(state, action.id, action.references);
|
||||
case STATUS_TRANSLATE_SUCCESS:
|
||||
|
@ -2,6 +2,7 @@ import { createSelector } from 'reselect';
|
||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
import { toServerSideType } from 'mastodon/utils/filters';
|
||||
import { me } from '../initial_state';
|
||||
import {reblogRequest} from '../actions/interactions';
|
||||
|
||||
const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
|
||||
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
|
||||
@ -35,22 +36,54 @@ export const makeGetStatus = () => {
|
||||
[
|
||||
(state, { id }) => state.getIn(['statuses', id]),
|
||||
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
|
||||
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id'])]),
|
||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id']), 'account'])]),
|
||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'quote', 'account'])]),
|
||||
(state, { id }) => state.getIn(['relationships', state.getIn(['statuses', id, 'account'])]),
|
||||
(state, { id }) => state.getIn(['relationships', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
||||
(state, { id }) => state.getIn(['relationships', state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id']), 'account'])]),
|
||||
(state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', id, 'account']), 'moved'])]),
|
||||
(state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account']), 'moved'])]),
|
||||
(state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id']), 'account']), 'moved'])]),
|
||||
getFilters,
|
||||
],
|
||||
|
||||
(statusBase, statusReblog, accountBase, accountReblog, filters) => {
|
||||
(statusBase, statusReblog, statusQuote, accountBase, accountReblog, accountQuote, accountReblogQuote, relationshipBase, relationshipReblog, relationshipQuote, movedBase, movedReblog, movedQuote, filters) => {
|
||||
if (!statusBase || statusBase.get('isLoading')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
accountBase = accountBase.withMutations(map => {
|
||||
map.set('relationship', relationshipBase);
|
||||
map.set('moved', movedBase);
|
||||
});
|
||||
|
||||
if (statusReblog) {
|
||||
accountReblog = accountReblog.withMutations(map => {
|
||||
map.set('relationship', relationshipReblog);
|
||||
map.set('moved', movedReblog);
|
||||
});
|
||||
statusReblog = statusReblog.set('account', accountReblog);
|
||||
} else {
|
||||
statusReblog = null;
|
||||
}
|
||||
|
||||
if (statusQuote) {
|
||||
accountQuote = accountQuote.withMutations(map => {
|
||||
map.set('relationship', relationshipQuote);
|
||||
map.set('moved', movedQuote);
|
||||
});
|
||||
statusQuote = statusQuote.set('account', accountQuote);
|
||||
} else {
|
||||
statusQuote = null;
|
||||
}
|
||||
|
||||
if (statusReblog && accountReblogQuote) {
|
||||
statusReblog = statusReblog.setIn(['quote', 'account'], accountReblogQuote);
|
||||
}
|
||||
|
||||
let filtered = false;
|
||||
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
||||
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
||||
@ -65,6 +98,7 @@ export const makeGetStatus = () => {
|
||||
|
||||
return statusBase.withMutations(map => {
|
||||
map.set('reblog', statusReblog);
|
||||
map.set('quote', statusQuote);
|
||||
map.set('account', accountBase);
|
||||
map.set('matched_filters', filtered);
|
||||
});
|
||||
|
1
app/javascript/mastodon/utils/uniq.js
Normal file
@ -0,0 +1 @@
|
||||
export const uniq = (array) => array.filter((x, i, self) => self.indexOf(x) === i);
|
@ -322,6 +322,29 @@ function main() {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
loadPolyfills()
|
||||
|
@ -23,3 +23,5 @@
|
||||
@import 'mastodon/dashboard';
|
||||
@import 'mastodon/rtl';
|
||||
@import 'mastodon/accessibility';
|
||||
|
||||
@import 'plugin';
|
3
app/javascript/styles/light-pink.scss
Normal file
@ -0,0 +1,3 @@
|
||||
@import 'light-pink/variables';
|
||||
@import 'application';
|
||||
@import 'mastodon-light/diff';
|
44
app/javascript/styles/light-pink/variables.scss
Normal file
@ -0,0 +1,44 @@
|
||||
// Dependent colors
|
||||
$black: #000000;
|
||||
$white: #ffffff;
|
||||
|
||||
$classic-base-color: #6e202f;
|
||||
$classic-primary-color: #ffa7ae;
|
||||
$classic-secondary-color: #faeef1;
|
||||
$classic-highlight-color: #ff375b;
|
||||
|
||||
// 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;
|
||||
|
||||
$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';
|
@ -736,10 +736,29 @@ body > [data-popper-placement] {
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
padding-top: 10px;
|
||||
|
||||
.compose-form__publish-button-wrapper {
|
||||
overflow: hidden;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -808,6 +827,10 @@ body > [data-popper-placement] {
|
||||
min-height: 23px;
|
||||
overflow-y: auto;
|
||||
flex: 0 2 auto;
|
||||
|
||||
&.quote-indicator {
|
||||
background: $success-green;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-indicator__header {
|
||||
@ -989,6 +1012,10 @@ body > [data-popper-placement] {
|
||||
|
||||
.status__content.status__content--collapsed {
|
||||
max-height: 22px * 15; // 15 lines is roughly above 500 characters
|
||||
|
||||
.quote-status & {
|
||||
max-height: 22px * 5;
|
||||
}
|
||||
}
|
||||
|
||||
.status__content__read-more-button {
|
||||
@ -1061,6 +1088,69 @@ 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: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.status__content__text {
|
||||
p {
|
||||
display: inline;
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quote-inline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.focusable {
|
||||
&:focus {
|
||||
outline: 0;
|
||||
@ -1076,9 +1166,12 @@ body > [data-popper-placement] {
|
||||
.status {
|
||||
padding: 16px;
|
||||
min-height: 54px;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
cursor: auto;
|
||||
|
||||
&:not(.quote-status) {
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
|
@ -40,7 +40,7 @@
|
||||
|
||||
&:last-child {
|
||||
.detailed-status,
|
||||
.status,
|
||||
.status:not(.quote-status),
|
||||
.load-more {
|
||||
border-bottom: 0;
|
||||
border-radius: 0 0 4px 4px;
|
||||
@ -63,9 +63,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.detailed-status .quote-status {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.quote-status {
|
||||
margin-top: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 740px) {
|
||||
.detailed-status,
|
||||
.status,
|
||||
.status:not(.quote-status),
|
||||
.load-more {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
@ -77,6 +86,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.standalone-timeline .quote-status {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button.logo-button {
|
||||
flex: 0 auto;
|
||||
font-size: 14px;
|
||||
|
45
app/javascript/styles/plugin.scss
Normal file
@ -0,0 +1,45 @@
|
||||
// ここから下タブバーの実装
|
||||
|
||||
//投稿ボタン
|
||||
.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;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 630px) {
|
||||
.tab-ber-bottom .timeline{
|
||||
width:100%;
|
||||
height: calc(100% - 55px);
|
||||
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%;
|
||||
}
|
||||
|
||||
.columns-area__panels__pane-tab-ber{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
//ここまで下タブバーの実装
|
1
app/javascript/styles/y-zu-dark
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit a58c699a605784b3318f54717bfaaa804db4d2f3
|
1
app/javascript/styles/y-zu-light
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit a5c22831445f5669315434f7df49c3c1e1f70d20
|
@ -76,6 +76,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
@silenced_account_ids = []
|
||||
@params = {}
|
||||
|
||||
process_quote
|
||||
process_status_params
|
||||
process_tags
|
||||
process_audience
|
||||
@ -83,8 +84,16 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
ApplicationRecord.transaction do
|
||||
@status = Status.create!(@params)
|
||||
attach_tags(@status)
|
||||
|
||||
# Delete status on zero follower user and nearly created account with include some replies
|
||||
if like_a_spam?
|
||||
@status = nil
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
|
||||
return if @status.nil?
|
||||
|
||||
resolve_thread(@status)
|
||||
fetch_replies(@status)
|
||||
distribute
|
||||
@ -126,6 +135,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
conversation: conversation_from_uri(@object['conversation']),
|
||||
media_attachment_ids: process_attachments.take(4).map(&:id),
|
||||
poll: process_poll,
|
||||
quote: @quote,
|
||||
}
|
||||
end
|
||||
end
|
||||
@ -426,4 +436,33 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
poll.reload
|
||||
retry
|
||||
end
|
||||
|
||||
def process_quote
|
||||
if (@quote = quote_from_url(@object['quoteUrl']))
|
||||
/<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(/<\/span>/, '')}</span>")
|
||||
|
||||
end
|
||||
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
|
||||
|
||||
SPAM_FILTER_MINIMUM_FOLLOWERS = ENV.fetch('SPAM_FILTER_MINIMUM_FOLLOWERS', 0).to_i
|
||||
SPAM_FILTER_MINIMUM_CREATE_DAYS = ENV.fetch('SPAM_FILTER_MINIMUM_CREATE_DAYS', 1).to_i
|
||||
SPAM_FILTER_MINIMUM_MENTIONS = ENV.fetch('SPAM_FILTER_MINIMUM_MENTIONS', 1).to_i
|
||||
def like_a_spam?
|
||||
(
|
||||
!@status.account.local? &&
|
||||
@status.account.followers_count <= SPAM_FILTER_MINIMUM_FOLLOWERS &&
|
||||
@status.account.created_at > SPAM_FILTER_MINIMUM_CREATE_DAYS.day.ago &&
|
||||
@mentions.count > SPAM_FILTER_MINIMUM_MENTIONS
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -14,6 +14,8 @@ module ActivityPub::CaseTransform
|
||||
when String
|
||||
camel_lower_cache[value] ||= if value.start_with?('_:')
|
||||
'_:' + value.gsub(/\A_:/, '').underscore.camelize(:lower)
|
||||
elsif value.start_with?('_')
|
||||
value
|
||||
else
|
||||
value.underscore.camelize(:lower)
|
||||
end
|
||||
|
@ -13,7 +13,8 @@ class TextFormatter
|
||||
multiline: true,
|
||||
}.freeze
|
||||
|
||||
attr_reader :text, :options
|
||||
attr_accessor :text
|
||||
attr_reader :options
|
||||
|
||||
# @param [String] text
|
||||
# @param [Hash] options
|
||||
@ -21,6 +22,7 @@ class TextFormatter
|
||||
# @option options [Boolean] :with_domains
|
||||
# @option options [Boolean] :with_rel_me
|
||||
# @option options [Array<Account>] :preloaded_accounts
|
||||
# @option options [Status] :quote
|
||||
def initialize(text, options = {})
|
||||
@text = text
|
||||
@options = DEFAULT_OPTIONS.merge(options)
|
||||
@ -31,7 +33,7 @@ class TextFormatter
|
||||
end
|
||||
|
||||
def to_s
|
||||
return ''.html_safe if text.blank?
|
||||
return ''.html_safe if text.blank? & !quote?
|
||||
|
||||
html = rewrite do |entity|
|
||||
if entity[:url]
|
||||
@ -43,6 +45,8 @@ class TextFormatter
|
||||
end
|
||||
end
|
||||
|
||||
html += render_quote if quote?
|
||||
|
||||
html = simple_format(html, {}, sanitize: false).delete("\n") if multiline?
|
||||
|
||||
html.html_safe # rubocop:disable Rails/OutputSafety
|
||||
@ -126,7 +130,7 @@ class TextFormatter
|
||||
|
||||
return "@#{h(entity[:screen_name])}" if account.nil?
|
||||
|
||||
url = ActivityPub::TagManager.instance.url_for(account)
|
||||
url = ap_tag_manager.url_for(account)
|
||||
display_username = same_username_hits&.positive? || with_domains? ? account.pretty_acct : account.username
|
||||
|
||||
<<~HTML.squish
|
||||
@ -134,6 +138,13 @@ class TextFormatter
|
||||
HTML
|
||||
end
|
||||
|
||||
def render_quote
|
||||
link = link_to_url({ url: ap_tag_manager.url_for(quote) })
|
||||
<<~HTML.squish
|
||||
<span class="quote-inline"><br/>~~~~~~~~~~<br/>[#{link}]</span>
|
||||
HTML
|
||||
end
|
||||
|
||||
def entity_cache
|
||||
@entity_cache ||= EntityCache.instance
|
||||
end
|
||||
@ -142,6 +153,10 @@ class TextFormatter
|
||||
@tag_manager ||= TagManager.instance
|
||||
end
|
||||
|
||||
def ap_tag_manager
|
||||
@ap_tag_manager ||= ActivityPub::TagManager.instance
|
||||
end
|
||||
|
||||
delegate :local_domain?, to: :tag_manager
|
||||
|
||||
def multiline?
|
||||
@ -163,4 +178,12 @@ class TextFormatter
|
||||
def preloaded_accounts?
|
||||
preloaded_accounts.present?
|
||||
end
|
||||
|
||||
def quote
|
||||
options[:quote]
|
||||
end
|
||||
|
||||
def quote?
|
||||
quote.present?
|
||||
end
|
||||
end
|
||||
|
@ -39,6 +39,7 @@ class UserSettingsDecorator
|
||||
user.settings['trends'] = trends_preference if change?('setting_trends')
|
||||
user.settings['crop_images'] = crop_images_preference if change?('setting_crop_images')
|
||||
user.settings['always_send_emails'] = always_send_emails_preference if change?('setting_always_send_emails')
|
||||
user.settings['place_tab_bar_at_bottom'] = place_tab_bar_at_bottom_preference if change?('setting_place_tab_bar_at_bottom')
|
||||
end
|
||||
|
||||
def merged_notification_emails
|
||||
@ -137,6 +138,10 @@ class UserSettingsDecorator
|
||||
boolean_cast_setting 'setting_always_send_emails'
|
||||
end
|
||||
|
||||
def place_tab_bar_at_bottom_preference
|
||||
boolean_cast_setting 'setting_place_tab_bar_at_bottom'
|
||||
end
|
||||
|
||||
def boolean_cast_setting(key)
|
||||
ActiveModel::Type::Boolean.new.cast(settings[key])
|
||||
end
|
||||
|
@ -21,6 +21,7 @@
|
||||
# account_id :bigint(8) not null
|
||||
# application_id :bigint(8)
|
||||
# in_reply_to_account_id :bigint(8)
|
||||
# quote_id :bigint(8)
|
||||
# poll_id :bigint(8)
|
||||
# deleted_at :datetime
|
||||
# edited_at :datetime
|
||||
@ -59,6 +60,7 @@ class Status < ApplicationRecord
|
||||
|
||||
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
|
||||
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
|
||||
belongs_to :quote, foreign_key: 'quote_id', class_name: 'Status', inverse_of: :quoted, optional: true
|
||||
|
||||
has_many :favourites, inverse_of: :status, dependent: :destroy
|
||||
has_many :bookmarks, inverse_of: :status, dependent: :destroy
|
||||
@ -69,6 +71,7 @@ class Status < ApplicationRecord
|
||||
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
|
||||
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
|
||||
has_many :media_attachments, dependent: :nullify
|
||||
has_many :quoted, foreign_key: 'quote_id', class_name: 'Status', inverse_of: :quote, dependent: :nullify
|
||||
|
||||
has_and_belongs_to_many :tags
|
||||
has_and_belongs_to_many :preview_cards
|
||||
@ -84,6 +87,7 @@ class Status < ApplicationRecord
|
||||
validates_with DisallowedHashtagsValidator
|
||||
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
|
||||
validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
|
||||
validates :quote_visibility, inclusion: { in: %w(public unlisted) }, if: :quote?
|
||||
|
||||
accepts_nested_attributes_for :poll
|
||||
|
||||
@ -191,6 +195,14 @@ class Status < ApplicationRecord
|
||||
!reblog_of_id.nil?
|
||||
end
|
||||
|
||||
def quote?
|
||||
!quote_id.nil? && quote
|
||||
end
|
||||
|
||||
def quote_visibility
|
||||
quote&.visibility
|
||||
end
|
||||
|
||||
def within_realtime_window?
|
||||
created_at >= REAL_TIME_WINDOW.ago
|
||||
end
|
||||
@ -255,7 +267,12 @@ class Status < ApplicationRecord
|
||||
fields = [spoiler_text, text]
|
||||
fields += preloadable_poll.options unless preloadable_poll.nil?
|
||||
|
||||
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain)
|
||||
quote_fields = []
|
||||
quote_fields += [quote.spoiler_text, quote.text] if quote?
|
||||
quote_fields += quote.preloadable_poll.options unless quote&.preloadable_poll.nil?
|
||||
|
||||
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain) +
|
||||
CustomEmoji.from_text(quote_fields.join(' '), quote&.account&.domain)
|
||||
end
|
||||
|
||||
def ordered_media_attachments
|
||||
|
@ -139,6 +139,7 @@ class User < ApplicationRecord
|
||||
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
|
||||
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images,
|
||||
:disable_swiping, :always_send_emails,
|
||||
:place_tab_bar_at_bottom,
|
||||
to: :settings, prefix: :setting, allow_nil: false
|
||||
|
||||
delegate :can?, to: :role
|
||||
|
@ -11,6 +11,9 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
:atom_uri, :in_reply_to_atom_uri,
|
||||
:conversation
|
||||
|
||||
attribute :quote_url, if: -> { object.quote? }
|
||||
attribute :misskey_quote, key: :_misskey_quote, if: -> { object.quote? }
|
||||
attribute :misskey_content, key: :_misskey_content, if: -> { object.quote? }
|
||||
attribute :content
|
||||
attribute :content_map, if: :language?
|
||||
attribute :updated, if: :edited?
|
||||
@ -138,6 +141,16 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
end
|
||||
end
|
||||
|
||||
def quote_url
|
||||
ActivityPub::TagManager.instance.uri_for(object.quote) if object.quote?
|
||||
end
|
||||
|
||||
alias misskey_quote quote_url
|
||||
|
||||
def misskey_content
|
||||
object.text if object.quote?
|
||||
end
|
||||
|
||||
def local?
|
||||
object.account.local?
|
||||
end
|
||||
|
@ -51,6 +51,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||
store[:use_pending_items] = object.current_account.user.setting_use_pending_items
|
||||
store[:trends] = Setting.trends && object.current_account.user.setting_trends
|
||||
store[:crop_images] = object.current_account.user.setting_crop_images
|
||||
store[:place_tab_bar_at_bottom] = object.current_account.user.setting_place_tab_bar_at_bottom
|
||||
else
|
||||
store[:auto_play_gif] = Setting.auto_play_gif
|
||||
store[:display_media] = Setting.display_media
|
||||
|
@ -11,7 +11,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||
|
||||
attributes :domain, :title, :version, :source_url, :description,
|
||||
:usage, :thumbnail, :languages, :configuration,
|
||||
:registrations
|
||||
:registrations, :feature_quote
|
||||
|
||||
has_one :contact, serializer: ContactSerializer
|
||||
has_many :rules, serializer: REST::RuleSerializer
|
||||
@ -88,6 +88,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||
}
|
||||
end
|
||||
|
||||
def feature_quote
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def registrations_enabled?
|
||||
|
@ -18,6 +18,8 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
attribute :content, unless: :source_requested?
|
||||
attribute :text, if: :source_requested?
|
||||
|
||||
attribute :quote_id, if: -> { object.quote? }
|
||||
|
||||
belongs_to :reblog, serializer: REST::StatusSerializer
|
||||
belongs_to :application, if: :show_application?
|
||||
belongs_to :account, serializer: REST::AccountSerializer
|
||||
@ -42,6 +44,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
object.in_reply_to_account_id&.to_s
|
||||
end
|
||||
|
||||
def quote_id
|
||||
object.quote_id.to_s
|
||||
end
|
||||
|
||||
def current_user?
|
||||
!current_user.nil?
|
||||
end
|
||||
@ -182,3 +188,26 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class REST::NestedQuoteSerializer < REST::StatusSerializer
|
||||
attribute :quote do
|
||||
nil
|
||||
end
|
||||
attribute :quote_muted, if: :current_user?
|
||||
|
||||
def quote_muted
|
||||
if instance_options && instance_options[:account_relationships]
|
||||
!!instance_options[:account_relationships].muting[object.account_id] ||
|
||||
instance_options[:account_relationships].blocking[object.account_id] ||
|
||||
instance_options[:account_relationships].blocked_by[object.account_id] ||
|
||||
instance_options[:account_relationships].domain_blocking[object.account_id]
|
||||
else
|
||||
current_user.account.muting?(object.account) || object.account.blocking?(current_user.account) || current_user.account.blocking?(object.account) || current_user.account.domain_blocking?(object.account.domain)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class REST::StatusSerializer < ActiveModel::Serializer
|
||||
belongs_to :quote, serializer: REST::NestedQuoteSerializer
|
||||
end
|
||||
|
@ -77,7 +77,7 @@ class FetchLinkCardService < BaseService
|
||||
@status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
|
||||
else
|
||||
document = Nokogiri::HTML(@status.text)
|
||||
links = document.css('a')
|
||||
links = document.css(':not(.quote-inline) > a')
|
||||
|
||||
links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize)
|
||||
end
|
||||
|
@ -21,17 +21,20 @@ class PostStatusService < BaseService
|
||||
# @option [Doorkeeper::Application] :application
|
||||
# @option [String] :idempotency Optional idempotency key
|
||||
# @option [Boolean] :with_rate_limit
|
||||
# @option [Integer] :quote_id
|
||||
# @return [Status]
|
||||
def call(account, options = {})
|
||||
@account = account
|
||||
@options = options
|
||||
@text = @options[:text] || ''
|
||||
@in_reply_to = @options[:thread]
|
||||
@quote_id = @options[:quote_id]
|
||||
|
||||
return idempotency_duplicate if idempotency_given? && idempotency_duplicate?
|
||||
|
||||
validate_media!
|
||||
preprocess_attributes!
|
||||
preprocess_quote!
|
||||
|
||||
if scheduled?
|
||||
schedule_status!
|
||||
@ -51,6 +54,19 @@ class PostStatusService < BaseService
|
||||
|
||||
private
|
||||
|
||||
def status_from_uri(uri)
|
||||
ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
|
||||
end
|
||||
|
||||
def quote_from_url(url)
|
||||
return nil if url.nil?
|
||||
|
||||
quote = ResolveURLService.new.call(url)
|
||||
status_from_uri(quote.uri) if quote
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
def preprocess_attributes!
|
||||
@sensitive = (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?
|
||||
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
|
||||
@ -58,10 +74,21 @@ class PostStatusService < BaseService
|
||||
@visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced?
|
||||
@scheduled_at = @options[:scheduled_at]&.to_datetime
|
||||
@scheduled_at = nil if scheduled_in_the_past?
|
||||
|
||||
if @quote_id.nil? && (link = @text&.match(/\n\[?(#{FetchLinkCardService::URL_PATTERN})\]?$/))
|
||||
@text = @text&.lines(chomp: true)[0...-1].join("\n") if (@quote_id = quote_from_url(link[1])&.id)
|
||||
end
|
||||
rescue ArgumentError
|
||||
raise ActiveRecord::RecordInvalid
|
||||
end
|
||||
|
||||
def preprocess_quote!
|
||||
if @quote_id.present?
|
||||
quote = Status.find(@quote_id)
|
||||
@quote_id = quote.reblog_of_id.to_s if quote.reblog?
|
||||
end
|
||||
end
|
||||
|
||||
def process_status!
|
||||
# The following transaction block is needed to wrap the UPDATEs to
|
||||
# the media attachments when the status is created
|
||||
@ -171,6 +198,7 @@ class PostStatusService < BaseService
|
||||
language: valid_locale_cascade(@options[:language], @account.user&.preferred_posting_language, I18n.default_locale),
|
||||
application: @options[:application],
|
||||
rate_limit: @options[:with_rate_limit],
|
||||
quote_id: @quote_id,
|
||||
}.compact
|
||||
end
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class StatusLengthValidator < ActiveModel::Validator
|
||||
MAX_CHARS = 500
|
||||
MAX_CHARS = 2048
|
||||
URL_PLACEHOLDER_CHARS = 23
|
||||
URL_PLACEHOLDER = 'x' * 23
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
%html{ lang: I18n.locale }
|
||||
%head
|
||||
%meta{ charset: 'utf-8' }/
|
||||
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' }/
|
||||
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, viewport-fit=cover'}/
|
||||
|
||||
- if cdn_host?
|
||||
%link{ rel: 'dns-prefetch', href: cdn_host }/
|
||||
|
@ -32,6 +32,7 @@
|
||||
= f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_disable_swiping, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_place_tab_bar_at_bottom, as: :boolean, wrapper: :with_label
|
||||
|
||||
%h4= t 'appearance.toot_layout'
|
||||
|
||||
|
20
app/views/statuses/_author.html.haml
Normal 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)
|
||||
- unless inline
|
||||
= fa_icon('lock') if author.locked?
|
@ -1,40 +1,14 @@
|
||||
.detailed-status.detailed-status--flex{ class: "detailed-status-#{status.visibility}" }
|
||||
.p-author.h-card
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'detailed-status__display-name u-url', target: stream_link_target, rel: 'noopener' do
|
||||
.detailed-status__display-avatar
|
||||
- if prefers_autoplay?
|
||||
= image_tag status.account.avatar_original_url, alt: '', class: 'account__avatar u-photo'
|
||||
- else
|
||||
= image_tag status.account.avatar_static_url, alt: '', class: 'account__avatar u-photo'
|
||||
%span.display-name
|
||||
%bdi
|
||||
%strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: prefers_autoplay?)
|
||||
%span.display-name__account
|
||||
= acct(status.account)
|
||||
= fa_icon('lock') if status.account.locked?
|
||||
= render 'statuses/author', author: status.account, detailed: true
|
||||
|
||||
= account_action_button(status.account)
|
||||
|
||||
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
|
||||
- if status.spoiler_text?
|
||||
%p<
|
||||
%span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}
|
||||
%button.status__content__spoiler-link= t('statuses.show_more')
|
||||
.e-content{ lang: status.language }
|
||||
= prerender_custom_emojis(status_content_format(status), status.emojis)
|
||||
= render 'statuses/text', status: status
|
||||
|
||||
- if status.preloadable_poll
|
||||
= render_poll_component(status)
|
||||
- if status.quote?
|
||||
= render 'statuses/quote', status: status.quote
|
||||
|
||||
- if !status.ordered_media_attachments.empty?
|
||||
- if status.ordered_media_attachments.first.video?
|
||||
= render_video_component(status, width: 670, height: 380, detailed: true)
|
||||
- elsif status.ordered_media_attachments.first.audio?
|
||||
= render_audio_component(status, width: 670, height: 380)
|
||||
- else
|
||||
= render_media_gallery_component(status, height: 380, standalone: true)
|
||||
- elsif status.preview_card
|
||||
= render_card_component(status)
|
||||
= render 'statuses/media', status: status, detailed: true
|
||||
|
||||
.detailed-status__meta
|
||||
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
||||
|
13
app/views/statuses/_media.html.haml
Normal 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)
|
6
app/views/statuses/_quote.html.haml
Normal 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
|
@ -12,41 +12,14 @@
|
||||
*
|
||||
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
||||
|
||||
.p-author.h-card
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||
.status__avatar
|
||||
%div
|
||||
- if prefers_autoplay?
|
||||
= image_tag status.account.avatar_original_url, alt: '', class: 'u-photo account__avatar'
|
||||
- else
|
||||
= image_tag status.account.avatar_static_url, alt: '', class: 'u-photo account__avatar'
|
||||
%span.display-name
|
||||
%bdi
|
||||
%strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: prefers_autoplay?)
|
||||
= ' '
|
||||
%span.display-name__account
|
||||
= acct(status.account)
|
||||
= fa_icon('lock') if status.account.locked?
|
||||
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
|
||||
- if status.spoiler_text?
|
||||
%p<
|
||||
%span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}
|
||||
%button.status__content__spoiler-link= t('statuses.show_more')
|
||||
.e-content{ lang: status.language }
|
||||
= prerender_custom_emojis(status_content_format(status), status.emojis)
|
||||
= render 'statuses/author', author: status.account
|
||||
|
||||
- if status.preloadable_poll
|
||||
= render_poll_component(status)
|
||||
= render 'statuses/text', status: status
|
||||
|
||||
- if !status.ordered_media_attachments.empty?
|
||||
- if status.ordered_media_attachments.first.video?
|
||||
= render_video_component(status, width: 610, height: 343)
|
||||
- elsif status.ordered_media_attachments.first.audio?
|
||||
= render_audio_component(status, width: 610, height: 343)
|
||||
- else
|
||||
= render_media_gallery_component(status, height: 343)
|
||||
- elsif status.preview_card
|
||||
= render_card_component(status)
|
||||
- if status.quote?
|
||||
= render 'statuses/quote', status: status.quote
|
||||
|
||||
= render 'statuses/media', status: status
|
||||
|
||||
- if !status.in_reply_to_id.nil? && status.in_reply_to_account_id == status.account.id && !hide_show_thread
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__content__read-more-button', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||
|
9
app/views/statuses/_text.html.haml
Normal 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)}
|
||||
%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)
|
@ -138,7 +138,7 @@ ja:
|
||||
push: プッシュ通知
|
||||
reports: 通報
|
||||
search: 検索
|
||||
statuses: 投稿
|
||||
statuses: トゥート
|
||||
layouts:
|
||||
admin:
|
||||
nav:
|
||||
@ -178,13 +178,13 @@ ja:
|
||||
read:notifications: 通知の読み取り
|
||||
read:reports: 通報の読み取り
|
||||
read:search: あなたの代わりに検索
|
||||
read:statuses: すべての投稿の読み取り
|
||||
read:statuses: すべてのトゥートの読み取り
|
||||
write: アカウントのすべてのデータの変更
|
||||
write:accounts: プロフィールの変更
|
||||
write:blocks: ユーザーのブロックやドメインの非表示
|
||||
write:bookmarks: 投稿のブックマーク登録
|
||||
write:bookmarks: トゥートのブックマーク登録
|
||||
write:conversations: 会話のミュートと削除
|
||||
write:favourites: 投稿のお気に入り登録
|
||||
write:favourites: トゥートのお気に入り登録
|
||||
write:filters: フィルターの変更
|
||||
write:follows: あなたの代わりにフォロー、アンフォロー
|
||||
write:lists: リストの変更
|
||||
@ -192,4 +192,4 @@ ja:
|
||||
write:mutes: アカウントや会話のミュート
|
||||
write:notifications: 通知の消去
|
||||
write:reports: 通報の作成
|
||||
write:statuses: 投稿の送信
|
||||
write:statuses: トゥートの送信
|
||||
|
@ -18,8 +18,8 @@ ja:
|
||||
pin_errors:
|
||||
following: おすすめしたい人はあなたが既にフォローしている必要があります
|
||||
posts:
|
||||
other: 投稿
|
||||
posts_tab_heading: 投稿
|
||||
other: トゥート
|
||||
posts_tab_heading: トゥート
|
||||
admin:
|
||||
account_actions:
|
||||
action: アクションを実行
|
||||
@ -141,7 +141,7 @@ ja:
|
||||
targeted_reports: このアカウントについての通報
|
||||
silence: サイレンス
|
||||
silenced: サイレンス済み
|
||||
statuses: 投稿数
|
||||
statuses: トゥート数
|
||||
strikes: 前回のストライク
|
||||
subscribe: 購読する
|
||||
suspend: サスペンド
|
||||
@ -190,7 +190,7 @@ ja:
|
||||
destroy_email_domain_block: メールドメインブロックを削除
|
||||
destroy_instance: ドメインをブロックする
|
||||
destroy_ip_block: IPルールを削除
|
||||
destroy_status: 投稿を削除
|
||||
destroy_status: トゥートを削除
|
||||
destroy_unavailable_domain: 配送できないドメインを削除
|
||||
destroy_user_role: ロールを削除
|
||||
disable_2fa_user: 二要素認証を無効化
|
||||
@ -221,7 +221,7 @@ ja:
|
||||
update_custom_emoji: カスタム絵文字を更新
|
||||
update_domain_block: ドメインブロックを更新
|
||||
update_ip_block: IPルールを更新
|
||||
update_status: 投稿を更新
|
||||
update_status: トゥートを更新
|
||||
update_user_role: ロールを更新
|
||||
actions:
|
||||
approve_appeal_html: "%{name}さんが%{target}さんからの抗議を承認しました"
|
||||
@ -249,7 +249,7 @@ ja:
|
||||
destroy_email_domain_block_html: "%{name}さんが%{target}をメールドメインブロックから外しました"
|
||||
destroy_instance_html: "%{name}さんがドメイン %{target}をブロックしました"
|
||||
destroy_ip_block_html: "%{name}さんが IP %{target}のルールを削除しました"
|
||||
destroy_status_html: "%{name}さんが%{target}さんの投稿を削除しました"
|
||||
destroy_status_html: "%{name}さんが%{target}さんのトゥートを削除しました"
|
||||
destroy_unavailable_domain_html: "%{name}がドメイン %{target}への配送を再開しました"
|
||||
destroy_user_role_html: "%{name}さんがロール『%{target}』を削除しました"
|
||||
disable_2fa_user_html: "%{name}さんが%{target}さんの二要素認証を無効化しました"
|
||||
@ -280,7 +280,7 @@ ja:
|
||||
update_custom_emoji_html: "%{name}さんがカスタム絵文字 %{target}を更新しました"
|
||||
update_domain_block_html: "%{name}さんが%{target}のドメインブロックを更新しました"
|
||||
update_ip_block_html: "%{name} さんがIP %{target} のルールを更新しました"
|
||||
update_status_html: "%{name}さんが%{target}さんの投稿を更新しました"
|
||||
update_status_html: "%{name}さんが%{target}さんのトゥートを更新しました"
|
||||
update_user_role_html: "%{name}さんがロール『%{target}』を変更しました"
|
||||
deleted_account: 削除されたアカウント
|
||||
empty: ログが見つかりませんでした
|
||||
@ -385,7 +385,7 @@ ja:
|
||||
create: ブロックを作成
|
||||
hint: ドメインブロックはデータベース中のアカウント項目の作成を妨げませんが、遡って自動的に指定されたモデレーションをそれらのアカウントに適用します。
|
||||
severity:
|
||||
desc_html: "<strong>制限</strong>は、このドメイン上のアカウントからの投稿が、相手をフォローしている場合を除き非表示になります。<strong>停止</strong>は、このドメイン上のすべてのコンテンツ、メディア、およびプロフィールデータを受け付けなくなります。メディアファイルのみを拒否したい場合は<strong>なし</strong>を選択します。"
|
||||
desc_html: "<strong>制限</strong>は、このドメイン上のアカウントからのトゥートが、相手をフォローしている場合を除き非表示になります。<strong>停止</strong>は、このドメイン上のすべてのコンテンツ、メディア、およびプロフィールデータを受け付けなくなります。メディアファイルのみを拒否したい場合は<strong>なし</strong>を選択します。"
|
||||
noop: なし
|
||||
silence: 制限
|
||||
suspend: 停止
|
||||
@ -479,7 +479,7 @@ ja:
|
||||
instance_languages_dimension: 人気の言語
|
||||
instance_media_attachments_measure: 保存されたメディア
|
||||
instance_reports_measure: 通報
|
||||
instance_statuses_measure: 保存された投稿
|
||||
instance_statuses_measure: 保存されたトゥート
|
||||
delivery:
|
||||
all: すべて
|
||||
clear: 配送エラーをクリア
|
||||
@ -537,11 +537,11 @@ ja:
|
||||
relays:
|
||||
add_new: リレーを追加
|
||||
delete: 削除
|
||||
description_html: "<strong>連合リレー</strong>とは、登録しているサーバー間の公開投稿を仲介するサーバーです。<strong>中小規模のサーバーが連合のコンテンツを見つけるのを助けます。</strong>これを使用しない場合、ローカルユーザーがリモートユーザーを手動でフォローする必要があります。"
|
||||
description_html: "<strong>連合リレー</strong>とは、登録しているサーバー間の公開トゥートを仲介するサーバーです。<strong>中小規模のサーバーが連合のコンテンツを見つけるのを助けます。</strong>これを使用しない場合、ローカルユーザーがリモートユーザーを手動でフォローする必要があります。"
|
||||
disable: 無効化
|
||||
disabled: 無効
|
||||
enable: 有効化
|
||||
enable_hint: 有効にすると、リレーから全ての公開投稿を受信するようになり、またこのサーバーの全ての公開投稿をリレーに送信するようになります。
|
||||
enable_hint: 有効にすると、リレーから全ての公開トゥートを受信するようになり、またこのサーバーの全ての公開トゥートをリレーに送信するようになります。
|
||||
enabled: 有効
|
||||
inbox_url: リレーURL
|
||||
pending: リレーサーバーの承認待ちです
|
||||
@ -560,8 +560,8 @@ ja:
|
||||
action_log: 監査ログ
|
||||
action_taken_by: 通報処理者
|
||||
actions:
|
||||
delete_description_html: 報告された投稿は削除され、ストライクが記録されます。同じアカウントによる今後の違反行為のエスカレーションに役立てられます。
|
||||
mark_as_sensitive_description_html: 報告された投稿のメディアは閲覧注意となり、ストライクが記録され、同じアカウントによる今後の違反行為のエスカレーションに役立てられます。
|
||||
delete_description_html: 報告されたトゥートは削除され、ストライクが記録されます。同じアカウントによる今後の違反行為のエスカレーションに役立てられます。
|
||||
mark_as_sensitive_description_html: 報告されたトゥートのメディアは閲覧注意となり、ストライクが記録され、同じアカウントによる今後の違反行為のエスカレーションに役立てられます。
|
||||
other_description_html: アカウントの動作を制御するためのオプションや、報告されたアカウントへの通信をカスタマイズするためのオプションを確認してください。
|
||||
resolve_description_html: 報告されたアカウントに対していかなる措置も取られず、ストライクも記録されず、報告は終了します。
|
||||
silence_description_html: このアカウントは、すでにフォローしている人、または手動で検索した人にしか見えないため、リーチが極端に制限されます。いつでも元に戻すことができます。このアカウントに対するすべての通報をクローズします。
|
||||
@ -580,7 +580,7 @@ ja:
|
||||
comment_description_html: "%{name}からの詳細情報:"
|
||||
confirm_action: "@%{acct} さんに対するアクション"
|
||||
created_at: 通報日時
|
||||
delete_and_resolve: 投稿を削除
|
||||
delete_and_resolve: トゥートを削除
|
||||
forwarded: 転送済み
|
||||
forwarded_to: "%{domain}に転送されました"
|
||||
mark_as_resolved: 解決済みとしてマーク
|
||||
@ -607,18 +607,18 @@ ja:
|
||||
skip_to_actions: アクションに移動
|
||||
status: ステータス
|
||||
statuses: 通報内容
|
||||
statuses_description_html: 問題の投稿は通報されたアカウントへの連絡時に引用されます
|
||||
statuses_description_html: 問題のトゥートは通報されたアカウントへの連絡時に引用されます
|
||||
summary:
|
||||
action_preambles:
|
||||
delete_html: "<strong>@%{acct}</strong>さんの投稿を<strong>削除</strong>します。この操作は:"
|
||||
mark_as_sensitive_html: "<strong>@%{acct}</strong>さんの投稿を<strong>閲覧注意</strong>として<strong>マーク</strong>します。この操作は:"
|
||||
delete_html: "<strong>@%{acct}</strong>さんのトゥートを<strong>削除</strong>します。この操作は:"
|
||||
mark_as_sensitive_html: "<strong>@%{acct}</strong>さんのトゥートを<strong>閲覧注意</strong>として<strong>マーク</strong>します。この操作は:"
|
||||
silence_html: "<strong>@%{acct}</strong>さんのアカウントを<strong>制限</strong>します。この操作は:"
|
||||
suspend_html: "<strong>@%{acct}</strong>さんのアカウントを<strong>停止</strong>します。この操作は:"
|
||||
actions:
|
||||
delete_html: 当該の投稿を削除します
|
||||
mark_as_sensitive_html: 当該の投稿に含まれるメディアを閲覧注意にします
|
||||
delete_html: 当該のトゥートを削除します
|
||||
mark_as_sensitive_html: 当該のトゥートに含まれるメディアを閲覧注意にします
|
||||
silence_html: プロフィールとコンテンツを、すでにフォローしている人や、意図的にプロフィールにアクセスする人にのみ表示することで、<strong>@%{acct}</strong>さんのリーチを厳しく制限します
|
||||
suspend_html: "<strong>@%{acct}</strong>さんのアカウントが凍結され、プロフィールとコンテンツへのアクセス、および投稿ができなくなります"
|
||||
suspend_html: "<strong>@%{acct}</strong>さんのアカウントが凍結され、プロフィールとコンテンツへのアクセス、およびトゥートができなくなります"
|
||||
close_report: '通報 #%{id} を解決済みにします'
|
||||
close_reports_html: "<strong>@%{acct}</strong>さんに対する<strong>すべての</strong>通報を解決済みにします"
|
||||
delete_data_html: 停止が解除されないまま30日経過すると、<strong>@%{acct}</strong>さんのプロフィールとコンテンツは削除されます
|
||||
@ -759,19 +759,19 @@ ja:
|
||||
title: メディア
|
||||
metadata: メタデータ
|
||||
no_status_selected: 何も選択されていないため、変更されていません
|
||||
open: 投稿を開く
|
||||
original_status: オリジナルの投稿
|
||||
open: トゥートを開く
|
||||
original_status: オリジナルのトゥート
|
||||
reblogs: ブースト
|
||||
status_changed: 投稿を変更しました
|
||||
title: 投稿一覧
|
||||
status_changed: トゥートを変更しました
|
||||
title: トゥート一覧
|
||||
trending: トレンド
|
||||
visibility: 公開範囲
|
||||
with_media: メディアあり
|
||||
strikes:
|
||||
actions:
|
||||
delete_statuses: "%{name}さんが%{target}さんの投稿を削除しました"
|
||||
delete_statuses: "%{name}さんが%{target}さんのトゥートを削除しました"
|
||||
disable: "%{name}さんが%{target}さんを凍結しました"
|
||||
mark_statuses_as_sensitive: "%{name}さんが%{target}さんの投稿を閲覧注意としてマークしました"
|
||||
mark_statuses_as_sensitive: "%{name}さんが%{target}さんのトゥートを閲覧注意としてマークしました"
|
||||
none: "%{name}さんが%{target}さんに警告を送信しました"
|
||||
sensitive: "%{name}さんが%{target}さんのアカウントを閲覧注意としてマークしました"
|
||||
silence: "%{name}さんが%{target}さんを制限しました"
|
||||
@ -822,15 +822,15 @@ ja:
|
||||
rejected: 拒否
|
||||
statuses:
|
||||
allow: 掲載を許可
|
||||
allow_account: 投稿者を許可
|
||||
description_html: これらは、このサーバーが知っている、たくさんシェアされ、お気に入り登録されている投稿です。新しいユーザーや久しぶりにアクセスするユーザーがフォローする人を探すのに役立ちます。あなたが投稿者を承認し、投稿者が許可するまで、表示されることはありません。また、個別の投稿を許可または拒否することもできます。
|
||||
allow_account: トゥート者を許可
|
||||
description_html: これらは、このサーバーが知っている、たくさんシェアされ、お気に入り登録されているトゥートです。新しいユーザーや久しぶりにアクセスするユーザーがフォローする人を探すのに役立ちます。あなたがトゥート者を承認し、トゥート者が許可するまで、表示されることはありません。また、個別のトゥートを許可または拒否することもできます。
|
||||
disallow: 掲載を拒否
|
||||
disallow_account: 投稿者を拒否
|
||||
disallow_account: トゥート者を拒否
|
||||
no_status_selected: 何も選択されていないため、変更されていません
|
||||
not_discoverable: 投稿者は発見可能であることに同意していません
|
||||
not_discoverable: トゥート者は発見可能であることに同意していません
|
||||
shared_by:
|
||||
other: "%{friendly_count} 回の共有、お気に入り"
|
||||
title: トレンド投稿
|
||||
title: トレンドトゥート
|
||||
tags:
|
||||
current_score: 現在のスコア %{score}
|
||||
dashboard:
|
||||
@ -839,7 +839,7 @@ ja:
|
||||
tag_servers_dimension: 人気のサーバー
|
||||
tag_servers_measure: その他のサーバー
|
||||
tag_uses_measure: 合計利用数
|
||||
description_html: これらは、多くの投稿に使用されているハッシュタグです。あなたのユーザーが、人々が今一番話題にしていることを知るのに役立ちます。あなたが承認するまで、ハッシュタグは一般に表示されません。
|
||||
description_html: これらは、多くのトゥートに使用されているハッシュタグです。あなたのユーザーが、人々が今一番話題にしていることを知るのに役立ちます。あなたが承認するまで、ハッシュタグは一般に表示されません。
|
||||
listable: おすすめに表示する
|
||||
no_tag_selected: 何も選択されていないため、変更されていません
|
||||
not_listable: おすすめに表示しない
|
||||
@ -877,13 +877,13 @@ ja:
|
||||
new: 新しいwebhook
|
||||
rotate_secret: シークレットをローテートする
|
||||
secret: シークレットに署名
|
||||
status: 投稿
|
||||
status: トゥート
|
||||
title: Webhooks
|
||||
webhook: Webhook
|
||||
admin_mailer:
|
||||
new_appeal:
|
||||
actions:
|
||||
delete_statuses: 投稿を削除する
|
||||
delete_statuses: トゥートを削除する
|
||||
disable: アカウントを無効にする
|
||||
mark_statuses_as_sensitive: 閲覧注意としてマーク
|
||||
none: 警告
|
||||
@ -905,7 +905,7 @@ ja:
|
||||
new_trending_links:
|
||||
title: トレンドリンク
|
||||
new_trending_statuses:
|
||||
title: トレンド投稿
|
||||
title: トレンドトゥート
|
||||
new_trending_tags:
|
||||
no_approved_tags: 承認されたトレンドハッシュタグはありません。
|
||||
requirements: 'これらの候補はいずれも %{rank} 位の承認済みトレンドハッシュタグのスコアを上回ります。現在 #%{lowest_tag_name} のスコアは %{lowest_tag_score} です。'
|
||||
@ -929,14 +929,14 @@ ja:
|
||||
guide_link: https://ja.crowdin.com/project/mastodon
|
||||
guide_link_text: 誰でも参加することができます。
|
||||
sensitive_content: 閲覧注意コンテンツ
|
||||
toot_layout: 投稿のレイアウト
|
||||
toot_layout: トゥートのレイアウト
|
||||
application_mailer:
|
||||
notification_preferences: メール設定の変更
|
||||
salutation: "%{name}さん"
|
||||
settings: 'メール設定の変更: %{link}'
|
||||
view: 'リンク:'
|
||||
view_profile: プロフィールを表示
|
||||
view_status: 投稿を表示
|
||||
view_status: トゥートを表示
|
||||
applications:
|
||||
created: アプリが作成されました
|
||||
destroyed: アプリが削除されました
|
||||
@ -1047,7 +1047,7 @@ ja:
|
||||
warning:
|
||||
before: '続行する前に、次の点を再度確認してください:'
|
||||
caches: 他のサーバーにコンテンツのキャッシュがずっと残る場合があります
|
||||
data_removal: あなたの投稿やその他のデータはこのサーバーから完全に削除されます
|
||||
data_removal: あなたのトゥートやその他のデータはこのサーバーから完全に削除されます
|
||||
email_change_html: アカウントを削除しなくても<a href="%{path}">メールアドレスを変更</a>できます
|
||||
email_contact_html: それでも届かない場合、<a href="mailto:%{email}">%{email}</a>までメールで問い合わせてください
|
||||
email_reconfirmation_html: 確認のメールが届かない場合、<a href="%{path}">もう一度申請</a>できます。
|
||||
@ -1071,13 +1071,13 @@ ja:
|
||||
description_html: これらは、%{instance}のスタッフがあなたのアカウントに対して行った措置や、あなたに送られた警告です。
|
||||
recipient: 送信元
|
||||
reject_appeal: 抗議を却下
|
||||
status: '投稿 #%{id}'
|
||||
status: 'トゥート #%{id}'
|
||||
status_removed: 既に削除されています
|
||||
title: "%{date}に%{action}"
|
||||
title_actions:
|
||||
delete_statuses: 投稿の削除
|
||||
delete_statuses: トゥートの削除
|
||||
disable: アカウント凍結
|
||||
mark_statuses_as_sensitive: 投稿を閲覧注意としてマーク
|
||||
mark_statuses_as_sensitive: トゥートを閲覧注意としてマーク
|
||||
none: 警告
|
||||
sensitive: アカウントを閲覧注意としてマーク
|
||||
silence: アカウントの制限
|
||||
@ -1109,7 +1109,7 @@ ja:
|
||||
archive_takeout:
|
||||
date: 日時
|
||||
download: ダウンロード
|
||||
hint_html: "<strong>投稿本文とメディア</strong>のアーカイブをリクエストできます。 データはActivityPub形式で、対応しているソフトウェアで読み込むことができます。7日毎にアーカイブをリクエストできます。"
|
||||
hint_html: "<strong>トゥート本文とメディア</strong>のアーカイブをリクエストできます。 データはActivityPub形式で、対応しているソフトウェアで読み込むことができます。7日毎にアーカイブをリクエストできます。"
|
||||
in_progress: 準備中...
|
||||
request: アーカイブをリクエスト
|
||||
size: 容量
|
||||
@ -1124,7 +1124,7 @@ ja:
|
||||
add_new: 追加
|
||||
errors:
|
||||
limit: 注目のハッシュタグの上限に達しました
|
||||
hint_html: "<strong>注目のハッシュタグとは?</strong> プロフィールページに目立つ形で表示され、そのハッシュタグのついたあなたの公開投稿だけを抽出して閲覧できるようにします。クリエイティブな仕事や長期的なプロジェクトを追うのに優れた機能です。"
|
||||
hint_html: "<strong>注目のハッシュタグとは?</strong> プロフィールページに目立つ形で表示され、そのハッシュタグのついたあなたの公開トゥートだけを抽出して閲覧できるようにします。クリエイティブな仕事や長期的なプロジェクトを追うのに優れた機能です。"
|
||||
filters:
|
||||
contexts:
|
||||
account: プロフィール
|
||||
@ -1135,8 +1135,8 @@ ja:
|
||||
edit:
|
||||
add_keyword: キーワードを追加
|
||||
keywords: キーワード
|
||||
statuses: 個別の投稿
|
||||
statuses_hint_html: このフィルタは、以下のキーワードにマッチするかどうかに関わらず、個々の投稿を選択して適用されます。 <a href="%{path}">フィルターを確認または投稿を削除</a>。
|
||||
statuses: 個別のトゥート
|
||||
statuses_hint_html: このフィルタは、以下のキーワードにマッチするかどうかに関わらず、個々のトゥートを選択して適用されます。 <a href="%{path}">フィルターを確認またはトゥートを削除</a>。
|
||||
title: フィルターを編集
|
||||
errors:
|
||||
deprecated_api_multiple_keywords: これらのパラメータは複数のフィルタキーワードに適用されるため、このアプリケーションから変更できません。 最新のアプリケーションまたはWebインターフェースを使用してください。
|
||||
@ -1150,9 +1150,9 @@ ja:
|
||||
keywords:
|
||||
other: "%{count}件のキーワード"
|
||||
statuses:
|
||||
other: "%{count}件の投稿"
|
||||
other: "%{count}件のトゥート"
|
||||
statuses_long:
|
||||
other: "%{count}件の投稿を非表示にしました"
|
||||
other: "%{count}件のトゥートを非表示にしました"
|
||||
title: フィルター
|
||||
new:
|
||||
save: 新規フィルターを保存
|
||||
@ -1162,8 +1162,8 @@ ja:
|
||||
batch:
|
||||
remove: フィルターから削除する
|
||||
index:
|
||||
hint: このフィルターは、他の条件に関係なく個々の投稿を選択する場合に適用されます。Webインターフェースからこのフィルターにさらに投稿を追加できます。
|
||||
title: フィルターされた投稿
|
||||
hint: このフィルターは、他の条件に関係なく個々のトゥートを選択する場合に適用されます。Webインターフェースからこのフィルターにさらにトゥートを追加できます。
|
||||
title: フィルターされたトゥート
|
||||
footer:
|
||||
trending_now: トレンドタグ
|
||||
generic:
|
||||
@ -1290,7 +1290,7 @@ ja:
|
||||
sign_up:
|
||||
subject: "%{name}さんがサインアップしました"
|
||||
favourite:
|
||||
body: "%{name}さんにお気に入り登録された、あなたの投稿があります:"
|
||||
body: "%{name}さんにお気に入り登録された、あなたのトゥートがあります:"
|
||||
subject: "%{name}さんにお気に入りに登録されました"
|
||||
title: 新たなお気に入り登録
|
||||
follow:
|
||||
@ -1310,13 +1310,13 @@ ja:
|
||||
poll:
|
||||
subject: "%{name} さんの投票が終了しました"
|
||||
reblog:
|
||||
body: "%{name}さんにブーストされた、あなたの投稿があります:"
|
||||
body: "%{name}さんにブーストされた、あなたのトゥートがあります:"
|
||||
subject: "%{name}さんにブーストされました"
|
||||
title: 新たなブースト
|
||||
status:
|
||||
subject: "%{name}さんが投稿しました"
|
||||
subject: "%{name}さんがトゥートしました"
|
||||
update:
|
||||
subject: "%{name}さんが投稿を更新しました"
|
||||
subject: "%{name}さんがトゥートを更新しました"
|
||||
notifications:
|
||||
email_events: メールによる通知
|
||||
email_events_hint: '受信する通知を選択:'
|
||||
@ -1340,9 +1340,9 @@ ja:
|
||||
setup: セットアップ
|
||||
wrong_code: コードが間違っています。サーバーとデバイスの時計にずれがあるかもしれません。
|
||||
pagination:
|
||||
newer: 新しい投稿
|
||||
newer: 新しいトゥート
|
||||
next: 次
|
||||
older: 以前の投稿
|
||||
older: 以前のトゥート
|
||||
prev: 前
|
||||
truncate: "…"
|
||||
polls:
|
||||
@ -1358,7 +1358,7 @@ ja:
|
||||
too_many_options: は%{max}個までです
|
||||
preferences:
|
||||
other: その他
|
||||
posting_defaults: デフォルトの投稿設定
|
||||
posting_defaults: デフォルトのトゥート設定
|
||||
public_timelines: 公開タイムライン
|
||||
privacy_policy:
|
||||
title: プライバシーポリシー
|
||||
@ -1394,11 +1394,11 @@ ja:
|
||||
rss:
|
||||
content_warning: '閲覧注意:'
|
||||
descriptions:
|
||||
account: "@%{acct}からの公開投稿"
|
||||
tag: "#%{hashtag}が付けられた公開投稿"
|
||||
account: "@%{acct}からの公開トゥート"
|
||||
tag: "#%{hashtag}が付けられた公開トゥート"
|
||||
scheduled_statuses:
|
||||
over_daily_limit: その日予約できる投稿数 %{limit}を超えています
|
||||
over_total_limit: 予約できる投稿数 %{limit}を超えています
|
||||
over_daily_limit: その日予約できるトゥート数 %{limit}を超えています
|
||||
over_total_limit: 予約できるトゥート数 %{limit}を超えています
|
||||
too_soon: より先の時間を指定してください
|
||||
sessions:
|
||||
activity: 最後のアクティビティ
|
||||
@ -1461,7 +1461,7 @@ ja:
|
||||
preferences: ユーザー設定
|
||||
profile: プロフィール
|
||||
relationships: フォロー・フォロワー
|
||||
statuses_cleanup: 投稿の自動削除
|
||||
statuses_cleanup: トゥートの自動削除
|
||||
strikes: モデレーションストライク
|
||||
two_factor_authentication: 二要素認証
|
||||
webauthn_authentication: セキュリティキー
|
||||
@ -1481,13 +1481,13 @@ ja:
|
||||
other: '許可されていないハッシュタグが含まれています: %{tags}'
|
||||
edited_at_html: "%{date} 編集済み"
|
||||
errors:
|
||||
in_reply_not_found: あなたが返信しようとしている投稿は存在しないようです。
|
||||
in_reply_not_found: あなたが返信しようとしているトゥートは存在しないようです。
|
||||
open_in_web: Webで開く
|
||||
over_character_limit: 上限は%{max}文字です
|
||||
pin_errors:
|
||||
direct: 返信したユーザーのみに表示される投稿はピン留めできません
|
||||
limit: 固定できる投稿数の上限に達しました
|
||||
ownership: 他人の投稿を固定することはできません
|
||||
direct: 返信したユーザーのみに表示されるトゥートはピン留めできません
|
||||
limit: 固定できるトゥート数の上限に達しました
|
||||
ownership: 他人のトゥートを固定することはできません
|
||||
reblog: ブーストを固定することはできません
|
||||
poll:
|
||||
total_people:
|
||||
@ -1510,26 +1510,26 @@ ja:
|
||||
unlisted: 未収載
|
||||
unlisted_long: 誰でも見ることができますが、公開タイムラインには表示されません
|
||||
statuses_cleanup:
|
||||
enabled: 古い投稿を自動的に削除する
|
||||
enabled_hint: 設定した期間を過ぎた投稿は、以下の例外に該当しない限り、自動的に削除されます
|
||||
enabled: 古いトゥートを自動的に削除する
|
||||
enabled_hint: 設定した期間を過ぎたトゥートは、以下の例外に該当しない限り、自動的に削除されます
|
||||
exceptions: 例外
|
||||
explanation: 投稿の削除はサーバーに負荷がかかるため、サーバーが混み合っていないときに時間をかけて行われます。
|
||||
explanation: トゥートの削除はサーバーに負荷がかかるため、サーバーが混み合っていないときに時間をかけて行われます。
|
||||
ignore_favs: 設定しない
|
||||
ignore_reblogs: 設定しない
|
||||
interaction_exceptions: インタラクションに基づく例外
|
||||
interaction_exceptions_explanation: お気に入りやブーストの基準値を一度超えてしまった投稿は、基準値を下回った後であっても、削除される保証はありません。
|
||||
interaction_exceptions_explanation: お気に入りやブーストの基準値を一度超えてしまったトゥートは、基準値を下回った後であっても、削除される保証はありません。
|
||||
keep_direct: ダイレクトメッセージを保持
|
||||
keep_direct_hint: ダイレクトメッセージを削除せずに残します
|
||||
keep_media: メディア付きの投稿を保持
|
||||
keep_media_hint: メディア付きの投稿を削除せずに残します
|
||||
keep_pinned: ピン留めされた投稿を保持
|
||||
keep_pinned_hint: ピン留めされた投稿を削除せずに残します
|
||||
keep_polls: アンケート付きの投稿を保持
|
||||
keep_polls_hint: アンケート付きの投稿を削除せずに残します
|
||||
keep_self_bookmark: ブックマークした投稿を保持
|
||||
keep_self_bookmark_hint: 自分自身でブックマークした投稿を削除せずに残します
|
||||
keep_self_fav: お気に入りに登録した投稿を保持
|
||||
keep_self_fav_hint: 自分自身でお気に入りに登録した投稿を削除せずに残します
|
||||
keep_media: メディア付きのトゥートを保持
|
||||
keep_media_hint: メディア付きのトゥートを削除せずに残します
|
||||
keep_pinned: ピン留めされたトゥートを保持
|
||||
keep_pinned_hint: ピン留めされたトゥートを削除せずに残します
|
||||
keep_polls: アンケート付きのトゥートを保持
|
||||
keep_polls_hint: アンケート付きのトゥートを削除せずに残します
|
||||
keep_self_bookmark: ブックマークしたトゥートを保持
|
||||
keep_self_bookmark_hint: 自分自身でブックマークしたトゥートを削除せずに残します
|
||||
keep_self_fav: お気に入りに登録したトゥートを保持
|
||||
keep_self_fav_hint: 自分自身でお気に入りに登録したトゥートを削除せずに残します
|
||||
min_age:
|
||||
'1209600': 2週間
|
||||
'15778476': 半年
|
||||
@ -1539,13 +1539,13 @@ ja:
|
||||
'604800': 1週間
|
||||
'63113904': 2年
|
||||
'7889238': 3ヶ月
|
||||
min_age_label: 投稿を保持する期間
|
||||
min_age_label: トゥートを保持する期間
|
||||
min_favs: お気に入りの基準値
|
||||
min_favs_hint: この数以上、お気に入りされた投稿を削除せずに残します。空白にしておくと、お気に入りに登録された数に関わらず投稿を削除します。
|
||||
min_favs_hint: この数以上、お気に入りされたトゥートを削除せずに残します。空白にしておくと、お気に入りに登録された数に関わらずトゥートを削除します。
|
||||
min_reblogs: ブーストの基準値
|
||||
min_reblogs_hint: この数以上、ブーストされた投稿を削除せずに残します。空白にしておくと、ブーストされた数に関わらず投稿を削除します。
|
||||
min_reblogs_hint: この数以上、ブーストされたトゥートを削除せずに残します。空白にしておくと、ブーストされた数に関わらずトゥートを削除します。
|
||||
stream_entries:
|
||||
pinned: 固定された投稿
|
||||
pinned: 固定されたトゥート
|
||||
reblogged: さんがブースト
|
||||
sensitive_content: 閲覧注意
|
||||
strikes:
|
||||
@ -1605,26 +1605,26 @@ ja:
|
||||
spam: スパム
|
||||
violation: コンテンツは以下のコミュニティガイドラインに違反しています
|
||||
explanation:
|
||||
delete_statuses: あなたの投稿のいくつかは、1つ以上のコミュニティガイドラインに違反していることが判明し、%{instance}のモデレータによって削除されました。
|
||||
delete_statuses: あなたのトゥートのいくつかは、1つ以上のコミュニティガイドラインに違反していることが判明し、%{instance}のモデレータによって削除されました。
|
||||
disable: アカウントは使用できませんが、プロフィールやその他のデータはそのまま残ります。 データのバックアップをリクエストしたり、アカウント設定を変更したり、アカウントを削除したりできます。
|
||||
mark_statuses_as_sensitive: あなたのいくつかの投稿は、%{instance}のモデレータによって閲覧注意としてマークされています。これは、プレビューが表示される前にユーザが投稿内のメディアをタップする必要があることを意味します。あなたは将来投稿する際に自分自身でメディアを閲覧注意としてマークすることができます。
|
||||
mark_statuses_as_sensitive: あなたのいくつかのトゥートは、%{instance}のモデレータによって閲覧注意としてマークされています。これは、プレビューが表示される前にユーザがトゥート内のメディアをタップする必要があることを意味します。あなたは将来トゥートする際に自分自身でメディアを閲覧注意としてマークすることができます。
|
||||
sensitive: 今後、アップロードされたすべてのメディアファイルは閲覧注意としてマークされ、クリック解除式の警告で覆われるようになります。
|
||||
silence: アカウントが制限されています。このサーバーでは既にフォローしている人だけがあなたの投稿を見ることができます。 様々な発見機能から除外されるかもしれません。他の人があなたを手動でフォローすることは可能です。
|
||||
silence: アカウントが制限されています。このサーバーでは既にフォローしている人だけがあなたのトゥートを見ることができます。 様々な発見機能から除外されるかもしれません。他の人があなたを手動でフォローすることは可能です。
|
||||
suspend: アカウントは使用できなくなり、プロフィールなどのデータにもアクセスできなくなります。約30日後にデータが完全に削除されるまでは、ログインしてデータのバックアップを要求することができますが、アカウントの停止回避を防ぐために一部の基本データを保持します。
|
||||
reason: '理由:'
|
||||
statuses: '投稿:'
|
||||
statuses: 'トゥート:'
|
||||
subject:
|
||||
delete_statuses: "%{acct}さんの投稿が削除されました"
|
||||
delete_statuses: "%{acct}さんのトゥートが削除されました"
|
||||
disable: あなたのアカウント %{acct}は凍結されました
|
||||
mark_statuses_as_sensitive: あなたの%{acct}の投稿は閲覧注意としてマークされました
|
||||
mark_statuses_as_sensitive: あなたの%{acct}のトゥートは閲覧注意としてマークされました
|
||||
none: "%{acct}に対する警告"
|
||||
sensitive: あなたの%{acct}の投稿はこれから閲覧注意としてマークされます
|
||||
sensitive: あなたの%{acct}のトゥートはこれから閲覧注意としてマークされます
|
||||
silence: あなたのアカウント %{acct}はサイレンスにされました
|
||||
suspend: あなたのアカウント %{acct}は停止されました
|
||||
title:
|
||||
delete_statuses: 投稿が削除されました
|
||||
delete_statuses: トゥートが削除されました
|
||||
disable: アカウントが凍結されました
|
||||
mark_statuses_as_sensitive: 閲覧注意としてマークされた投稿
|
||||
mark_statuses_as_sensitive: 閲覧注意としてマークされたトゥート
|
||||
none: 警告
|
||||
sensitive: 閲覧注意としてマークされたアカウント
|
||||
silence: アカウントがサイレンスにされました
|
||||
@ -1636,7 +1636,7 @@ ja:
|
||||
新しいフォロワーからフォローリクエストを承認する前に、オプトインで確認できます。
|
||||
explanation: 始めるにあたってのアドバイスです
|
||||
final_action: 始めましょう
|
||||
final_step: 'さあ、始めましょう! たとえフォロワーがまだいなくても、あなたの公開した投稿はローカルタイムラインやハッシュタグなどを通じて誰かの目にとまるはずです。自己紹介をしたいときには #introductions ハッシュタグが便利かもしれません。'
|
||||
final_step: 'さあ、始めましょう! たとえフォロワーがまだいなくても、あなたの公開したトゥートはローカルタイムラインやハッシュタグなどを通じて誰かの目にとまるはずです。自己紹介をしたいときには #introductions ハッシュタグが便利かもしれません。'
|
||||
full_handle: あなたの正式なユーザーID
|
||||
full_handle_hint: 別のサーバーの友達とフォローやメッセージをやり取りする際には、これを伝えることになります。
|
||||
subject: Mastodonへようこそ
|
||||
|
@ -209,8 +209,10 @@ en:
|
||||
setting_expand_spoilers: Always expand posts marked with content warnings
|
||||
setting_hide_network: Hide your social graph
|
||||
setting_noindex: Opt-out of search engine indexing
|
||||
setting_place_tab_bar_at_bottom: Place the tab bar at the bottom
|
||||
setting_reduce_motion: Reduce motion in animations
|
||||
setting_show_application: Disclose application used to send posts
|
||||
setting_show_status_reaction: Show emoji reactions
|
||||
setting_system_font_ui: Use system's default font
|
||||
setting_theme: Site theme
|
||||
setting_trends: Show today's trends
|
||||
|
@ -7,18 +7,18 @@ ja:
|
||||
account_migration:
|
||||
acct: 引っ越し先のユーザー名@ドメインを指定してください
|
||||
account_warning_preset:
|
||||
text: URL、ハッシュタグ、メンションなど、投稿に用いる構文が使用できます
|
||||
text: URL、ハッシュタグ、メンションなど、トゥートに用いる構文が使用できます
|
||||
title: オプションです。受信者には表示されません。
|
||||
admin_account_action:
|
||||
include_statuses: ユーザーは取られた制限や警告の原因となった投稿を確認できるようになります
|
||||
include_statuses: ユーザーは取られた制限や警告の原因となったトゥートを確認できるようになります
|
||||
send_email_notification: ユーザーは自分のアカウントに何が起こったのか説明を受け取ります
|
||||
text_html: オプションです。投稿に用いる構文を使うことができます。簡略化のため<a href="%{path}">プリセット警告文を追加</a>することができます
|
||||
text_html: オプションです。トゥートに用いる構文を使うことができます。簡略化のため<a href="%{path}">プリセット警告文を追加</a>することができます
|
||||
type_html: "<strong>%{acct}</strong>さんに対し、何を行うか選択してください"
|
||||
types:
|
||||
disable: ユーザーが自分のアカウントを使用できないようにします。コンテンツを削除したり非表示にすることはありません。
|
||||
none: これを使用すると、他の操作をせずにユーザーに警告を送信できます。
|
||||
sensitive: このユーザーが添付したメディアを強制的に閲覧注意にする
|
||||
silence: ユーザーによる公開投稿を禁止し、フォローしていない人に投稿や通知が表示されないようにします。また、このアカウントに対するすべての通報をクローズします。
|
||||
silence: ユーザーによる公開トゥートを禁止し、フォローしていない人にトゥートや通知が表示されないようにします。また、このアカウントに対するすべての通報をクローズします。
|
||||
suspend: このアカウントによるすべての活動を禁止し、コンテンツを削除します。この操作は30日以内であれば取り消しが可能です。また、このアカウントに対するすべての通報をクローズします。
|
||||
warning_preset_id: オプションです。プリセット警告文の末尾に任意の文字列を追加することができます
|
||||
announcement:
|
||||
@ -26,7 +26,7 @@ ja:
|
||||
ends_at: オプションです。指定すると、お知らせの掲載はその日時で自動的に終了します
|
||||
scheduled_at: お知らせを今すぐ掲載する場合は空欄にしてください
|
||||
starts_at: オプションです。お知らせしたい事柄の期間が決まっている場合に使用します
|
||||
text: 投稿と同じ構文を使用できます。アナウンスが占める画面のスペースに注意してください
|
||||
text: トゥートと同じ構文を使用できます。アナウンスが占める画面のスペースに注意してください
|
||||
appeal:
|
||||
text: 一度だけ異議を申し立てることができます
|
||||
defaults:
|
||||
@ -42,21 +42,21 @@ ja:
|
||||
fields: プロフィールに表として4つまでの項目を表示することができます
|
||||
header: "%{size}までのPNG、GIF、JPGが利用可能です。 %{dimensions}pxまで縮小されます"
|
||||
inbox_url: 使用したいリレーサーバーのトップページからURLをコピーします
|
||||
irreversible: フィルターが後で削除されても、除外された投稿は元に戻せなくなります
|
||||
irreversible: フィルターが後で削除されても、除外されたトゥートは元に戻せなくなります
|
||||
locale: ユーザーインターフェース、メールやプッシュ通知の言語
|
||||
locked: フォロワーを手動で承認する必要があります
|
||||
password: 少なくとも8文字は入力してください
|
||||
phrase: 投稿内容の大文字小文字や閲覧注意に関係なく一致
|
||||
phrase: トゥート内容の大文字小文字や閲覧注意に関係なく一致
|
||||
scopes: アプリの API に許可するアクセス権を選択してください。最上位のスコープを選択する場合、個々のスコープを選択する必要はありません。
|
||||
setting_aggregate_reblogs: 最近ブーストされた投稿が新たにブーストされても表示しません (設定後受信したものにのみ影響)
|
||||
setting_aggregate_reblogs: 最近ブーストされたトゥートが新たにブーストされても表示しません (設定後受信したものにのみ影響)
|
||||
setting_always_send_emails: 通常、Mastodon からメール通知は行われません。
|
||||
setting_default_sensitive: 閲覧注意状態のメディアはデフォルトでは内容が伏せられ、クリックして初めて閲覧できるようになります
|
||||
setting_display_media_default: 閲覧注意としてマークされたメディアは隠す
|
||||
setting_display_media_hide_all: メディアを常に隠す
|
||||
setting_display_media_show_all: メディアを常に表示する
|
||||
setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします
|
||||
setting_noindex: 公開プロフィールおよび各投稿ページに影響します
|
||||
setting_show_application: 投稿するのに使用したアプリが投稿の詳細ビューに表示されるようになります
|
||||
setting_noindex: 公開プロフィールおよび各トゥートページに影響します
|
||||
setting_show_application: トゥートするのに使用したアプリがトゥートの詳細ビューに表示されるようになります
|
||||
setting_use_blurhash: ぼかしはメディアの色を元に生成されますが、細部は見えにくくなっています
|
||||
setting_use_pending_items: 新着があってもタイムラインを自動的にスクロールしないようにします
|
||||
username: あなたのユーザー名は%{domain}の中で重複していない必要があります
|
||||
@ -69,16 +69,16 @@ ja:
|
||||
featured_tag:
|
||||
name: 最近使用したハッシュタグ
|
||||
filters:
|
||||
action: 投稿がフィルタに一致したときに実行するアクションを選択
|
||||
action: トゥートがフィルタに一致したときに実行するアクションを選択
|
||||
actions:
|
||||
hide: フィルタに一致した投稿を完全に非表示にします
|
||||
warn: フィルタに一致した投稿を非表示にし、フィルタのタイトルを含む警告を表示します
|
||||
hide: フィルタに一致したトゥートを完全に非表示にします
|
||||
warn: フィルタに一致したトゥートを非表示にし、フィルタのタイトルを含む警告を表示します
|
||||
form_admin_settings:
|
||||
activity_api_enabled: 週単位でローカルで公開された投稿数、アクティブユーザー数、新規登録者数を表示します
|
||||
activity_api_enabled: 週単位でローカルで公開されたトゥート数、アクティブユーザー数、新規登録者数を表示します
|
||||
backups_retention_period: 生成されたユーザーのアーカイブを指定した日数の間保持します。
|
||||
bootstrap_timeline_accounts: これらのアカウントは、新しいユーザー向けのおすすめユーザーの一番上にピン留めされます。
|
||||
closed_registrations_message: アカウント作成を停止している時に表示されます
|
||||
content_cache_retention_period: 正の値に設定されている場合、他のサーバーの投稿は指定された日数の後に削除されます。元に戻せません。
|
||||
content_cache_retention_period: 正の値に設定されている場合、他のサーバーのトゥートは指定された日数の後に削除されます。元に戻せません。
|
||||
custom_css: ウェブ版のMastodonでカスタムスタイルを適用できます。
|
||||
mascot: 上級者向けWebインターフェースのイラストを上書きします。
|
||||
media_cache_retention_period: 正の値に設定されている場合、ダウンロードされたメディアファイルは指定された日数の後に削除され、リクエストに応じて再ダウンロードされます。
|
||||
@ -94,9 +94,9 @@ ja:
|
||||
status_page_url: 障害発生時などにユーザーがサーバーの状態を確認できるページのURL
|
||||
theme: ログインしていない人と新規ユーザーに表示されるテーマ。
|
||||
thumbnail: サーバー情報と共に表示される、アスペクト比が約 2:1 の画像。
|
||||
timeline_preview: ログインしていないユーザーがサーバー上の最新の公開投稿を閲覧できるようにします。
|
||||
timeline_preview: ログインしていないユーザーがサーバー上の最新の公開トゥートを閲覧できるようにします。
|
||||
trendable_by_default: トレンドの審査を省略します。トレンドは掲載後でも個別に除外できます。
|
||||
trends: トレンドは、サーバー上で人気を集めている投稿、ハッシュタグ、ニュース記事などが表示されます。
|
||||
trends: トレンドは、サーバー上で人気を集めているトゥート、ハッシュタグ、ニュース記事などが表示されます。
|
||||
trends_as_landing_page: ログインしていないユーザーに対して、サーバーの説明の代わりにトレンドコンテンツを表示します。トレンドを有効にする必要があります。
|
||||
form_challenge:
|
||||
current_password: セキュリティ上重要なエリアにアクセスしています
|
||||
@ -121,7 +121,7 @@ ja:
|
||||
tag:
|
||||
name: 視認性向上などのためにアルファベット大文字小文字の変更のみ行うことができます
|
||||
user:
|
||||
chosen_languages: 選択すると、選択した言語の投稿のみが公開タイムラインに表示されるようになります
|
||||
chosen_languages: 選択すると、選択した言語のトゥートのみが公開タイムラインに表示されるようになります
|
||||
role: このロールはユーザーが持つ権限を管理します
|
||||
user_role:
|
||||
color: UI 全体でロールの表示に使用される色(16進数RGB形式)
|
||||
@ -145,7 +145,7 @@ ja:
|
||||
text: プリセット警告文
|
||||
title: タイトル
|
||||
admin_account_action:
|
||||
include_statuses: 通報された投稿をメールに含める
|
||||
include_statuses: 通報されたトゥートをメールに含める
|
||||
send_email_notification: メールでユーザーに通知
|
||||
text: カスタム警告文
|
||||
type: アクション
|
||||
@ -196,19 +196,20 @@ ja:
|
||||
setting_always_send_emails: 常にメール通知を送信する
|
||||
setting_auto_play_gif: アニメーションGIFを自動再生する
|
||||
setting_boost_modal: ブーストする前に確認ダイアログを表示する
|
||||
setting_crop_images: 投稿の詳細以外では画像を16:9に切り抜く
|
||||
setting_default_language: 投稿する言語
|
||||
setting_default_privacy: 投稿の公開範囲
|
||||
setting_crop_images: トゥートの詳細以外では画像を16:9に切り抜く
|
||||
setting_default_language: トゥートする言語
|
||||
setting_default_privacy: トゥートの公開範囲
|
||||
setting_default_sensitive: メディアを常に閲覧注意としてマークする
|
||||
setting_delete_modal: 投稿を削除する前に確認ダイアログを表示する
|
||||
setting_delete_modal: トゥートを削除する前に確認ダイアログを表示する
|
||||
setting_disable_swiping: スワイプでの切り替えを無効にする
|
||||
setting_display_media: メディアの表示
|
||||
setting_display_media_default: 標準
|
||||
setting_display_media_hide_all: 非表示
|
||||
setting_display_media_show_all: 表示
|
||||
setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する
|
||||
setting_expand_spoilers: 閲覧注意としてマークされたトゥートを常に展開する
|
||||
setting_hide_network: 繋がりを隠す
|
||||
setting_noindex: 検索エンジンによるインデックスを拒否する
|
||||
setting_place_tab_bar_at_bottom: タブバーを下に配置する
|
||||
setting_reduce_motion: アニメーションの動きを減らす
|
||||
setting_show_application: 送信したアプリを開示する
|
||||
setting_system_font_ui: システムのデフォルトフォントを使う
|
||||
@ -284,7 +285,7 @@ ja:
|
||||
follow_request: フォローリクエストを受けた時
|
||||
mention: 返信が来た時
|
||||
pending_account: 新しいアカウントの承認が必要な時
|
||||
reblog: 投稿がブーストされた時
|
||||
reblog: トゥートがブーストされた時
|
||||
report: 新しい通報が送信された時
|
||||
trending_tag: 新しいトレンドのレビューをする必要がある時
|
||||
rule:
|
||||
@ -293,7 +294,7 @@ ja:
|
||||
listable: 検索とディレクトリへの使用を許可する
|
||||
name: ハッシュタグ
|
||||
trendable: トレンドへの表示を許可する
|
||||
usable: 投稿への使用を許可する
|
||||
usable: トゥートへの使用を許可する
|
||||
user:
|
||||
role: ロール
|
||||
user_role:
|
||||
|
@ -38,6 +38,7 @@ defaults: &defaults
|
||||
trends_as_landing_page: true
|
||||
trendable_by_default: false
|
||||
crop_images: true
|
||||
place_tab_bar_at_bottom: false
|
||||
notification_emails:
|
||||
follow: true
|
||||
reblog: false
|
||||
|
@ -1,3 +1,4 @@
|
||||
default: styles/application.scss
|
||||
contrast: styles/contrast.scss
|
||||
mastodon-light: styles/mastodon-light.scss
|
||||
light-pink: styles/light-pink.scss
|
5
db/migrate/20180419235016_add_quote_id_to_statuses.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class AddQuoteIdToStatuses < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
add_column :statuses, :quote_id, :bigint, null: true, default: nil
|
||||
end
|
||||
end
|
@ -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
|
@ -935,6 +935,7 @@ ActiveRecord::Schema.define(version: 2022_12_06_114142) do
|
||||
t.bigint "account_id", null: false
|
||||
t.bigint "application_id"
|
||||
t.bigint "in_reply_to_account_id"
|
||||
t.bigint "quote_id"
|
||||
t.bigint "poll_id"
|
||||
t.datetime "deleted_at"
|
||||
t.datetime "edited_at"
|
||||
@ -947,6 +948,7 @@ ActiveRecord::Schema.define(version: 2022_12_06_114142) do
|
||||
t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
|
||||
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id", where: "(in_reply_to_account_id IS NOT NULL)"
|
||||
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", where: "(in_reply_to_id IS NOT NULL)"
|
||||
t.index ["quote_id"], name: "index_statuses_on_quote_id"
|
||||
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
|
||||
t.index ["uri"], name: "index_statuses_on_uri", unique: true, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)"
|
||||
end
|
||||
|
@ -31,6 +31,7 @@ class Sanitize
|
||||
next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes
|
||||
next true if /^(mention|hashtag)$/.match?(e) # semantic classes
|
||||
next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes
|
||||
next true if /^quote-inline$/.match?(e) # quote inline classes
|
||||
end
|
||||
|
||||
node['class'] = class_list.join(' ')
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.4 KiB |
1
public/y-zu-logo.svg
Normal file
After Width: | Height: | Size: 15 KiB |