Merge commit '7ed9c590b98610f8d68deab9ef8df260eec6d8f0' into glitch-soc/merge-upstream

This commit is contained in:
Claire 2024-09-21 15:06:06 +02:00
commit 221bba1897
310 changed files with 2257 additions and 2167 deletions

View file

@ -47,7 +47,6 @@ gem 'color_diff', '~> 0.1'
gem 'csv', '~> 3.2' gem 'csv', '~> 3.2'
gem 'discard', '~> 1.2' gem 'discard', '~> 1.2'
gem 'doorkeeper', '~> 5.6' gem 'doorkeeper', '~> 5.6'
gem 'ed25519', '~> 1.3'
gem 'fast_blank', '~> 1.0' gem 'fast_blank', '~> 1.0'
gem 'fastimage' gem 'fastimage'
gem 'hiredis', '~> 0.6' gem 'hiredis', '~> 0.6'

View file

@ -100,8 +100,8 @@ GEM
attr_required (1.0.2) attr_required (1.0.2)
awrence (1.2.1) awrence (1.2.1)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.974.0) aws-partitions (1.977.0)
aws-sdk-core (3.205.0) aws-sdk-core (3.206.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
@ -109,11 +109,11 @@ GEM
aws-sdk-kms (1.91.0) aws-sdk-kms (1.91.0)
aws-sdk-core (~> 3, >= 3.205.0) aws-sdk-core (~> 3, >= 3.205.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.162.0) aws-sdk-s3 (1.163.0)
aws-sdk-core (~> 3, >= 3.205.0) aws-sdk-core (~> 3, >= 3.205.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.9.1) aws-sigv4 (1.10.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3) azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0) azure-storage-common (~> 2.0)
@ -197,7 +197,7 @@ GEM
railties (>= 4.1.0) railties (>= 4.1.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (5.1.0) devise-two-factor (6.0.0)
activesupport (~> 7.0) activesupport (~> 7.0)
devise (~> 4.0) devise (~> 4.0)
railties (~> 7.0) railties (~> 7.0)
@ -212,9 +212,8 @@ GEM
domain_name (0.6.20240107) domain_name (0.6.20240107)
doorkeeper (5.7.1) doorkeeper (5.7.1)
railties (>= 5) railties (>= 5)
dotenv (3.1.2) dotenv (3.1.4)
drb (2.2.1) drb (2.2.1)
ed25519 (1.3.0)
elasticsearch (7.17.11) elasticsearch (7.17.11)
elasticsearch-api (= 7.17.11) elasticsearch-api (= 7.17.11)
elasticsearch-transport (= 7.17.11) elasticsearch-transport (= 7.17.11)
@ -429,7 +428,7 @@ GEM
addressable (~> 2.5) addressable (~> 2.5)
azure-storage-blob (~> 2.0.1) azure-storage-blob (~> 2.0.1)
hashie (~> 5.0) hashie (~> 5.0)
memory_profiler (1.0.2) memory_profiler (1.1.0)
mime-types (3.5.2) mime-types (3.5.2)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2024.0820) mime-types-data (3.2024.0820)
@ -610,7 +609,7 @@ GEM
psych (5.1.2) psych (5.1.2)
stringio stringio
public_suffix (6.0.1) public_suffix (6.0.1)
puma (6.4.2) puma (6.4.3)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.4.0) pundit (2.4.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -937,7 +936,6 @@ DEPENDENCIES
discard (~> 1.2) discard (~> 1.2)
doorkeeper (~> 5.6) doorkeeper (~> 5.6)
dotenv dotenv
ed25519 (~> 1.3)
email_spec email_spec
fabrication (~> 2.30) fabrication (~> 2.30)
faker (~> 3.2) faker (~> 3.2)

View file

@ -1,18 +0,0 @@
# frozen_string_literal: true
class ActivityPub::ClaimsController < ActivityPub::BaseController
skip_before_action :authenticate_user!
before_action :require_account_signature!
before_action :set_claim_result
def create
render json: @claim_result, serializer: ActivityPub::OneTimeKeySerializer
end
private
def set_claim_result
@claim_result = ::Keys::ClaimService.new.call(@account.id, params[:id])
end
end

View file

@ -22,8 +22,6 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
@items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) } @items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) }
when 'tags' when 'tags'
@items = for_signed_account { @account.featured_tags } @items = for_signed_account { @account.featured_tags }
when 'devices'
@items = @account.devices
else else
not_found not_found
end end
@ -31,7 +29,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def set_size def set_size
case params[:id] case params[:id]
when 'featured', 'devices', 'tags' when 'featured', 'tags'
@size = @items.size @size = @items.size
else else
not_found not_found
@ -42,7 +40,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
case params[:id] case params[:id]
when 'featured' when 'featured'
@type = :ordered @type = :ordered
when 'devices', 'tags' when 'tags'
@type = :unordered @type = :unordered
else else
not_found not_found

View file

@ -7,7 +7,7 @@ class Api::OEmbedController < Api::BaseController
before_action :require_public_status! before_action :require_public_status!
def show def show
render json: @status, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default render json: @status, serializer: OEmbedSerializer, width: params[:maxwidth], height: params[:maxheight]
end end
private private
@ -23,12 +23,4 @@ class Api::OEmbedController < Api::BaseController
def status_finder def status_finder
StatusFinder.new(params[:url]) StatusFinder.new(params[:url])
end end
def maxwidth_or_default
(params[:maxwidth].presence || 400).to_i
end
def maxheight_or_default
params[:maxheight].present? ? params[:maxheight].to_i : nil
end
end end

View file

@ -1,30 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::DeliveriesController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_current_device
def create
devices.each do |device_params|
DeliverToDeviceService.new.call(current_account, @current_device, device_params)
end
render_empty
end
private
def set_current_device
@current_device = Device.find_by!(access_token: doorkeeper_token)
end
def resource_params
params.require(:device)
params.permit(device: [:account_id, :device_id, :type, :body, :hmac])
end
def devices
Array(resource_params[:device])
end
end

View file

@ -1,47 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
LIMIT = 80
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_current_device
before_action :set_encrypted_messages, only: :index
after_action :insert_pagination_headers, only: :index
def index
render json: @encrypted_messages, each_serializer: REST::EncryptedMessageSerializer
end
def clear
@current_device.encrypted_messages.up_to(params[:up_to_id]).delete_all
render_empty
end
private
def set_current_device
@current_device = Device.find_by!(access_token: doorkeeper_token)
end
def set_encrypted_messages
@encrypted_messages = @current_device.encrypted_messages.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def next_path
api_v1_crypto_encrypted_messages_url pagination_params(max_id: pagination_max_id) if records_continue?
end
def prev_path
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
end
def pagination_collection
@encrypted_messages
end
def records_continue?
@encrypted_messages.size == limit_param(LIMIT)
end
end

View file

@ -1,25 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_claim_results
def create
render json: @claim_results, each_serializer: REST::Keys::ClaimResultSerializer
end
private
def set_claim_results
@claim_results = devices.filter_map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }
end
def resource_params
params.permit(device: [:account_id, :device_id])
end
def devices
Array(resource_params[:device])
end
end

View file

@ -1,17 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::Keys::CountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_current_device
def show
render json: { one_time_keys: @current_device.one_time_keys.count }
end
private
def set_current_device
@current_device = Device.find_by!(access_token: doorkeeper_token)
end
end

View file

@ -1,26 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::Keys::QueriesController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_accounts
before_action :set_query_results
def create
render json: @query_results, each_serializer: REST::Keys::QueryResultSerializer
end
private
def set_accounts
@accounts = Account.where(id: account_ids).includes(:devices)
end
def set_query_results
@query_results = @accounts.filter_map { |account| ::Keys::QueryService.new.call(account) }
end
def account_ids
Array(params[:id]).map(&:to_i)
end
end

View file

@ -1,29 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::Keys::UploadsController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
def create
device = Device.find_or_initialize_by(access_token: doorkeeper_token)
device.transaction do
device.account = current_account
device.update!(resource_params[:device])
if resource_params[:one_time_keys].present? && resource_params[:one_time_keys].is_a?(Enumerable)
resource_params[:one_time_keys].each do |one_time_key_params|
device.one_time_keys.create!(one_time_key_params)
end
end
end
render json: device, serializer: REST::Keys::DeviceSerializer
end
private
def resource_params
params.permit(device: [:device_id, :name, :fingerprint_key, :identity_key], one_time_keys: [:key_id, :key, :signature])
end
end

View file

@ -7,6 +7,8 @@ class Api::V1::Peers::SearchController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
skip_around_action :set_locale skip_around_action :set_locale
LIMIT = 10
vary_by '' vary_by ''
def index def index
@ -35,10 +37,10 @@ class Api::V1::Peers::SearchController < Api::BaseController
field: 'accounts_count', field: 'accounts_count',
modifier: 'log2p', modifier: 'log2p',
}, },
}).limit(10).pluck(:domain) }).limit(LIMIT).pluck(:domain)
else else
domain = normalized_domain domain = normalized_domain
@domains = Instance.searchable.domain_starts_with(domain).limit(10).pluck(:domain) @domains = Instance.searchable.domain_starts_with(domain).limit(LIMIT).pluck(:domain)
end end
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError
@domains = [] @domains = []

View file

@ -9,7 +9,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController
return not_found if @status.hidden? return not_found if @status.hidden?
if @status.local? if @status.local?
render json: @status, serializer: OEmbedSerializer, width: 400 render json: @status, serializer: OEmbedSerializer
else else
return not_found unless user_signed_in? return not_found unless user_signed_in?

View file

@ -20,11 +20,6 @@ class Auth::SessionsController < Devise::SessionsController
p.form_action(false) p.form_action(false)
end end
def check_suspicious!
user = find_user
@login_is_suspicious = suspicious_sign_in?(user) unless user.nil?
end
def create def create
super do |resource| super do |resource|
# We only need to call this if this hasn't already been # We only need to call this if this hasn't already been
@ -101,6 +96,11 @@ class Auth::SessionsController < Devise::SessionsController
private private
def check_suspicious!
user = find_user
@login_is_suspicious = suspicious_sign_in?(user) unless user.nil?
end
def home_paths(resource) def home_paths(resource)
paths = [about_path, '/explore'] paths = [about_path, '/explore']

View file

@ -24,23 +24,6 @@ module ContextHelper
indexable: { 'toot' => 'http://joinmastodon.org/ns#', 'indexable' => 'toot:indexable' }, indexable: { 'toot' => 'http://joinmastodon.org/ns#', 'indexable' => 'toot:indexable' },
memorial: { 'toot' => 'http://joinmastodon.org/ns#', 'memorial' => 'toot:memorial' }, memorial: { 'toot' => 'http://joinmastodon.org/ns#', 'memorial' => 'toot:memorial' },
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, 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' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } }, attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
}.freeze }.freeze

View file

@ -0,0 +1,31 @@
import { useMemo, useCallback } from 'react';
import { useLocation, useHistory } from 'react-router';
export function useSearchParams() {
const { search } = useLocation();
return useMemo(() => new URLSearchParams(search), [search]);
}
export function useSearchParam(name: string, defaultValue?: string) {
const searchParams = useSearchParams();
const history = useHistory();
const value = searchParams.get(name) ?? defaultValue;
const setValue = useCallback(
(value: string | null) => {
if (value === null) {
searchParams.delete(name);
} else {
searchParams.set(name, value);
}
history.push({ search: searchParams.toString() });
},
[history, name, searchParams],
);
return [value, setValue] as const;
}

View file

@ -1,5 +1,7 @@
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
import { AxiosError } from 'axios';
const messages = defineMessages({ const messages = defineMessages({
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' }, unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
@ -50,6 +52,11 @@ export const showAlertForError = (error, skipNotFound = false) => {
}); });
} }
// An aborted request, e.g. due to reloading the browser window, it not really error
if (error.code === AxiosError.ECONNABORTED) {
return { type: ALERT_NOOP };
}
console.error(error); console.error(error);
return showAlert({ return showAlert({

View file

@ -42,6 +42,9 @@ const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => {
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
export default function api(withAuthorization = true) { export default function api(withAuthorization = true) {
return axios.create({ return axios.create({
transitional: {
clarifyTimeoutError: true,
},
headers: { headers: {
...csrfHeader, ...csrfHeader,
...(withAuthorization ? authorizationTokenFromInitialState() : {}), ...(withAuthorization ? authorizationTokenFromInitialState() : {}),

View file

@ -148,7 +148,7 @@ class ModalRoot extends PureComponent {
return ( return (
<div className='modal-root' ref={this.setRef}> <div className='modal-root' ref={this.setRef}>
<div style={{ pointerEvents: visible ? 'auto' : 'none' }}> <div style={{ pointerEvents: visible ? 'auto' : 'none' }}>
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.7)` : null }} /> <div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.9)` : null }} />
<div role='dialog' className='modal-root__container'>{children}</div> <div role='dialog' className='modal-root__container'>{children}</div>
</div> </div>
</div> </div>

View file

@ -51,7 +51,8 @@ function normalizePath(
if ( if (
layoutFromWindow() === 'multi-column' && layoutFromWindow() === 'multi-column' &&
!location.pathname?.startsWith('/deck') location.pathname &&
!location.pathname.startsWith('/deck')
) { ) {
location.pathname = `/deck${location.pathname}`; location.pathname = `/deck${location.pathname}`;
} }

View file

@ -1,5 +1,5 @@
import type { ChangeEventHandler } from 'react'; import type { ChangeEventHandler } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -23,6 +23,8 @@ import { RadioButton } from 'mastodon/components/radio_button';
import ScrollContainer from 'mastodon/containers/scroll_container'; import ScrollContainer from 'mastodon/containers/scroll_container';
import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { useSearchParam } from '../../../hooks/useSearchParam';
import { AccountCard } from './components/account_card'; import { AccountCard } from './components/account_card';
const messages = defineMessages({ const messages = defineMessages({
@ -47,18 +49,19 @@ export const Directory: React.FC<{
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [state, setState] = useState<{
order: string | null;
local: boolean | null;
}>({
order: null,
local: null,
});
const column = useRef<Column>(null); const column = useRef<Column>(null);
const order = state.order ?? params?.order ?? 'active'; const [orderParam, setOrderParam] = useSearchParam('order');
const local = state.local ?? params?.local ?? false; const [localParam, setLocalParam] = useSearchParam('local');
let localParamBool: boolean | undefined;
if (localParam === 'false') {
localParamBool = false;
}
const order = orderParam ?? params?.order ?? 'active';
const local = localParamBool ?? params?.local ?? true;
const handlePin = useCallback(() => { const handlePin = useCallback(() => {
if (columnId) { if (columnId) {
@ -101,10 +104,10 @@ export const Directory: React.FC<{
if (columnId) { if (columnId) {
dispatch(changeColumnParams(columnId, ['order'], e.target.value)); dispatch(changeColumnParams(columnId, ['order'], e.target.value));
} else { } else {
setState((s) => ({ order: e.target.value, local: s.local })); setOrderParam(e.target.value);
} }
}, },
[dispatch, columnId], [dispatch, columnId, setOrderParam],
); );
const handleChangeLocal = useCallback<ChangeEventHandler<HTMLInputElement>>( const handleChangeLocal = useCallback<ChangeEventHandler<HTMLInputElement>>(
@ -113,11 +116,13 @@ export const Directory: React.FC<{
dispatch( dispatch(
changeColumnParams(columnId, ['local'], e.target.value === '1'), changeColumnParams(columnId, ['local'], e.target.value === '1'),
); );
} else if (e.target.value === '1') {
setLocalParam('true');
} else { } else {
setState((s) => ({ local: e.target.value === '1', order: s.order })); setLocalParam('false');
} }
}, },
[dispatch, columnId], [dispatch, columnId, setLocalParam],
); );
const handleLoadMore = useCallback(() => { const handleLoadMore = useCallback(() => {

View file

@ -4,8 +4,6 @@ import { Children, cloneElement, useCallback } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { supportsPassiveEvents } from 'detect-passive-events';
import { scrollRight } from '../../../scroll'; import { scrollRight } from '../../../scroll';
import BundleContainer from '../containers/bundle_container'; import BundleContainer from '../containers/bundle_container';
import { import {
@ -71,10 +69,6 @@ export default class ColumnsArea extends ImmutablePureComponent {
}; };
componentDidMount() { componentDidMount() {
if (!this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
}
if (this.mediaQuery) { if (this.mediaQuery) {
if (this.mediaQuery.addEventListener) { if (this.mediaQuery.addEventListener) {
this.mediaQuery.addEventListener('change', this.handleLayoutChange); this.mediaQuery.addEventListener('change', this.handleLayoutChange);
@ -87,23 +81,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl'); this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
} }
UNSAFE_componentWillUpdate(nextProps) {
if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
}
componentDidUpdate(prevProps) {
if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
}
}
componentWillUnmount () { componentWillUnmount () {
if (!this.props.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
if (this.mediaQuery) { if (this.mediaQuery) {
if (this.mediaQuery.removeEventListener) { if (this.mediaQuery.removeEventListener) {
this.mediaQuery.removeEventListener('change', this.handleLayoutChange); this.mediaQuery.removeEventListener('change', this.handleLayoutChange);
@ -116,7 +94,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
handleChildrenContentChange() { handleChildrenContentChange() {
if (!this.props.singleColumn) { if (!this.props.singleColumn) {
const modifier = this.isRtlLayout ? -1 : 1; const modifier = this.isRtlLayout ? -1 : 1;
this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier); scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
} }
} }
@ -124,14 +102,6 @@ export default class ColumnsArea extends ImmutablePureComponent {
this.setState({ renderComposePanel: !e.matches }); this.setState({ renderComposePanel: !e.matches });
}; };
handleWheel = () => {
if (typeof this._interruptScrollAnimation !== 'function') {
return;
}
this._interruptScrollAnimation();
};
setRef = (node) => { setRef = (node) => {
this.node = node; this.node = node;
}; };

View file

@ -17,7 +17,7 @@ export default class ImageLoader extends PureComponent {
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
onClick: PropTypes.func, onClick: PropTypes.func,
zoomButtonHidden: PropTypes.bool, zoomedIn: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -134,7 +134,7 @@ export default class ImageLoader extends PureComponent {
}; };
render () { render () {
const { alt, lang, src, width, height, onClick } = this.props; const { alt, lang, src, width, height, onClick, zoomedIn } = this.props;
const { loading } = this.state; const { loading } = this.state;
const className = classNames('image-loader', { const className = classNames('image-loader', {
@ -149,6 +149,7 @@ export default class ImageLoader extends PureComponent {
<div className='loading-bar__container' style={{ width: this.state.width || width }}> <div className='loading-bar__container' style={{ width: this.state.width || width }}>
<LoadingBar className='loading-bar' loading={1} /> <LoadingBar className='loading-bar' loading={1} />
</div> </div>
<canvas <canvas
className='image-loader__preview-canvas' className='image-loader__preview-canvas'
ref={this.setCanvasRef} ref={this.setCanvasRef}
@ -164,7 +165,7 @@ export default class ImageLoader extends PureComponent {
onClick={onClick} onClick={onClick}
width={width} width={width}
height={height} height={height}
zoomButtonHidden={this.props.zoomButtonHidden} zoomedIn={zoomedIn}
/> />
)} )}
</div> </div>

View file

@ -12,6 +12,8 @@ import ReactSwipeableViews from 'react-swipeable-views';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import FitScreenIcon from '@/material-icons/400-24px/fit_screen.svg?react';
import ActualSizeIcon from '@/svg-icons/actual_size.svg?react';
import { getAverageFromBlurhash } from 'mastodon/blurhash'; import { getAverageFromBlurhash } from 'mastodon/blurhash';
import { GIFV } from 'mastodon/components/gifv'; import { GIFV } from 'mastodon/components/gifv';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
@ -26,6 +28,8 @@ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' }, close: { id: 'lightbox.close', defaultMessage: 'Close' },
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' }, next: { id: 'lightbox.next', defaultMessage: 'Next' },
zoomIn: { id: 'lightbox.zoom_in', defaultMessage: 'Zoom to actual size' },
zoomOut: { id: 'lightbox.zoom_out', defaultMessage: 'Zoom to fit' },
}); });
class MediaModal extends ImmutablePureComponent { class MediaModal extends ImmutablePureComponent {
@ -46,30 +50,39 @@ class MediaModal extends ImmutablePureComponent {
state = { state = {
index: null, index: null,
navigationHidden: false, navigationHidden: false,
zoomButtonHidden: false, zoomedIn: false,
};
handleZoomClick = () => {
this.setState(prevState => ({
zoomedIn: !prevState.zoomedIn,
}));
}; };
handleSwipe = (index) => { handleSwipe = (index) => {
this.setState({ index: index % this.props.media.size }); this.setState({
index: index % this.props.media.size,
zoomedIn: false,
});
}; };
handleTransitionEnd = () => { handleTransitionEnd = () => {
this.setState({ this.setState({
zoomButtonHidden: false, zoomedIn: false,
}); });
}; };
handleNextClick = () => { handleNextClick = () => {
this.setState({ this.setState({
index: (this.getIndex() + 1) % this.props.media.size, index: (this.getIndex() + 1) % this.props.media.size,
zoomButtonHidden: true, zoomedIn: false,
}); });
}; };
handlePrevClick = () => { handlePrevClick = () => {
this.setState({ this.setState({
index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size, index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size,
zoomButtonHidden: true, zoomedIn: false,
}); });
}; };
@ -78,7 +91,7 @@ class MediaModal extends ImmutablePureComponent {
this.setState({ this.setState({
index: index % this.props.media.size, index: index % this.props.media.size,
zoomButtonHidden: true, zoomedIn: false,
}); });
}; };
@ -130,15 +143,22 @@ class MediaModal extends ImmutablePureComponent {
return this.state.index !== null ? this.state.index : this.props.index; return this.state.index !== null ? this.state.index : this.props.index;
} }
toggleNavigation = () => { handleToggleNavigation = () => {
this.setState(prevState => ({ this.setState(prevState => ({
navigationHidden: !prevState.navigationHidden, navigationHidden: !prevState.navigationHidden,
})); }));
}; };
setRef = c => {
this.setState({
viewportWidth: c?.clientWidth,
viewportHeight: c?.clientHeight,
});
};
render () { render () {
const { media, statusId, lang, intl, onClose } = this.props; const { media, statusId, lang, intl, onClose } = this.props;
const { navigationHidden } = this.state; const { navigationHidden, zoomedIn, viewportWidth, viewportHeight } = this.state;
const index = this.getIndex(); const index = this.getIndex();
@ -160,8 +180,8 @@ class MediaModal extends ImmutablePureComponent {
alt={description} alt={description}
lang={lang} lang={lang}
key={image.get('url')} key={image.get('url')}
onClick={this.toggleNavigation} onClick={this.handleToggleNavigation}
zoomButtonHidden={this.state.zoomButtonHidden} zoomedIn={zoomedIn}
/> />
); );
} else if (image.get('type') === 'video') { } else if (image.get('type') === 'video') {
@ -230,8 +250,11 @@ class MediaModal extends ImmutablePureComponent {
)); ));
} }
const currentMedia = media.get(index);
const zoomable = currentMedia.get('type') === 'image' && (currentMedia.getIn(['meta', 'original', 'width']) > viewportWidth || currentMedia.getIn(['meta', 'original', 'height']) > viewportHeight);
return ( return (
<div className='modal-root__modal media-modal'> <div className='modal-root__modal media-modal' ref={this.setRef}>
<div className='media-modal__closer' role='presentation' onClick={onClose}> <div className='media-modal__closer' role='presentation' onClick={onClose}>
<ReactSwipeableViews <ReactSwipeableViews
style={swipeableViewsStyle} style={swipeableViewsStyle}
@ -246,7 +269,10 @@ class MediaModal extends ImmutablePureComponent {
</div> </div>
<div className={navigationClassName}> <div className={navigationClassName}>
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} size={40} /> <div className='media-modal__buttons'>
{zoomable && <IconButton title={intl.formatMessage(zoomedIn ? messages.zoomOut : messages.zoomIn)} iconComponent={zoomedIn ? FitScreenIcon : ActualSizeIcon} onClick={this.handleZoomClick} />}
<IconButton title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} />
</div>
{leftNav} {leftNav}
{rightNav} {rightNav}

View file

@ -1,17 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import FullscreenExitIcon from '@/material-icons/400-24px/fullscreen_exit.svg?react';
import RectangleIcon from '@/material-icons/400-24px/rectangle.svg?react';
import { IconButton } from 'mastodon/components/icon_button';
const messages = defineMessages({
compress: { id: 'lightbox.compress', defaultMessage: 'Compress image view box' },
expand: { id: 'lightbox.expand', defaultMessage: 'Expand image view box' },
});
const MIN_SCALE = 1; const MIN_SCALE = 1;
const MAX_SCALE = 4; const MAX_SCALE = 4;
const NAV_BAR_HEIGHT = 66; const NAV_BAR_HEIGHT = 66;
@ -104,8 +93,7 @@ class ZoomableImage extends PureComponent {
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
onClick: PropTypes.func, onClick: PropTypes.func,
zoomButtonHidden: PropTypes.bool, zoomedIn: PropTypes.bool,
intl: PropTypes.object.isRequired,
}; };
static defaultProps = { static defaultProps = {
@ -131,8 +119,6 @@ class ZoomableImage extends PureComponent {
translateX: null, translateX: null,
translateY: null, translateY: null,
}, },
zoomState: 'expand', // 'expand' 'compress'
navigationHidden: false,
dragPosition: { top: 0, left: 0, x: 0, y: 0 }, dragPosition: { top: 0, left: 0, x: 0, y: 0 },
dragged: false, dragged: false,
lockScroll: { x: 0, y: 0 }, lockScroll: { x: 0, y: 0 },
@ -169,35 +155,20 @@ class ZoomableImage extends PureComponent {
this.container.addEventListener('DOMMouseScroll', handler); this.container.addEventListener('DOMMouseScroll', handler);
this.removers.push(() => this.container.removeEventListener('DOMMouseScroll', handler)); this.removers.push(() => this.container.removeEventListener('DOMMouseScroll', handler));
this.initZoomMatrix(); this._initZoomMatrix();
} }
componentWillUnmount () { componentWillUnmount () {
this.removeEventListeners(); this._removeEventListeners();
} }
componentDidUpdate () { componentDidUpdate (prevProps) {
this.setState({ zoomState: this.state.scale >= this.state.zoomMatrix.rate ? 'compress' : 'expand' }); if (prevProps.zoomedIn !== this.props.zoomedIn) {
this._toggleZoom();
if (this.state.scale === MIN_SCALE) {
this.container.style.removeProperty('cursor');
} }
} }
UNSAFE_componentWillReceiveProps () { _removeEventListeners () {
// reset when slide to next image
if (this.props.zoomButtonHidden) {
this.setState({
scale: MIN_SCALE,
lockTranslate: { x: 0, y: 0 },
}, () => {
this.container.scrollLeft = 0;
this.container.scrollTop = 0;
});
}
}
removeEventListeners () {
this.removers.forEach(listeners => listeners()); this.removers.forEach(listeners => listeners());
this.removers = []; this.removers = [];
} }
@ -220,9 +191,6 @@ class ZoomableImage extends PureComponent {
}; };
mouseDownHandler = e => { mouseDownHandler = e => {
this.container.style.cursor = 'grabbing';
this.container.style.userSelect = 'none';
this.setState({ dragPosition: { this.setState({ dragPosition: {
left: this.container.scrollLeft, left: this.container.scrollLeft,
top: this.container.scrollTop, top: this.container.scrollTop,
@ -246,9 +214,6 @@ class ZoomableImage extends PureComponent {
}; };
mouseUpHandler = () => { mouseUpHandler = () => {
this.container.style.cursor = 'grab';
this.container.style.removeProperty('user-select');
this.image.removeEventListener('mousemove', this.mouseMoveHandler); this.image.removeEventListener('mousemove', this.mouseMoveHandler);
this.image.removeEventListener('mouseup', this.mouseUpHandler); this.image.removeEventListener('mouseup', this.mouseUpHandler);
}; };
@ -276,13 +241,13 @@ class ZoomableImage extends PureComponent {
const _MAX_SCALE = Math.max(MAX_SCALE, this.state.zoomMatrix.rate); const _MAX_SCALE = Math.max(MAX_SCALE, this.state.zoomMatrix.rate);
const scale = clamp(MIN_SCALE, _MAX_SCALE, this.state.scale * distance / this.lastDistance); const scale = clamp(MIN_SCALE, _MAX_SCALE, this.state.scale * distance / this.lastDistance);
this.zoom(scale, midpoint); this._zoom(scale, midpoint);
this.lastMidpoint = midpoint; this.lastMidpoint = midpoint;
this.lastDistance = distance; this.lastDistance = distance;
}; };
zoom(nextScale, midpoint) { _zoom(nextScale, midpoint) {
const { scale, zoomMatrix } = this.state; const { scale, zoomMatrix } = this.state;
const { scrollLeft, scrollTop } = this.container; const { scrollLeft, scrollTop } = this.container;
@ -318,14 +283,13 @@ class ZoomableImage extends PureComponent {
if (dragged) return; if (dragged) return;
const handler = this.props.onClick; const handler = this.props.onClick;
if (handler) handler(); if (handler) handler();
this.setState({ navigationHidden: !this.state.navigationHidden });
}; };
handleMouseDown = e => { handleMouseDown = e => {
e.preventDefault(); e.preventDefault();
}; };
initZoomMatrix = () => { _initZoomMatrix = () => {
const { width, height } = this.props; const { width, height } = this.props;
const { clientWidth, clientHeight } = this.container; const { clientWidth, clientHeight } = this.container;
const { offsetWidth, offsetHeight } = this.image; const { offsetWidth, offsetHeight } = this.image;
@ -357,10 +321,7 @@ class ZoomableImage extends PureComponent {
}); });
}; };
handleZoomClick = e => { _toggleZoom () {
e.preventDefault();
e.stopPropagation();
const { scale, zoomMatrix } = this.state; const { scale, zoomMatrix } = this.state;
if ( scale >= zoomMatrix.rate ) { if ( scale >= zoomMatrix.rate ) {
@ -394,10 +355,7 @@ class ZoomableImage extends PureComponent {
this.container.scrollTop = zoomMatrix.scrollTop; this.container.scrollTop = zoomMatrix.scrollTop;
}); });
} }
}
this.container.style.cursor = 'grab';
this.container.style.removeProperty('user-select');
};
setContainerRef = c => { setContainerRef = c => {
this.container = c; this.container = c;
@ -408,29 +366,16 @@ class ZoomableImage extends PureComponent {
}; };
render () { render () {
const { alt, lang, src, width, height, intl } = this.props; const { alt, lang, src, width, height } = this.props;
const { scale, lockTranslate } = this.state; const { scale, lockTranslate, dragged } = this.state;
const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll'; const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll';
const zoomButtonShouldHide = this.state.navigationHidden || this.props.zoomButtonHidden || this.state.zoomMatrix.rate <= MIN_SCALE ? 'media-modal__zoom-button--hidden' : ''; const cursor = scale === MIN_SCALE ? null : (dragged ? 'grabbing' : 'grab');
const zoomButtonTitle = this.state.zoomState === 'compress' ? intl.formatMessage(messages.compress) : intl.formatMessage(messages.expand);
return ( return (
<>
<IconButton
className={`media-modal__zoom-button ${zoomButtonShouldHide}`}
title={zoomButtonTitle}
icon={this.state.zoomState}
iconComponent={this.state.zoomState === 'compress' ? FullscreenExitIcon : RectangleIcon}
onClick={this.handleZoomClick}
size={40}
style={{
fontSize: '30px', /* Fontawesome's fa-compress fa-expand is larger than fa-close */
}}
/>
<div <div
className='zoomable-image' className='zoomable-image'
ref={this.setContainerRef} ref={this.setContainerRef}
style={{ overflow }} style={{ overflow, cursor, userSelect: 'none' }}
> >
<img <img
role='presentation' role='presentation'
@ -450,10 +395,8 @@ class ZoomableImage extends PureComponent {
onMouseDown={this.handleMouseDown} onMouseDown={this.handleMouseDown}
/> />
</div> </div>
</>
); );
} }
} }
export default injectIntl(ZoomableImage); export default ZoomableImage;

View file

@ -36,6 +36,7 @@
"account.followers.empty": "لا أحدَ يُتابع هذا المُستخدم إلى حد الآن.", "account.followers.empty": "لا أحدَ يُتابع هذا المُستخدم إلى حد الآن.",
"account.followers_counter": "{count, plural, zero{لا مُتابع} one {مُتابعٌ واحِد} two {مُتابعانِ اِثنان} few {{counter} مُتابِعين} many {{counter} مُتابِعًا} other {{counter} مُتابع}}", "account.followers_counter": "{count, plural, zero{لا مُتابع} one {مُتابعٌ واحِد} two {مُتابعانِ اِثنان} few {{counter} مُتابِعين} many {{counter} مُتابِعًا} other {{counter} مُتابع}}",
"account.following": "الاشتراكات", "account.following": "الاشتراكات",
"account.following_counter": "{count, plural, zero{لا يُتابِع أحدًا} one {يُتابِعُ واحد} two{يُتابِعُ اِثنان} few{يُتابِعُ {counter}} many{يُتابِعُ {counter}} other {يُتابِعُ {counter}}}",
"account.follows.empty": "لا يُتابع هذا المُستخدمُ أيَّ أحدٍ حتى الآن.", "account.follows.empty": "لا يُتابع هذا المُستخدمُ أيَّ أحدٍ حتى الآن.",
"account.go_to_profile": "اذهب إلى الملف الشخصي", "account.go_to_profile": "اذهب إلى الملف الشخصي",
"account.hide_reblogs": "إخفاء المعاد نشرها مِن @{name}", "account.hide_reblogs": "إخفاء المعاد نشرها مِن @{name}",
@ -309,7 +310,7 @@
"follow_request.authorize": "ترخيص", "follow_request.authorize": "ترخيص",
"follow_request.reject": "رفض", "follow_request.reject": "رفض",
"follow_requests.unlocked_explanation": "حتى وإن كان حسابك غير مقفل، يعتقد فريق {domain} أنك قد ترغب في مراجعة طلبات المتابعة من هذه الحسابات يدوياً.", "follow_requests.unlocked_explanation": "حتى وإن كان حسابك غير مقفل، يعتقد فريق {domain} أنك قد ترغب في مراجعة طلبات المتابعة من هذه الحسابات يدوياً.",
"follow_suggestions.curated_suggestion": "اختيار الموظفين", "follow_suggestions.curated_suggestion": "انتقاه الفريق",
"follow_suggestions.dismiss": "لا تُظهرها مجدّدًا", "follow_suggestions.dismiss": "لا تُظهرها مجدّدًا",
"follow_suggestions.featured_longer": "مختار يدوياً من قِبل فريق {domain}", "follow_suggestions.featured_longer": "مختار يدوياً من قِبل فريق {domain}",
"follow_suggestions.friends_of_friends_longer": "مشهور بين الأشخاص الذين تتابعهم", "follow_suggestions.friends_of_friends_longer": "مشهور بين الأشخاص الذين تتابعهم",
@ -752,7 +753,7 @@
"status.edit": "تعديل", "status.edit": "تعديل",
"status.edited": "آخر تعديل يوم {date}", "status.edited": "آخر تعديل يوم {date}",
"status.edited_x_times": "عُدّل {count, plural, zero {} one {مرةً واحدة} two {مرّتان} few {{count} مرات} many {{count} مرة} other {{count} مرة}}", "status.edited_x_times": "عُدّل {count, plural, zero {} one {مرةً واحدة} two {مرّتان} few {{count} مرات} many {{count} مرة} other {{count} مرة}}",
"status.embed": "الحصول على شفرة الإدماج", "status.embed": "الحصول على شيفرة الدمج",
"status.favourite": "فضّل", "status.favourite": "فضّل",
"status.favourites": "{count, plural, zero {}one {مفضلة واحدة} two {مفضلتان} few {# مفضلات} many {# مفضلات} other {# مفضلات}}", "status.favourites": "{count, plural, zero {}one {مفضلة واحدة} two {مفضلتان} few {# مفضلات} many {# مفضلات} other {# مفضلات}}",
"status.filter": "تصفية هذا المنشور", "status.filter": "تصفية هذا المنشور",
@ -773,7 +774,7 @@
"status.reblog": "إعادة النشر", "status.reblog": "إعادة النشر",
"status.reblog_private": "إعادة النشر إلى الجمهور الأصلي", "status.reblog_private": "إعادة النشر إلى الجمهور الأصلي",
"status.reblogged_by": "شارَكَه {name}", "status.reblogged_by": "شارَكَه {name}",
"status.reblogs": "{count, plural, one {تعزيز واحد} two {تعزيزتان} few {# تعزيزات} many {# تعزيزات} other {# تعزيزات}}", "status.reblogs": "{count, plural, one {إعادة نشر واحدة} two {معاد نشرها مرتان} few {# إعادات نشر} many {# إعادات نشر} other {# إعادة نشر}}",
"status.reblogs.empty": "لم يقم أي أحد بمشاركة هذا المنشور بعد. عندما يقوم أحدهم بذلك سوف يظهر هنا.", "status.reblogs.empty": "لم يقم أي أحد بمشاركة هذا المنشور بعد. عندما يقوم أحدهم بذلك سوف يظهر هنا.",
"status.redraft": "إزالة وإعادة الصياغة", "status.redraft": "إزالة وإعادة الصياغة",
"status.remove_bookmark": "احذفه مِن الفواصل المرجعية", "status.remove_bookmark": "احذفه مِن الفواصل المرجعية",

View file

@ -432,10 +432,10 @@
"keyboard_shortcuts.unfocus": "Unfocus compose textarea/search", "keyboard_shortcuts.unfocus": "Unfocus compose textarea/search",
"keyboard_shortcuts.up": "Move up in the list", "keyboard_shortcuts.up": "Move up in the list",
"lightbox.close": "Close", "lightbox.close": "Close",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
"lightbox.next": "Next", "lightbox.next": "Next",
"lightbox.previous": "Previous", "lightbox.previous": "Previous",
"lightbox.zoom_in": "Zoom to actual size",
"lightbox.zoom_out": "Zoom to fit",
"limited_account_hint.action": "Show profile anyway", "limited_account_hint.action": "Show profile anyway",
"limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.", "limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.",
"link_preview.author": "By {name}", "link_preview.author": "By {name}",

View file

@ -97,6 +97,8 @@
"block_modal.title": "Ĉu bloki uzanton?", "block_modal.title": "Ĉu bloki uzanton?",
"block_modal.you_wont_see_mentions": "Vi ne vidos afiŝojn, ke mencii ilin.", "block_modal.you_wont_see_mentions": "Vi ne vidos afiŝojn, ke mencii ilin.",
"boost_modal.combo": "Vi povas premi {combo} por preterpasi sekvafoje", "boost_modal.combo": "Vi povas premi {combo} por preterpasi sekvafoje",
"boost_modal.reblog": "Ĉu diskonigi afiŝon?",
"boost_modal.undo_reblog": "Ĉu ĉesi diskonigi afiŝon?",
"bundle_column_error.copy_stacktrace": "Kopii la eraran raporton", "bundle_column_error.copy_stacktrace": "Kopii la eraran raporton",
"bundle_column_error.error.body": "La petita paĝo ne povas redonitis. Eble estas eraro.", "bundle_column_error.error.body": "La petita paĝo ne povas redonitis. Eble estas eraro.",
"bundle_column_error.error.title": "Ho, ve!", "bundle_column_error.error.title": "Ho, ve!",
@ -188,8 +190,12 @@
"confirmations.redraft.title": "Ĉu forigi kaj redakcii afiŝon?", "confirmations.redraft.title": "Ĉu forigi kaj redakcii afiŝon?",
"confirmations.reply.confirm": "Respondi", "confirmations.reply.confirm": "Respondi",
"confirmations.reply.message": "Respondi nun anstataŭigos la skribatan afiŝon. Ĉu vi certas, ke vi volas daŭrigi?", "confirmations.reply.message": "Respondi nun anstataŭigos la skribatan afiŝon. Ĉu vi certas, ke vi volas daŭrigi?",
"confirmations.reply.title": "Ĉu superskribi afiŝon?",
"confirmations.unfollow.confirm": "Ne plu sekvi", "confirmations.unfollow.confirm": "Ne plu sekvi",
"confirmations.unfollow.message": "Ĉu vi certas, ke vi volas ĉesi sekvi {name}?", "confirmations.unfollow.message": "Ĉu vi certas, ke vi volas ĉesi sekvi {name}?",
"confirmations.unfollow.title": "Ĉu ĉesi sekvi uzanton?",
"content_warning.hide": "Kaŝi afiŝon",
"content_warning.show": "Montri ĉiukaze",
"conversation.delete": "Forigi konversacion", "conversation.delete": "Forigi konversacion",
"conversation.mark_as_read": "Marki legita", "conversation.mark_as_read": "Marki legita",
"conversation.open": "Vidi konversacion", "conversation.open": "Vidi konversacion",
@ -209,6 +215,8 @@
"dismissable_banner.explore_statuses": "Ĉi tioj estas afiŝoj de socia reto kiu populariĝas hodiau.", "dismissable_banner.explore_statuses": "Ĉi tioj estas afiŝoj de socia reto kiu populariĝas hodiau.",
"dismissable_banner.explore_tags": "Ĉi tiuj kradvostoj populariĝas en ĉi tiu kaj aliaj serviloj en la malcentraliza reto nun.", "dismissable_banner.explore_tags": "Ĉi tiuj kradvostoj populariĝas en ĉi tiu kaj aliaj serviloj en la malcentraliza reto nun.",
"dismissable_banner.public_timeline": "Ĉi tioj estas plej lastaj publikaj afiŝoj de personoj ĉe socia reto kiu personoj ĉe {domain} sekvas.", "dismissable_banner.public_timeline": "Ĉi tioj estas plej lastaj publikaj afiŝoj de personoj ĉe socia reto kiu personoj ĉe {domain} sekvas.",
"domain_block_modal.they_cant_follow": "Neniu el ĉi tiu servilo povas sekvi vin.",
"domain_pill.username": "Uzantnomo",
"embed.instructions": "Enkorpigu ĉi tiun afiŝon en vian retejon per kopio de la suba kodo.", "embed.instructions": "Enkorpigu ĉi tiun afiŝon en vian retejon per kopio de la suba kodo.",
"embed.preview": "Ĝi aperos tiel:", "embed.preview": "Ĝi aperos tiel:",
"emoji_button.activity": "Agadoj", "emoji_button.activity": "Agadoj",
@ -281,6 +289,12 @@
"follow_request.authorize": "Rajtigi", "follow_request.authorize": "Rajtigi",
"follow_request.reject": "Rifuzi", "follow_request.reject": "Rifuzi",
"follow_requests.unlocked_explanation": "Kvankam via konto ne estas ŝlosita, la dungitaro de {domain} opinias, ke vi eble volas revizii petojn pri sekvado de ĉi tiuj kontoj permane.", "follow_requests.unlocked_explanation": "Kvankam via konto ne estas ŝlosita, la dungitaro de {domain} opinias, ke vi eble volas revizii petojn pri sekvado de ĉi tiuj kontoj permane.",
"follow_suggestions.dismiss": "Ne montri denove",
"follow_suggestions.hints.friends_of_friends": "Ĉi tiu profilo estas populara inter la homoj, kiujn vi sekvas.",
"follow_suggestions.hints.most_followed": "Ĉi tiu profilo estas unu el la plej sekvataj en {domain}.",
"follow_suggestions.popular_suggestion_longer": "Populara en {domain}",
"follow_suggestions.view_all": "Vidi ĉiujn",
"follow_suggestions.who_to_follow": "Kiun sekvi",
"followed_tags": "Sekvataj kradvortoj", "followed_tags": "Sekvataj kradvortoj",
"footer.about": "Pri", "footer.about": "Pri",
"footer.directory": "Profilujo", "footer.directory": "Profilujo",
@ -374,6 +388,7 @@
"limited_account_hint.action": "Montru profilon ĉiukaze", "limited_account_hint.action": "Montru profilon ĉiukaze",
"limited_account_hint.title": "La profilo estas kaŝita de la moderigantoj de {domain}.", "limited_account_hint.title": "La profilo estas kaŝita de la moderigantoj de {domain}.",
"link_preview.author": "De {name}", "link_preview.author": "De {name}",
"link_preview.shares": "{count, plural, one {{counter} afiŝo} other {{counter} afiŝoj}}",
"lists.account.add": "Aldoni al la listo", "lists.account.add": "Aldoni al la listo",
"lists.account.remove": "Forigi de la listo", "lists.account.remove": "Forigi de la listo",
"lists.delete": "Forigi la liston", "lists.delete": "Forigi la liston",
@ -390,8 +405,12 @@
"lists.subheading": "Viaj listoj", "lists.subheading": "Viaj listoj",
"load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}", "load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}",
"loading_indicator.label": "Ŝargado…", "loading_indicator.label": "Ŝargado…",
"media_gallery.hide": "Kaŝi",
"moved_to_account_banner.text": "Via konto {disabledAccount} estas malvalidigita ĉar vi movis ĝin al {movedToAccount}.", "moved_to_account_banner.text": "Via konto {disabledAccount} estas malvalidigita ĉar vi movis ĝin al {movedToAccount}.",
"mute_modal.show_options": "Montri agordojn",
"mute_modal.they_can_mention_and_follow": "Ili povas mencii kaj sekvi vin, sed vi ne vidos ilin.",
"navigation_bar.about": "Pri", "navigation_bar.about": "Pri",
"navigation_bar.administration": "Administrado",
"navigation_bar.advanced_interface": "Malfermi altnivelan retpaĝan interfacon", "navigation_bar.advanced_interface": "Malfermi altnivelan retpaĝan interfacon",
"navigation_bar.blocks": "Blokitaj uzantoj", "navigation_bar.blocks": "Blokitaj uzantoj",
"navigation_bar.bookmarks": "Legosignoj", "navigation_bar.bookmarks": "Legosignoj",
@ -422,10 +441,18 @@
"notification.favourite": "{name} stelumis vian afiŝon", "notification.favourite": "{name} stelumis vian afiŝon",
"notification.follow": "{name} eksekvis vin", "notification.follow": "{name} eksekvis vin",
"notification.follow_request": "{name} petis sekvi vin", "notification.follow_request": "{name} petis sekvi vin",
"notification.label.mention": "Mencii",
"notification.label.private_mention": "Privata mencio",
"notification.label.private_reply": "Privata respondo",
"notification.label.reply": "Respondi",
"notification.mention": "Mencii",
"notification.moderation-warning.learn_more": "Lerni pli",
"notification.own_poll": "Via enketo finiĝis", "notification.own_poll": "Via enketo finiĝis",
"notification.reblog": "{name} diskonigis vian afiŝon", "notification.reblog": "{name} diskonigis vian afiŝon",
"notification.relationships_severance_event.learn_more": "Lerni pli",
"notification.status": "{name} ĵus afiŝis", "notification.status": "{name} ĵus afiŝis",
"notification.update": "{name} redaktis afiŝon", "notification.update": "{name} redaktis afiŝon",
"notification_requests.accept": "Akcepti",
"notifications.clear": "Forviŝi sciigojn", "notifications.clear": "Forviŝi sciigojn",
"notifications.clear_confirmation": "Ĉu vi certas, ke vi volas porĉiame forviŝi ĉiujn viajn sciigojn?", "notifications.clear_confirmation": "Ĉu vi certas, ke vi volas porĉiame forviŝi ĉiujn viajn sciigojn?",
"notifications.column_settings.admin.report": "Novaj raportoj:", "notifications.column_settings.admin.report": "Novaj raportoj:",
@ -457,6 +484,8 @@
"notifications.permission_denied": "Labortablaj sciigoj ne disponeblas pro peto antaŭe rifuzita de retumiloj", "notifications.permission_denied": "Labortablaj sciigoj ne disponeblas pro peto antaŭe rifuzita de retumiloj",
"notifications.permission_denied_alert": "Labortablaj sciigoj ne povas esti ebligitaj, ĉar retumilpermeso antaŭe estis rifuzita", "notifications.permission_denied_alert": "Labortablaj sciigoj ne povas esti ebligitaj, ĉar retumilpermeso antaŭe estis rifuzita",
"notifications.permission_required": "Labortablaj sciigoj ne disponeblas ĉar la bezonata permeso ne estis donita.", "notifications.permission_required": "Labortablaj sciigoj ne disponeblas ĉar la bezonata permeso ne estis donita.",
"notifications.policy.accept": "Akcepti",
"notifications.policy.filter_new_accounts_title": "Novaj kontoj",
"notifications_permission_banner.enable": "Ŝalti retumilajn sciigojn", "notifications_permission_banner.enable": "Ŝalti retumilajn sciigojn",
"notifications_permission_banner.how_to_control": "Por ricevi sciigojn kiam Mastodon ne estas malfermita, ebligu labortablajn sciigojn. Vi povas regi precize kiuj specoj de interagoj generas labortablajn sciigojn per la supra butono {icon} post kiam ili estas ebligitaj.", "notifications_permission_banner.how_to_control": "Por ricevi sciigojn kiam Mastodon ne estas malfermita, ebligu labortablajn sciigojn. Vi povas regi precize kiuj specoj de interagoj generas labortablajn sciigojn per la supra butono {icon} post kiam ili estas ebligitaj.",
"notifications_permission_banner.title": "Neniam preterlasas iun ajn", "notifications_permission_banner.title": "Neniam preterlasas iun ajn",
@ -581,6 +610,7 @@
"report_notification.attached_statuses": "{count, plural, one {{count} afiŝo almetita} other {{count} afiŝoj almetitaj}}", "report_notification.attached_statuses": "{count, plural, one {{count} afiŝo almetita} other {{count} afiŝoj almetitaj}}",
"report_notification.categories.legal": "Laŭleĝa", "report_notification.categories.legal": "Laŭleĝa",
"report_notification.categories.other": "Alia", "report_notification.categories.other": "Alia",
"report_notification.categories.other_sentence": "alia",
"report_notification.categories.spam": "Trudmesaĝo", "report_notification.categories.spam": "Trudmesaĝo",
"report_notification.categories.violation": "Malobservo de la regulo", "report_notification.categories.violation": "Malobservo de la regulo",
"report_notification.open": "Malfermi la raporton", "report_notification.open": "Malfermi la raporton",

View file

@ -39,11 +39,11 @@
"account.following_counter": "{count, plural, one {{counter} siguiendo} other {{counter} siguiendo}}", "account.following_counter": "{count, plural, one {{counter} siguiendo} other {{counter} siguiendo}}",
"account.follows.empty": "Este usuario todavía no sigue a nadie.", "account.follows.empty": "Este usuario todavía no sigue a nadie.",
"account.go_to_profile": "Ir al perfil", "account.go_to_profile": "Ir al perfil",
"account.hide_reblogs": "Ocultar retoots de @{name}", "account.hide_reblogs": "Ocultar impulsos de @{name}",
"account.in_memoriam": "En memoria.", "account.in_memoriam": "En memoria.",
"account.joined_short": "Se unió", "account.joined_short": "Se unió",
"account.languages": "Cambiar idiomas suscritos", "account.languages": "Cambiar idiomas suscritos",
"account.link_verified_on": "El proprietario de este link fue comprobado el {date}", "account.link_verified_on": "El proprietario de este enlace fue comprobado el {date}",
"account.locked_info": "El estado de privacidad de esta cuenta està configurado como bloqueado. El proprietario debe revisar manualmente quien puede seguirle.", "account.locked_info": "El estado de privacidad de esta cuenta està configurado como bloqueado. El proprietario debe revisar manualmente quien puede seguirle.",
"account.media": "Multimedia", "account.media": "Multimedia",
"account.mention": "Mencionar a @{name}", "account.mention": "Mencionar a @{name}",
@ -61,7 +61,7 @@
"account.requested": "Esperando aprobación. Haga clic para cancelar la solicitud de seguimiento", "account.requested": "Esperando aprobación. Haga clic para cancelar la solicitud de seguimiento",
"account.requested_follow": "{name} ha solicitado seguirte", "account.requested_follow": "{name} ha solicitado seguirte",
"account.share": "Compartir el perfil de @{name}", "account.share": "Compartir el perfil de @{name}",
"account.show_reblogs": "Mostrar retoots de @{name}", "account.show_reblogs": "Mostrar impulsos de @{name}",
"account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}", "account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
"account.unblock": "Desbloquear a @{name}", "account.unblock": "Desbloquear a @{name}",
"account.unblock_domain": "Mostrar a {domain}", "account.unblock_domain": "Mostrar a {domain}",
@ -70,8 +70,8 @@
"account.unfollow": "Dejar de seguir", "account.unfollow": "Dejar de seguir",
"account.unmute": "Dejar de silenciar a @{name}", "account.unmute": "Dejar de silenciar a @{name}",
"account.unmute_notifications_short": "Dejar de silenciar notificaciones", "account.unmute_notifications_short": "Dejar de silenciar notificaciones",
"account.unmute_short": "Desmutear", "account.unmute_short": "Dejar de silenciar",
"account_note.placeholder": "Clic para añadir nota", "account_note.placeholder": "Haz clic para agregar una nota",
"admin.dashboard.daily_retention": "Tasa de retención de usuarios por día después de unirse", "admin.dashboard.daily_retention": "Tasa de retención de usuarios por día después de unirse",
"admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes después de unirse", "admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes después de unirse",
"admin.dashboard.retention.average": "Promedio", "admin.dashboard.retention.average": "Promedio",
@ -97,7 +97,7 @@
"block_modal.title": "¿Bloquear usuario?", "block_modal.title": "¿Bloquear usuario?",
"block_modal.you_wont_see_mentions": "No verás publicaciones que los mencionen.", "block_modal.you_wont_see_mentions": "No verás publicaciones que los mencionen.",
"boost_modal.combo": "Puedes hacer clic en {combo} para saltar este aviso la próxima vez", "boost_modal.combo": "Puedes hacer clic en {combo} para saltar este aviso la próxima vez",
"boost_modal.reblog": "¿Impulsar la publicación?", "boost_modal.reblog": "¿Deseas impulsar la publicación?",
"boost_modal.undo_reblog": "¿Dejar de impulsar la publicación?", "boost_modal.undo_reblog": "¿Dejar de impulsar la publicación?",
"bundle_column_error.copy_stacktrace": "Copiar informe de error", "bundle_column_error.copy_stacktrace": "Copiar informe de error",
"bundle_column_error.error.body": "La página solicitada no pudo ser renderizada. Podría deberse a un error en nuestro código o a un problema de compatibilidad con el navegador.", "bundle_column_error.error.body": "La página solicitada no pudo ser renderizada. Podría deberse a un error en nuestro código o a un problema de compatibilidad con el navegador.",
@ -130,7 +130,7 @@
"column.lists": "Listas", "column.lists": "Listas",
"column.mutes": "Usuarios silenciados", "column.mutes": "Usuarios silenciados",
"column.notifications": "Notificaciones", "column.notifications": "Notificaciones",
"column.pins": "Toots fijados", "column.pins": "Publicaciones fijadas",
"column.public": "Línea de tiempo federada", "column.public": "Línea de tiempo federada",
"column_back_button.label": "Atrás", "column_back_button.label": "Atrás",
"column_header.hide_settings": "Ocultar configuración", "column_header.hide_settings": "Ocultar configuración",
@ -148,10 +148,10 @@
"compose.published.body": "Publicado.", "compose.published.body": "Publicado.",
"compose.published.open": "Abrir", "compose.published.open": "Abrir",
"compose.saved.body": "Publicación guardada.", "compose.saved.body": "Publicación guardada.",
"compose_form.direct_message_warning_learn_more": "Aprender mas", "compose_form.direct_message_warning_learn_more": "Saber más",
"compose_form.encryption_warning": "Las publicaciones en Mastodon no están cifradas de extremo a extremo. No comparta ninguna información sensible en Mastodon.", "compose_form.encryption_warning": "Las publicaciones en Mastodon no están cifradas de extremo a extremo. No comparta ninguna información sensible en Mastodon.",
"compose_form.hashtag_warning": "Este toot no será listado bajo ningún hashtag dado que no es público. Solo toots públicos pueden ser buscados por hashtag.", "compose_form.hashtag_warning": "Esta publicación no será listada bajo ninguna etiqueta dado que no es pública. Solo publicaciones públicas pueden ser buscadas por etiqueta.",
"compose_form.lock_disclaimer": "Tu cuenta no está bloqueada. Todos pueden seguirte para ver tus toots solo para seguidores.", "compose_form.lock_disclaimer": "Tu cuenta no está {locked}. Todos pueden seguirte para ver tus publicaciones solo para seguidores.",
"compose_form.lock_disclaimer.lock": "bloqueado", "compose_form.lock_disclaimer.lock": "bloqueado",
"compose_form.placeholder": "¿En qué estás pensando?", "compose_form.placeholder": "¿En qué estás pensando?",
"compose_form.poll.duration": "Duración de la encuesta", "compose_form.poll.duration": "Duración de la encuesta",
@ -165,32 +165,32 @@
"compose_form.publish_form": "Publicar", "compose_form.publish_form": "Publicar",
"compose_form.reply": "Respuesta", "compose_form.reply": "Respuesta",
"compose_form.save_changes": "Actualización", "compose_form.save_changes": "Actualización",
"compose_form.spoiler.marked": "Texto oculto tras la advertencia", "compose_form.spoiler.marked": "Quitar advertencia de contenido",
"compose_form.spoiler.unmarked": "Texto no oculto", "compose_form.spoiler.unmarked": "Añadir advertencia de contenido",
"compose_form.spoiler_placeholder": "Advertencia de contenido (opcional)", "compose_form.spoiler_placeholder": "Advertencia de contenido (opcional)",
"confirmation_modal.cancel": "Cancelar", "confirmation_modal.cancel": "Cancelar",
"confirmations.block.confirm": "Bloquear", "confirmations.block.confirm": "Bloquear",
"confirmations.delete.confirm": "Eliminar", "confirmations.delete.confirm": "Eliminar",
"confirmations.delete.message": "¿Estás seguro de que quieres borrar este toot?", "confirmations.delete.message": "¿Estás seguro de que quieres borrar esta publicación?",
"confirmations.delete.title": "¿Eliminar publicación?", "confirmations.delete.title": "¿Eliminar publicación?",
"confirmations.delete_list.confirm": "Eliminar", "confirmations.delete_list.confirm": "Eliminar",
"confirmations.delete_list.message": "¿Seguro que quieres borrar esta lista permanentemente?", "confirmations.delete_list.message": "¿Seguro que quieres borrar esta lista permanentemente?",
"confirmations.delete_list.title": "¿Eliminar lista?", "confirmations.delete_list.title": "¿Deseas eliminar la lista?",
"confirmations.discard_edit_media.confirm": "Descartar", "confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Tienes cambios sin guardar en la descripción o vista previa del archivo, ¿deseas descartarlos de cualquier manera?", "confirmations.discard_edit_media.message": "Tienes cambios sin guardar en la descripción o vista previa del archivo, ¿deseas descartarlos de cualquier manera?",
"confirmations.edit.confirm": "Editar", "confirmations.edit.confirm": "Editar",
"confirmations.edit.message": "Editar sobrescribirá el mensaje que estás escribiendo. ¿Estás seguro de que deseas continuar?", "confirmations.edit.message": "Editar sobrescribirá el mensaje que estás escribiendo. ¿Estás seguro de que deseas continuar?",
"confirmations.edit.title": "¿Sobrescribir publicación?", "confirmations.edit.title": "¿Sobreescribir publicación?",
"confirmations.logout.confirm": "Cerrar sesión", "confirmations.logout.confirm": "Cerrar sesión",
"confirmations.logout.message": "¿Estás seguro de querer cerrar la sesión?", "confirmations.logout.message": "¿Estás seguro de que quieres cerrar la sesión?",
"confirmations.logout.title": "¿Cerrar sesión?", "confirmations.logout.title": "¿Deseas cerrar sesión?",
"confirmations.mute.confirm": "Silenciar", "confirmations.mute.confirm": "Silenciar",
"confirmations.redraft.confirm": "Borrar y volver a borrador", "confirmations.redraft.confirm": "Borrar y volver a borrador",
"confirmations.redraft.message": "¿Estás seguro que quieres borrar esta publicación y editarla? Los favoritos e impulsos se perderán, y las respuestas a la publicación original quedarán separadas.", "confirmations.redraft.message": "¿Estás seguro que quieres borrar esta publicación y editarla? Los favoritos e impulsos se perderán, y las respuestas a la publicación original quedarán separadas.",
"confirmations.redraft.title": "¿Borrar y volver a redactar la publicación?", "confirmations.redraft.title": "¿Borrar y volver a redactar la publicación?",
"confirmations.reply.confirm": "Responder", "confirmations.reply.confirm": "Responder",
"confirmations.reply.message": "Responder sobrescribirá el mensaje que estás escribiendo. ¿Estás seguro de que deseas continuar?", "confirmations.reply.message": "Responder sobrescribirá el mensaje que estás escribiendo. ¿Estás seguro de que deseas continuar?",
"confirmations.reply.title": "¿Sobrescribir publicación?", "confirmations.reply.title": "¿Sobreescribir publicación?",
"confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.confirm": "Dejar de seguir",
"confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?", "confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?",
"confirmations.unfollow.title": "¿Dejar de seguir al usuario?", "confirmations.unfollow.title": "¿Dejar de seguir al usuario?",
@ -213,8 +213,8 @@
"dismissable_banner.dismiss": "Descartar", "dismissable_banner.dismiss": "Descartar",
"dismissable_banner.explore_links": "Estas noticias están siendo discutidas por personas en este y otros servidores de la red descentralizada en este momento.", "dismissable_banner.explore_links": "Estas noticias están siendo discutidas por personas en este y otros servidores de la red descentralizada en este momento.",
"dismissable_banner.explore_statuses": "Estas son las publicaciones que están en tendencia en la red ahora. Las publicaciones recientes con más impulsos y favoritos se muestran más arriba.", "dismissable_banner.explore_statuses": "Estas son las publicaciones que están en tendencia en la red ahora. Las publicaciones recientes con más impulsos y favoritos se muestran más arriba.",
"dismissable_banner.explore_tags": "Se trata de hashtags que están ganando adeptos en las redes sociales hoy en día. Los hashtags que son utilizados por más personas diferentes se clasifican mejor.", "dismissable_banner.explore_tags": "Se trata de etiquetas que están ganando adeptos en las redes sociales hoy en día. Las etiquetas que son utilizadas por más personas diferentes se clasifican mejor.",
"dismissable_banner.public_timeline": "Estos son los toots públicos más recientes de personas en la web social a las que sigue la gente en {domain}.", "dismissable_banner.public_timeline": "Estas son las publicaciones públicas más recientes de personas en la web social a las que sigue la gente en {domain}.",
"domain_block_modal.block": "Bloquear servidor", "domain_block_modal.block": "Bloquear servidor",
"domain_block_modal.block_account_instead": "Bloquear @{name} en su lugar", "domain_block_modal.block_account_instead": "Bloquear @{name} en su lugar",
"domain_block_modal.they_can_interact_with_old_posts": "Las personas de este servidor pueden interactuar con tus publicaciones antiguas.", "domain_block_modal.they_can_interact_with_old_posts": "Las personas de este servidor pueden interactuar con tus publicaciones antiguas.",
@ -236,7 +236,7 @@
"domain_pill.your_handle": "Tu alias:", "domain_pill.your_handle": "Tu alias:",
"domain_pill.your_server": "Tu hogar digital, donde residen todas tus publicaciones. ¿No te gusta este sitio? Muévete a otro servidor en cualquier momento y llévate a tus seguidores.", "domain_pill.your_server": "Tu hogar digital, donde residen todas tus publicaciones. ¿No te gusta este sitio? Muévete a otro servidor en cualquier momento y llévate a tus seguidores.",
"domain_pill.your_username": "Tu identificador único en este servidor. Es posible encontrar usuarios con el mismo nombre de usuario en diferentes servidores.", "domain_pill.your_username": "Tu identificador único en este servidor. Es posible encontrar usuarios con el mismo nombre de usuario en diferentes servidores.",
"embed.instructions": "Añade este toot a tu sitio web con el siguiente código.", "embed.instructions": "Añade esta publicación a tu sitio web con el siguiente código.",
"embed.preview": "Así es como se verá:", "embed.preview": "Así es como se verá:",
"emoji_button.activity": "Actividad", "emoji_button.activity": "Actividad",
"emoji_button.clear": "Borrar", "emoji_button.clear": "Borrar",
@ -249,16 +249,16 @@
"emoji_button.objects": "Objetos", "emoji_button.objects": "Objetos",
"emoji_button.people": "Gente", "emoji_button.people": "Gente",
"emoji_button.recent": "Usados frecuentemente", "emoji_button.recent": "Usados frecuentemente",
"emoji_button.search": "Buscar", "emoji_button.search": "Buscar...",
"emoji_button.search_results": "Resultados de búsqueda", "emoji_button.search_results": "Resultados de búsqueda",
"emoji_button.symbols": "Símbolos", "emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viajes y lugares", "emoji_button.travel": "Viajes y lugares",
"empty_column.account_hides_collections": "Este usuario ha elegido no hacer disponible esta información", "empty_column.account_hides_collections": "Este usuario ha elegido no hacer disponible esta información",
"empty_column.account_suspended": "Cuenta suspendida", "empty_column.account_suspended": "Cuenta suspendida",
"empty_column.account_timeline": "¡No hay toots aquí!", "empty_column.account_timeline": "¡No hay publicaciones aquí!",
"empty_column.account_unavailable": "Perfil no disponible", "empty_column.account_unavailable": "Perfil no disponible",
"empty_column.blocks": "Aún no has bloqueado a ningún usuario.", "empty_column.blocks": "Aún no has bloqueado a ningún usuario.",
"empty_column.bookmarked_statuses": "Aún no tienes ningún toot guardado como marcador. Cuando guardes uno, se mostrará aquí.", "empty_column.bookmarked_statuses": "Aún no tienes ninguna publicación guardada como marcador. Cuando guardes una, se mostrará aquí.",
"empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!", "empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!",
"empty_column.direct": "Aún no tienes menciones privadas. Cuando envíes o recibas una, aparecerán aquí.", "empty_column.direct": "Aún no tienes menciones privadas. Cuando envíes o recibas una, aparecerán aquí.",
"empty_column.domain_blocks": "Todavía no hay dominios ocultos.", "empty_column.domain_blocks": "Todavía no hay dominios ocultos.",
@ -266,8 +266,8 @@
"empty_column.favourited_statuses": "Todavía no tienes publicaciones favoritas. Cuando le des favorito a una publicación se mostrarán acá.", "empty_column.favourited_statuses": "Todavía no tienes publicaciones favoritas. Cuando le des favorito a una publicación se mostrarán acá.",
"empty_column.favourites": "Todavía nadie marcó como favorito esta publicación. Cuando alguien lo haga, se mostrará aquí.", "empty_column.favourites": "Todavía nadie marcó como favorito esta publicación. Cuando alguien lo haga, se mostrará aquí.",
"empty_column.follow_requests": "No tienes ninguna petición de seguidor. Cuando recibas una, se mostrará aquí.", "empty_column.follow_requests": "No tienes ninguna petición de seguidor. Cuando recibas una, se mostrará aquí.",
"empty_column.followed_tags": "No estás siguiendo ningún hashtag todavía. Cuando lo hagas, aparecerá aquí.", "empty_column.followed_tags": "No estás siguiendo ninguna etiqueta todavía. Cuando lo hagas, aparecerá aquí.",
"empty_column.hashtag": "No hay nada en este hashtag aún.", "empty_column.hashtag": "No hay nada en esta etiqueta aún.",
"empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.", "empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.",
"empty_column.list": "No hay nada en esta lista aún. Cuando miembros de esta lista publiquen nuevos estatus, estos aparecerán qui.", "empty_column.list": "No hay nada en esta lista aún. Cuando miembros de esta lista publiquen nuevos estatus, estos aparecerán qui.",
"empty_column.lists": "No tienes ninguna lista. cuando crees una, se mostrará aquí.", "empty_column.lists": "No tienes ninguna lista. cuando crees una, se mostrará aquí.",
@ -304,7 +304,7 @@
"filter_modal.select_filter.title": "Filtrar esta publicación", "filter_modal.select_filter.title": "Filtrar esta publicación",
"filter_modal.title.status": "Filtrar una publicación", "filter_modal.title.status": "Filtrar una publicación",
"filter_warning.matches_filter": "Coincide con el filtro “{title}”", "filter_warning.matches_filter": "Coincide con el filtro “{title}”",
"filtered_notifications_banner.pending_requests": "De {count, plural, =0 {nadie} one {una persona} other {# personas}} que puede que conozcas", "filtered_notifications_banner.pending_requests": "De {count, plural, =0 {nadie} one {una persona} other {# people}} que puede que tú conozcas",
"filtered_notifications_banner.title": "Notificaciones filtradas", "filtered_notifications_banner.title": "Notificaciones filtradas",
"firehose.all": "Todas", "firehose.all": "Todas",
"firehose.local": "Este servidor", "firehose.local": "Este servidor",
@ -315,7 +315,7 @@
"follow_suggestions.curated_suggestion": "Recomendaciones del equipo", "follow_suggestions.curated_suggestion": "Recomendaciones del equipo",
"follow_suggestions.dismiss": "No mostrar de nuevo", "follow_suggestions.dismiss": "No mostrar de nuevo",
"follow_suggestions.featured_longer": "Escogidos por el equipo de {domain}", "follow_suggestions.featured_longer": "Escogidos por el equipo de {domain}",
"follow_suggestions.friends_of_friends_longer": "Populares entre las personas a las que sigues", "follow_suggestions.friends_of_friends_longer": "Popular entre las personas a las que sigues",
"follow_suggestions.hints.featured": "Este perfil ha sido seleccionado a mano por el equipo de {domain}.", "follow_suggestions.hints.featured": "Este perfil ha sido seleccionado a mano por el equipo de {domain}.",
"follow_suggestions.hints.friends_of_friends": "Este perfil es popular entre las personas que sigues.", "follow_suggestions.hints.friends_of_friends": "Este perfil es popular entre las personas que sigues.",
"follow_suggestions.hints.most_followed": "Este perfil es uno de los más seguidos en {domain}.", "follow_suggestions.hints.most_followed": "Este perfil es uno de los más seguidos en {domain}.",
@ -323,11 +323,11 @@
"follow_suggestions.hints.similar_to_recently_followed": "Este perfil es similar a los perfiles que has seguido recientemente.", "follow_suggestions.hints.similar_to_recently_followed": "Este perfil es similar a los perfiles que has seguido recientemente.",
"follow_suggestions.personalized_suggestion": "Sugerencia personalizada", "follow_suggestions.personalized_suggestion": "Sugerencia personalizada",
"follow_suggestions.popular_suggestion": "Sugerencia popular", "follow_suggestions.popular_suggestion": "Sugerencia popular",
"follow_suggestions.popular_suggestion_longer": "Populares en {domain}", "follow_suggestions.popular_suggestion_longer": "Popular en {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Similares a los perfiles que has seguido recientemente", "follow_suggestions.similar_to_recently_followed_longer": "Similares a los perfiles que has seguido recientemente",
"follow_suggestions.view_all": "Ver todo", "follow_suggestions.view_all": "Ver todo",
"follow_suggestions.who_to_follow": "Recomendamos seguir", "follow_suggestions.who_to_follow": "Recomendamos seguir",
"followed_tags": "Hashtags seguidos", "followed_tags": "Etiquetas seguidas",
"footer.about": "Acerca de", "footer.about": "Acerca de",
"footer.directory": "Directorio de perfiles", "footer.directory": "Directorio de perfiles",
"footer.get_app": "Obtener la aplicación", "footer.get_app": "Obtener la aplicación",
@ -344,8 +344,8 @@
"hashtag.column_settings.select.no_options_message": "No se encontraron sugerencias", "hashtag.column_settings.select.no_options_message": "No se encontraron sugerencias",
"hashtag.column_settings.select.placeholder": "Introducir etiquetas…", "hashtag.column_settings.select.placeholder": "Introducir etiquetas…",
"hashtag.column_settings.tag_mode.all": "Todos estos", "hashtag.column_settings.tag_mode.all": "Todos estos",
"hashtag.column_settings.tag_mode.any": "Cualquiera de estos", "hashtag.column_settings.tag_mode.any": "Cualquiera de estas",
"hashtag.column_settings.tag_mode.none": "Ninguno de estos", "hashtag.column_settings.tag_mode.none": "Ninguna de estas",
"hashtag.column_settings.tag_toggle": "Incluye etiquetas adicionales para esta columna", "hashtag.column_settings.tag_toggle": "Incluye etiquetas adicionales para esta columna",
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}", "hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
@ -361,7 +361,7 @@
"hints.profiles.see_more_posts": "Ver más publicaciones en {domain}", "hints.profiles.see_more_posts": "Ver más publicaciones en {domain}",
"hints.threads.replies_may_be_missing": "Puede que no se muestren algunas respuestas de otros servidores.", "hints.threads.replies_may_be_missing": "Puede que no se muestren algunas respuestas de otros servidores.",
"hints.threads.see_more": "Ver más respuestas en {domain}", "hints.threads.see_more": "Ver más respuestas en {domain}",
"home.column_settings.show_reblogs": "Mostrar retoots", "home.column_settings.show_reblogs": "Mostrar impulsos",
"home.column_settings.show_replies": "Mostrar respuestas", "home.column_settings.show_replies": "Mostrar respuestas",
"home.hide_announcements": "Ocultar anuncios", "home.hide_announcements": "Ocultar anuncios",
"home.pending_critical_update.body": "¡Por favor actualiza tu servidor Mastodon lo antes posible!", "home.pending_critical_update.body": "¡Por favor actualiza tu servidor Mastodon lo antes posible!",
@ -369,7 +369,7 @@
"home.pending_critical_update.title": "¡Actualización de seguridad crítica disponible!", "home.pending_critical_update.title": "¡Actualización de seguridad crítica disponible!",
"home.show_announcements": "Mostrar anuncios", "home.show_announcements": "Mostrar anuncios",
"ignore_notifications_modal.disclaimer": "Mastodon no puede informar a los usuarios que has ignorado sus notificaciones. Ignorar notificaciones no impedirá que se sigan enviando los mensajes.", "ignore_notifications_modal.disclaimer": "Mastodon no puede informar a los usuarios que has ignorado sus notificaciones. Ignorar notificaciones no impedirá que se sigan enviando los mensajes.",
"ignore_notifications_modal.filter_instead": "Filtrar en vez de ignorar", "ignore_notifications_modal.filter_instead": "Filtrar en su lugar",
"ignore_notifications_modal.filter_to_act_users": "Aún podrás aceptar, rechazar o reportar usuarios", "ignore_notifications_modal.filter_to_act_users": "Aún podrás aceptar, rechazar o reportar usuarios",
"ignore_notifications_modal.filter_to_avoid_confusion": "Filtrar ayuda a evitar confusiones potenciales", "ignore_notifications_modal.filter_to_avoid_confusion": "Filtrar ayuda a evitar confusiones potenciales",
"ignore_notifications_modal.filter_to_review_separately": "Puedes revisar las notificaciones filtradas por separado", "ignore_notifications_modal.filter_to_review_separately": "Puedes revisar las notificaciones filtradas por separado",
@ -399,13 +399,13 @@
"intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
"keyboard_shortcuts.back": "volver atrás", "keyboard_shortcuts.back": "volver atrás",
"keyboard_shortcuts.blocked": "abrir una lista de usuarios bloqueados", "keyboard_shortcuts.blocked": "abrir una lista de usuarios bloqueados",
"keyboard_shortcuts.boost": "retootear", "keyboard_shortcuts.boost": "Impulsar publicación",
"keyboard_shortcuts.column": "enfocar un estado en una de las columnas", "keyboard_shortcuts.column": "enfocar un estado en una de las columnas",
"keyboard_shortcuts.compose": "enfocar el área de texto de redacción", "keyboard_shortcuts.compose": "enfocar el área de texto de redacción",
"keyboard_shortcuts.description": "Descripción", "keyboard_shortcuts.description": "Descripción",
"keyboard_shortcuts.direct": "para abrir la columna de menciones privadas", "keyboard_shortcuts.direct": "para abrir la columna de menciones privadas",
"keyboard_shortcuts.down": "mover hacia abajo en la lista", "keyboard_shortcuts.down": "mover hacia abajo en la lista",
"keyboard_shortcuts.enter": "abrir estado", "keyboard_shortcuts.enter": "Abrir publicación",
"keyboard_shortcuts.favourite": "Marcar como favorita la publicación", "keyboard_shortcuts.favourite": "Marcar como favorita la publicación",
"keyboard_shortcuts.favourites": "Abrir lista de favoritos", "keyboard_shortcuts.favourites": "Abrir lista de favoritos",
"keyboard_shortcuts.federated": "abrir el timeline federado", "keyboard_shortcuts.federated": "abrir el timeline federado",
@ -419,16 +419,16 @@
"keyboard_shortcuts.my_profile": "abrir tu perfil", "keyboard_shortcuts.my_profile": "abrir tu perfil",
"keyboard_shortcuts.notifications": "abrir la columna de notificaciones", "keyboard_shortcuts.notifications": "abrir la columna de notificaciones",
"keyboard_shortcuts.open_media": "para abrir archivos multimedia", "keyboard_shortcuts.open_media": "para abrir archivos multimedia",
"keyboard_shortcuts.pinned": "abrir la lista de toots destacados", "keyboard_shortcuts.pinned": "Abrir la lista de publicaciones fijadas",
"keyboard_shortcuts.profile": "abrir el perfil del autor", "keyboard_shortcuts.profile": "abrir el perfil del autor",
"keyboard_shortcuts.reply": "para responder", "keyboard_shortcuts.reply": "Responder a la publicación",
"keyboard_shortcuts.requests": "abrir la lista de peticiones de seguidores", "keyboard_shortcuts.requests": "abrir la lista de peticiones de seguidores",
"keyboard_shortcuts.search": "para poner el foco en la búsqueda", "keyboard_shortcuts.search": "para poner el foco en la búsqueda",
"keyboard_shortcuts.spoilers": "para mostrar/ocultar el campo CW", "keyboard_shortcuts.spoilers": "para mostrar/ocultar el campo CW",
"keyboard_shortcuts.start": "abrir la columna \"comenzar\"", "keyboard_shortcuts.start": "abrir la columna \"comenzar\"",
"keyboard_shortcuts.toggle_hidden": "mostrar/ocultar texto tras aviso de contenido (CW)", "keyboard_shortcuts.toggle_hidden": "mostrar/ocultar texto tras aviso de contenido (CW)",
"keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar medios", "keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar medios",
"keyboard_shortcuts.toot": "para comenzar un nuevo toot", "keyboard_shortcuts.toot": "Comenzar una nueva publicación",
"keyboard_shortcuts.unfocus": "para retirar el foco de la caja de redacción/búsqueda", "keyboard_shortcuts.unfocus": "para retirar el foco de la caja de redacción/búsqueda",
"keyboard_shortcuts.up": "para ir hacia arriba en la lista", "keyboard_shortcuts.up": "para ir hacia arriba en la lista",
"lightbox.close": "Cerrar", "lightbox.close": "Cerrar",
@ -474,7 +474,7 @@
"navigation_bar.blocks": "Usuarios bloqueados", "navigation_bar.blocks": "Usuarios bloqueados",
"navigation_bar.bookmarks": "Marcadores", "navigation_bar.bookmarks": "Marcadores",
"navigation_bar.community_timeline": "Historia local", "navigation_bar.community_timeline": "Historia local",
"navigation_bar.compose": "Escribir un nuevo toot", "navigation_bar.compose": "Redactar una nueva publicación",
"navigation_bar.direct": "Menciones privadas", "navigation_bar.direct": "Menciones privadas",
"navigation_bar.discover": "Descubrir", "navigation_bar.discover": "Descubrir",
"navigation_bar.domain_blocks": "Dominios ocultos", "navigation_bar.domain_blocks": "Dominios ocultos",
@ -482,7 +482,7 @@
"navigation_bar.favourites": "Favoritos", "navigation_bar.favourites": "Favoritos",
"navigation_bar.filters": "Palabras silenciadas", "navigation_bar.filters": "Palabras silenciadas",
"navigation_bar.follow_requests": "Solicitudes para seguirte", "navigation_bar.follow_requests": "Solicitudes para seguirte",
"navigation_bar.followed_tags": "Hashtags seguidos", "navigation_bar.followed_tags": "Etiquetas seguidas",
"navigation_bar.follows_and_followers": "Siguiendo y seguidores", "navigation_bar.follows_and_followers": "Siguiendo y seguidores",
"navigation_bar.lists": "Listas", "navigation_bar.lists": "Listas",
"navigation_bar.logout": "Cerrar sesión", "navigation_bar.logout": "Cerrar sesión",
@ -490,25 +490,25 @@
"navigation_bar.mutes": "Usuarios silenciados", "navigation_bar.mutes": "Usuarios silenciados",
"navigation_bar.opened_in_classic_interface": "Publicaciones, cuentas y otras páginas específicas se abren por defecto en la interfaz web clásica.", "navigation_bar.opened_in_classic_interface": "Publicaciones, cuentas y otras páginas específicas se abren por defecto en la interfaz web clásica.",
"navigation_bar.personal": "Personal", "navigation_bar.personal": "Personal",
"navigation_bar.pins": "Toots fijados", "navigation_bar.pins": "Publicaciones fijadas",
"navigation_bar.preferences": "Preferencias", "navigation_bar.preferences": "Preferencias",
"navigation_bar.public_timeline": "Historia federada", "navigation_bar.public_timeline": "Historia federada",
"navigation_bar.search": "Buscar", "navigation_bar.search": "Buscar",
"navigation_bar.security": "Seguridad", "navigation_bar.security": "Seguridad",
"not_signed_in_indicator.not_signed_in": "Necesitas iniciar sesión para acceder a este recurso.", "not_signed_in_indicator.not_signed_in": "Necesitas iniciar sesión para acceder a este recurso.",
"notification.admin.report": "{name} denunció a {target}", "notification.admin.report": "{name} denunció a {target}",
"notification.admin.report_account": "{name} informó de {count, plural, one {una publicación} other {# publicaciones}} de {target} por {category}", "notification.admin.report_account": "{name} reportó {count, plural, one {una publicación} other {# publicaciones}} de {target} por {category}",
"notification.admin.report_account_other": "{name} informó de {count, plural, one {una publicación} other {# publicaciones}} de {target}", "notification.admin.report_account_other": "{name} reportó {count, plural, one {una publicación} other {# publicaciones}} de {target}",
"notification.admin.report_statuses": "{name} informó de {target} por {category}", "notification.admin.report_statuses": "{name} reportó {target} por {category}",
"notification.admin.report_statuses_other": "{name} informó de {target}", "notification.admin.report_statuses_other": "{name} reportó {target}",
"notification.admin.sign_up": "{name} se unio", "notification.admin.sign_up": "{name} se unio",
"notification.admin.sign_up.name_and_others": "{name} y {count, plural, one {# más} other {# más}} se registraron", "notification.admin.sign_up.name_and_others": "{name} y {count, plural, one {# otro} other {# otros}} se registraron",
"notification.favourite": "{name} marcó como favorita tu publicación", "notification.favourite": "{name} marcó como favorita tu publicación",
"notification.favourite.name_and_others_with_link": "{name} y <a>{count, plural, one {# más} other {# más}}</a> marcaron tu publicación como favorita", "notification.favourite.name_and_others_with_link": "{name} y <a>{count, plural, one {# otro} other {# otros}}</a> marcaron tu publicación como favorita",
"notification.follow": "{name} te empezó a seguir", "notification.follow": "{name} te empezó a seguir",
"notification.follow.name_and_others": "{name} y {count, plural, one {# más} other {# más}} te siguieron", "notification.follow.name_and_others": "{name} y {count, plural, one {# otro} other {# otros}} te siguieron",
"notification.follow_request": "{name} ha solicitado seguirte", "notification.follow_request": "{name} ha solicitado seguirte",
"notification.follow_request.name_and_others": "{name} y {count, plural, one {# más} other {# más}} han solicitado seguirte", "notification.follow_request.name_and_others": "{name} y {count, plural, one {# otro} other {# otros}} han solicitado seguirte",
"notification.label.mention": "Mención", "notification.label.mention": "Mención",
"notification.label.private_mention": "Mención privada", "notification.label.private_mention": "Mención privada",
"notification.label.private_reply": "Respuesta privada", "notification.label.private_reply": "Respuesta privada",
@ -519,14 +519,14 @@
"notification.moderation_warning.action_delete_statuses": "Se han eliminado algunas de tus publicaciones.", "notification.moderation_warning.action_delete_statuses": "Se han eliminado algunas de tus publicaciones.",
"notification.moderation_warning.action_disable": "Tu cuenta ha sido desactivada.", "notification.moderation_warning.action_disable": "Tu cuenta ha sido desactivada.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Se han marcado como sensibles algunas de tus publicaciones.", "notification.moderation_warning.action_mark_statuses_as_sensitive": "Se han marcado como sensibles algunas de tus publicaciones.",
"notification.moderation_warning.action_none": "Tu cuenta ha recibido un aviso de moderación.", "notification.moderation_warning.action_none": "Tu cuenta ha recibido una advertencia de moderación.",
"notification.moderation_warning.action_sensitive": "De ahora en adelante, todas tus publicaciones se marcarán como sensibles.", "notification.moderation_warning.action_sensitive": "De ahora en adelante, todas tus publicaciones se marcarán como sensibles.",
"notification.moderation_warning.action_silence": "Tu cuenta ha sido limitada.", "notification.moderation_warning.action_silence": "Tu cuenta ha sido limitada.",
"notification.moderation_warning.action_suspend": "Tu cuenta ha sido suspendida.", "notification.moderation_warning.action_suspend": "Tu cuenta ha sido suspendida.",
"notification.own_poll": "Tu encuesta ha terminado", "notification.own_poll": "Tu encuesta ha terminado",
"notification.poll": "Una encuesta ha terminado", "notification.poll": "Una encuesta en la que has votado ha terminado",
"notification.reblog": "{name} ha retooteado tu estado", "notification.reblog": "{name} ha impulsado tu publicación",
"notification.reblog.name_and_others_with_link": "{name} y <a>{count, plural, one {# más} other {# más}}</a> impulsaron tu publicación", "notification.reblog.name_and_others_with_link": "{name} y <a>{count, plural, one {# otro} other {# otros}}</a> impulsaron tu publicación",
"notification.relationships_severance_event": "Conexiones perdidas con {name}", "notification.relationships_severance_event": "Conexiones perdidas con {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} ha suspendido {target}, lo que significa que ya no puedes recibir actualizaciones de sus cuentas o interactuar con ellas.", "notification.relationships_severance_event.account_suspension": "Un administrador de {from} ha suspendido {target}, lo que significa que ya no puedes recibir actualizaciones de sus cuentas o interactuar con ellas.",
"notification.relationships_severance_event.domain_block": "Un administrador de {from} ha bloqueado {target}, incluyendo {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que sigues.", "notification.relationships_severance_event.domain_block": "Un administrador de {from} ha bloqueado {target}, incluyendo {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que sigues.",
@ -536,18 +536,18 @@
"notification.update": "{name} editó una publicación", "notification.update": "{name} editó una publicación",
"notification_requests.accept": "Aceptar", "notification_requests.accept": "Aceptar",
"notification_requests.accept_multiple": "{count, plural, one {Aceptar # solicitud…} other {Aceptar # solicitudes…}}", "notification_requests.accept_multiple": "{count, plural, one {Aceptar # solicitud…} other {Aceptar # solicitudes…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Aceptar solicitud} other {Aceptar solicitudes}}", "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Solicitud aceptada} other {Solicitudes aceptadas}}",
"notification_requests.confirm_accept_multiple.message": "Vas a aceptar {count, plural, one {una solicitud} other {# solicitudes}}. ¿Quieres continuar?", "notification_requests.confirm_accept_multiple.message": "Estás por aceptar {count, plural, one {una solicitud de notificación} other {# solicitudes de notificación}}. ¿Estás seguro de que quieres continuar?",
"notification_requests.confirm_accept_multiple.title": "¿Aceptar las solicitudes?", "notification_requests.confirm_accept_multiple.title": "¿Deseas aceptar las solicitudes de notificación?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Descartar solicitud} other {Descartar solicitudes}}", "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Solicitud descartada} other {Solicitudes descartadas}}",
"notification_requests.confirm_dismiss_multiple.message": "Vas a descartar {count, plural, one {una solicitud} other {# solicitudes}}. No podrás volver a acceder fácilmente a {count, plural, one {ella} other {ellas}} de nuevo. ¿Seguro que quieres continuar?", "notification_requests.confirm_dismiss_multiple.message": "Estás por descartar {count, plural, one {una solicitud de notificación} other {# solicitudes de notificación}}. No serás capaz de acceder fácilmente a {count, plural, one {ella} other {ellas}} de nuevo. ¿Estás seguro de que quieres continuar?",
"notification_requests.confirm_dismiss_multiple.title": "¿Descartar las solicitudes?", "notification_requests.confirm_dismiss_multiple.title": "¿Deseas descartar las solicitudes de notificación?",
"notification_requests.dismiss": "Descartar", "notification_requests.dismiss": "Descartar",
"notification_requests.dismiss_multiple": "{count, plural, one {Descartar # solicitud…} other {Descartar # solicitudes…}}", "notification_requests.dismiss_multiple": "{count, plural, one {Descartar # solicitud…} other {Descartar # solicitudes…}}",
"notification_requests.edit_selection": "Editar", "notification_requests.edit_selection": "Editar",
"notification_requests.exit_selection": "Hecho", "notification_requests.exit_selection": "Hecho",
"notification_requests.explainer_for_limited_account": "Las notificaciones de esta cuenta han sido filtradas porque la cuenta ha sido limitada por un moderador.", "notification_requests.explainer_for_limited_account": "Las notificaciones de esta cuenta han sido filtradas, ya que la cuenta ha sido limitada por un moderador.",
"notification_requests.explainer_for_limited_remote_account": "Las notificaciones de esta cuenta han sido filtradas porque la cuenta o su servidor ha sido limitada por un moderador.", "notification_requests.explainer_for_limited_remote_account": "Las notificaciones de esta cuenta han sido filtradas, ya que la cuenta o su servidor ha sido limitada por un moderador.",
"notification_requests.maximize": "Maximizar", "notification_requests.maximize": "Maximizar",
"notification_requests.minimize_banner": "Minimizar banner de notificaciones filtradas", "notification_requests.minimize_banner": "Minimizar banner de notificaciones filtradas",
"notification_requests.notifications_from": "Notificaciones de {name}", "notification_requests.notifications_from": "Notificaciones de {name}",
@ -555,7 +555,7 @@
"notification_requests.view": "Ver notificaciones", "notification_requests.view": "Ver notificaciones",
"notifications.clear": "Limpiar notificaciones", "notifications.clear": "Limpiar notificaciones",
"notifications.clear_confirmation": "¿Seguro de querer borrar permanentemente todas tus notificaciones?", "notifications.clear_confirmation": "¿Seguro de querer borrar permanentemente todas tus notificaciones?",
"notifications.clear_title": "¿Borrar notificaciones?", "notifications.clear_title": "¿Limpiar notificaciones?",
"notifications.column_settings.admin.report": "Nuevas denuncias:", "notifications.column_settings.admin.report": "Nuevas denuncias:",
"notifications.column_settings.admin.sign_up": "Registros nuevos:", "notifications.column_settings.admin.sign_up": "Registros nuevos:",
"notifications.column_settings.alert": "Notificaciones de escritorio", "notifications.column_settings.alert": "Notificaciones de escritorio",
@ -567,7 +567,7 @@
"notifications.column_settings.mention": "Menciones:", "notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.poll": "Resultados de la votación:", "notifications.column_settings.poll": "Resultados de la votación:",
"notifications.column_settings.push": "Notificaciones push", "notifications.column_settings.push": "Notificaciones push",
"notifications.column_settings.reblog": "Retoots:", "notifications.column_settings.reblog": "Impulsos:",
"notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido", "notifications.column_settings.sound": "Reproducir sonido",
"notifications.column_settings.status": "Nuevas publicaciones:", "notifications.column_settings.status": "Nuevas publicaciones:",
@ -575,7 +575,7 @@
"notifications.column_settings.unread_notifications.highlight": "Destacar notificaciones no leídas", "notifications.column_settings.unread_notifications.highlight": "Destacar notificaciones no leídas",
"notifications.column_settings.update": "Ediciones:", "notifications.column_settings.update": "Ediciones:",
"notifications.filter.all": "Todos", "notifications.filter.all": "Todos",
"notifications.filter.boosts": "Retoots", "notifications.filter.boosts": "Impulsos",
"notifications.filter.favourites": "Favoritos", "notifications.filter.favourites": "Favoritos",
"notifications.filter.follows": "Seguidores", "notifications.filter.follows": "Seguidores",
"notifications.filter.mentions": "Menciones", "notifications.filter.mentions": "Menciones",
@ -621,7 +621,7 @@
"onboarding.profile.display_name_hint": "Tu nombre completo o tu apodo…", "onboarding.profile.display_name_hint": "Tu nombre completo o tu apodo…",
"onboarding.profile.lead": "Siempre puedes completar esto más tarde en los ajustes, donde hay aún más opciones de personalización disponibles.", "onboarding.profile.lead": "Siempre puedes completar esto más tarde en los ajustes, donde hay aún más opciones de personalización disponibles.",
"onboarding.profile.note": "Biografía", "onboarding.profile.note": "Biografía",
"onboarding.profile.note_hint": "Puedes @mencionar a otras personas o #hashtags…", "onboarding.profile.note_hint": "Puedes @mencionar a otras personas o #etiquetas…",
"onboarding.profile.save_and_continue": "Guardar y continuar", "onboarding.profile.save_and_continue": "Guardar y continuar",
"onboarding.profile.title": "Configuración del perfil", "onboarding.profile.title": "Configuración del perfil",
"onboarding.profile.upload_avatar": "Subir foto de perfil", "onboarding.profile.upload_avatar": "Subir foto de perfil",
@ -639,7 +639,7 @@
"onboarding.steps.publish_status.title": "Escribe tu primera publicación", "onboarding.steps.publish_status.title": "Escribe tu primera publicación",
"onboarding.steps.setup_profile.body": "Si rellenas tu perfil tendrás más posibilidades de que otros interactúen contigo.", "onboarding.steps.setup_profile.body": "Si rellenas tu perfil tendrás más posibilidades de que otros interactúen contigo.",
"onboarding.steps.setup_profile.title": "Personaliza tu perfil", "onboarding.steps.setup_profile.title": "Personaliza tu perfil",
"onboarding.steps.share_profile.body": "¡Dile a tus amigos cómo encontrarte en Mastodon!", "onboarding.steps.share_profile.body": "Dile a tus amigos cómo encontrarte en Mastodon",
"onboarding.steps.share_profile.title": "Comparte tu perfil", "onboarding.steps.share_profile.title": "Comparte tu perfil",
"onboarding.tips.2fa": "<strong>¿Sabías que?</strong> Puedes proteger tu cuenta configurando la autenticación de dos factores en la configuración de su cuenta. Funciona con cualquier aplicación TOTP de su elección, ¡no necesitas número de teléfono!", "onboarding.tips.2fa": "<strong>¿Sabías que?</strong> Puedes proteger tu cuenta configurando la autenticación de dos factores en la configuración de su cuenta. Funciona con cualquier aplicación TOTP de su elección, ¡no necesitas número de teléfono!",
"onboarding.tips.accounts_from_other_servers": "<strong>¿Sabías que?</strong> Como Mastodon es descentralizado, algunos perfiles que encuentras están alojados en servidores distintos del tuyo. Y sin embargo, ¡puedes interactuar con ellos! ¡Su servidor corresponde a la segunda mitad de su nombre de usuario!", "onboarding.tips.accounts_from_other_servers": "<strong>¿Sabías que?</strong> Como Mastodon es descentralizado, algunos perfiles que encuentras están alojados en servidores distintos del tuyo. Y sin embargo, ¡puedes interactuar con ellos! ¡Su servidor corresponde a la segunda mitad de su nombre de usuario!",
@ -665,7 +665,7 @@
"privacy.private.short": "Seguidores", "privacy.private.short": "Seguidores",
"privacy.public.long": "Cualquiera dentro y fuera de Mastodon", "privacy.public.long": "Cualquiera dentro y fuera de Mastodon",
"privacy.public.short": "Público", "privacy.public.short": "Público",
"privacy.unlisted.additional": "Esto se comporta exactamente igual que el público, excepto que el post no aparecerá en las cronologías en directo o en los hashtags, la exploración o busquedas en Mastodon, incluso si está optado por activar la cuenta de usuario.", "privacy.unlisted.additional": "Esto se comporta exactamente igual que el público, excepto que el post no aparecerá en las cronologías en directo o en las etiquetas, la exploración o busquedas en Mastodon, incluso si está optado por activar la cuenta de usuario.",
"privacy.unlisted.long": "Menos fanfares algorítmicos", "privacy.unlisted.long": "Menos fanfares algorítmicos",
"privacy.unlisted.short": "Público silencioso", "privacy.unlisted.short": "Público silencioso",
"privacy_policy.last_updated": "Actualizado por última vez {date}", "privacy_policy.last_updated": "Actualizado por última vez {date}",
@ -699,7 +699,7 @@
"report.category.title_account": "perfil", "report.category.title_account": "perfil",
"report.category.title_status": "publicación", "report.category.title_status": "publicación",
"report.close": "Realizado", "report.close": "Realizado",
"report.comment.title": "¿Hay algo más que usted cree que debamos saber?", "report.comment.title": "¿Hay algo más que creas que deberíamos saber?",
"report.forward": "Reenviar a {target}", "report.forward": "Reenviar a {target}",
"report.forward_hint": "Esta cuenta es de otro servidor. ¿Enviar una copia anonimizada del informe allí también?", "report.forward_hint": "Esta cuenta es de otro servidor. ¿Enviar una copia anonimizada del informe allí también?",
"report.mute": "Silenciar", "report.mute": "Silenciar",
@ -776,9 +776,9 @@
"status.admin_status": "Abrir este estado en la interfaz de moderación", "status.admin_status": "Abrir este estado en la interfaz de moderación",
"status.block": "Bloquear a @{name}", "status.block": "Bloquear a @{name}",
"status.bookmark": "Añadir marcador", "status.bookmark": "Añadir marcador",
"status.cancel_reblog_private": "Eliminar retoot", "status.cancel_reblog_private": "Deshacer impulso",
"status.cannot_reblog": "Este toot no puede retootearse", "status.cannot_reblog": "Esta publicación no puede ser impulsada",
"status.continued_thread": "Continuó el hilo", "status.continued_thread": "Hilo continuado",
"status.copy": "Copiar enlace al estado", "status.copy": "Copiar enlace al estado",
"status.delete": "Borrar", "status.delete": "Borrar",
"status.detailed_status": "Vista de conversación detallada", "status.detailed_status": "Vista de conversación detallada",
@ -803,16 +803,16 @@
"status.mute_conversation": "Silenciar conversación", "status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir estado", "status.open": "Expandir estado",
"status.pin": "Fijar", "status.pin": "Fijar",
"status.pinned": "Toot fijado", "status.pinned": "Publicación fijada",
"status.read_more": "Leer más", "status.read_more": "Leer más",
"status.reblog": "Retootear", "status.reblog": "Impulsar",
"status.reblog_private": "Implusar a la audiencia original", "status.reblog_private": "Implusar a la audiencia original",
"status.reblogged_by": "Retooteado por {name}", "status.reblogged_by": "Impulsado por {name}",
"status.reblogs": "{count, plural, one {impulso} other {impulsos}}", "status.reblogs": "{count, plural, one {impulso} other {impulsos}}",
"status.reblogs.empty": "Nadie retooteó este toot todavía. Cuando alguien lo haga, aparecerá aquí.", "status.reblogs.empty": "Nadie impulsó esta publicación todavía. Cuando alguien lo haga, aparecerá aquí.",
"status.redraft": "Borrar y volver a borrador", "status.redraft": "Borrar y volver a borrador",
"status.remove_bookmark": "Eliminar marcador", "status.remove_bookmark": "Eliminar marcador",
"status.replied_in_thread": "Respondió en el hilo", "status.replied_in_thread": "Respondido en el hilo",
"status.replied_to": "Respondió a {name}", "status.replied_to": "Respondió a {name}",
"status.reply": "Responder", "status.reply": "Responder",
"status.replyAll": "Responder al hilo", "status.replyAll": "Responder al hilo",

View file

@ -312,7 +312,7 @@
"follow_request.authorize": "Autoriser", "follow_request.authorize": "Autoriser",
"follow_request.reject": "Rejeter", "follow_request.reject": "Rejeter",
"follow_requests.unlocked_explanation": "Même si votre compte nest pas privé, léquipe de {domain} a pensé que vous pourriez vouloir peut-être consulter manuellement les demandes d'abonnement de ces comptes.", "follow_requests.unlocked_explanation": "Même si votre compte nest pas privé, léquipe de {domain} a pensé que vous pourriez vouloir peut-être consulter manuellement les demandes d'abonnement de ces comptes.",
"follow_suggestions.curated_suggestion": "Choix du staff", "follow_suggestions.curated_suggestion": "Sélectionné par l'équipe",
"follow_suggestions.dismiss": "Ne plus afficher", "follow_suggestions.dismiss": "Ne plus afficher",
"follow_suggestions.featured_longer": "Sélectionné par l'équipe de {domain}", "follow_suggestions.featured_longer": "Sélectionné par l'équipe de {domain}",
"follow_suggestions.friends_of_friends_longer": "Populaire dans le cercle des personnes que vous suivez", "follow_suggestions.friends_of_friends_longer": "Populaire dans le cercle des personnes que vous suivez",

View file

@ -312,7 +312,7 @@
"follow_request.authorize": "Accepter", "follow_request.authorize": "Accepter",
"follow_request.reject": "Rejeter", "follow_request.reject": "Rejeter",
"follow_requests.unlocked_explanation": "Même si votre compte nest pas privé, léquipe de {domain} a pensé que vous pourriez vouloir consulter manuellement les demandes de suivi de ces comptes.", "follow_requests.unlocked_explanation": "Même si votre compte nest pas privé, léquipe de {domain} a pensé que vous pourriez vouloir consulter manuellement les demandes de suivi de ces comptes.",
"follow_suggestions.curated_suggestion": "Choix du staff", "follow_suggestions.curated_suggestion": "Sélectionné par l'équipe",
"follow_suggestions.dismiss": "Ne plus afficher", "follow_suggestions.dismiss": "Ne plus afficher",
"follow_suggestions.featured_longer": "Sélectionné par l'équipe de {domain}", "follow_suggestions.featured_longer": "Sélectionné par l'équipe de {domain}",
"follow_suggestions.friends_of_friends_longer": "Populaire dans le cercle des personnes que vous suivez", "follow_suggestions.friends_of_friends_longer": "Populaire dans le cercle des personnes que vous suivez",

View file

@ -36,7 +36,7 @@
"account.followers.empty": "Chan eil neach sam bith a leantainn air a chleachdaiche seo fhathast.", "account.followers.empty": "Chan eil neach sam bith a leantainn air a chleachdaiche seo fhathast.",
"account.followers_counter": "{count, plural, one {{counter} neach-leantainn} other {{counter} luchd-leantainn}}", "account.followers_counter": "{count, plural, one {{counter} neach-leantainn} other {{counter} luchd-leantainn}}",
"account.following": "A leantainn", "account.following": "A leantainn",
"account.following_counter": "{count, plural, one {Tha {counter} a leantainn} other {Tha {counter} a leantainn}}", "account.following_counter": "{count, plural, one {A leantainn {counter}} other {A leantainn {counter}}}",
"account.follows.empty": "Chan eil an cleachdaiche seo a leantainn neach sam bith fhathast.", "account.follows.empty": "Chan eil an cleachdaiche seo a leantainn neach sam bith fhathast.",
"account.go_to_profile": "Tadhail air a phròifil", "account.go_to_profile": "Tadhail air a phròifil",
"account.hide_reblogs": "Falaich na brosnachaidhean o @{name}", "account.hide_reblogs": "Falaich na brosnachaidhean o @{name}",

View file

@ -787,6 +787,7 @@
"status.edit": "עריכה", "status.edit": "עריכה",
"status.edited": "נערך לאחרונה {date}", "status.edited": "נערך לאחרונה {date}",
"status.edited_x_times": "נערך {count, plural, one {פעם {count}} other {{count} פעמים}}", "status.edited_x_times": "נערך {count, plural, one {פעם {count}} other {{count} פעמים}}",
"status.embed": "העתקת קוד להטמעה",
"status.favourite": "חיבוב", "status.favourite": "חיבוב",
"status.favourites": "{count, plural, one {חיבוב אחד} two {זוג חיבובים} other {# חיבובים}}", "status.favourites": "{count, plural, one {חיבוב אחד} two {זוג חיבובים} other {# חיבובים}}",
"status.filter": "סנן הודעה זו", "status.filter": "סנן הודעה זו",

View file

@ -2,12 +2,14 @@
"about.blocks": "Ulac agbur", "about.blocks": "Ulac agbur",
"about.contact": "Anermis:", "about.contact": "Anermis:",
"about.disclaimer": "Mastodon d aseɣẓan ilelli, d aseɣẓan n uɣbalu yeldin, d tnezzut n Mastodon gGmbH.", "about.disclaimer": "Mastodon d aseɣẓan ilelli, d aseɣẓan n uɣbalu yeldin, d tnezzut n Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Ulac taɣẓint",
"about.domain_blocks.preamble": "Maṣṭudun s umata yeḍmen-ak ad teẓreḍ agbur, ad tesdemreḍ akked yimseqdacen-nniḍen seg yal aqeddac deg fedivers. Ha-tent-an ɣur-k tsuraf i yellan deg uqeddac-agi.", "about.domain_blocks.preamble": "Maṣṭudun s umata yeḍmen-ak ad teẓreḍ agbur, ad tesdemreḍ akked yimseqdacen-nniḍen seg yal aqeddac deg fedivers. Ha-tent-an ɣur-k tsuraf i yellan deg uqeddac-agi.",
"about.domain_blocks.silenced.title": "Ɣur-s talast", "about.domain_blocks.silenced.title": "Ɣur-s talast",
"about.domain_blocks.suspended.title": "Yeḥbes", "about.domain_blocks.suspended.title": "Yeḥbes",
"about.not_available": "Talɣut-a ur tettwabder ara deg uqeddac-a.", "about.not_available": "Talɣut-a ur tettwabder ara deg uqeddac-a.",
"about.powered_by": "Azeṭṭa inmetti yettwasɣelsen sɣur {mastodon}", "about.powered_by": "Azeṭṭa inmetti yettwasɣelsen sɣur {mastodon}",
"about.rules": "Ilugan n uqeddac", "about.rules": "Ilugan n uqeddac",
"account.account_note_header": "Tamawt tudmawant",
"account.add_or_remove_from_list": "Rnu neɣ kkes seg tebdarin", "account.add_or_remove_from_list": "Rnu neɣ kkes seg tebdarin",
"account.badges.bot": "Aṛubut", "account.badges.bot": "Aṛubut",
"account.badges.group": "Agraw", "account.badges.group": "Agraw",
@ -46,6 +48,7 @@
"account.mute_notifications_short": "Susem alɣuten", "account.mute_notifications_short": "Susem alɣuten",
"account.mute_short": "Sgugem", "account.mute_short": "Sgugem",
"account.muted": "Yettwasgugem", "account.muted": "Yettwasgugem",
"account.mutual": "Temṭafarem",
"account.no_bio": "Ulac aglam i d-yettunefken.", "account.no_bio": "Ulac aglam i d-yettunefken.",
"account.open_original_page": "Ldi asebter anasli", "account.open_original_page": "Ldi asebter anasli",
"account.posts": "Tisuffaɣ", "account.posts": "Tisuffaɣ",
@ -62,6 +65,7 @@
"account.unendorse": "Ur ttwellih ara fell-as deg umaɣnu-inek", "account.unendorse": "Ur ttwellih ara fell-as deg umaɣnu-inek",
"account.unfollow": "Ur ṭṭafaṛ ara", "account.unfollow": "Ur ṭṭafaṛ ara",
"account.unmute": "Kkes asgugem ɣef @{name}", "account.unmute": "Kkes asgugem ɣef @{name}",
"account.unmute_notifications_short": "Serreḥ i yilɣa",
"account.unmute_short": "Kkes asgugem", "account.unmute_short": "Kkes asgugem",
"account_note.placeholder": "Ulac iwenniten", "account_note.placeholder": "Ulac iwenniten",
"admin.dashboard.retention.cohort_size": "Iseqdacen imaynuten", "admin.dashboard.retention.cohort_size": "Iseqdacen imaynuten",
@ -152,6 +156,7 @@
"confirmations.edit.message": "Abeddel tura ad d-yaru izen-nni i d-tegreḍ akka tura. Tetḥeqqeḍ tebɣiḍ ad tkemmleḍ?", "confirmations.edit.message": "Abeddel tura ad d-yaru izen-nni i d-tegreḍ akka tura. Tetḥeqqeḍ tebɣiḍ ad tkemmleḍ?",
"confirmations.logout.confirm": "Ffeɣ", "confirmations.logout.confirm": "Ffeɣ",
"confirmations.logout.message": "D tidet tebɣiḍ ad teffɣeḍ?", "confirmations.logout.message": "D tidet tebɣiḍ ad teffɣeḍ?",
"confirmations.logout.title": "Tebɣiḍ ad teffɣeḍ ssya?",
"confirmations.mute.confirm": "Sgugem", "confirmations.mute.confirm": "Sgugem",
"confirmations.redraft.confirm": "Kkes sakin ɛiwed tira", "confirmations.redraft.confirm": "Kkes sakin ɛiwed tira",
"confirmations.reply.confirm": "Err", "confirmations.reply.confirm": "Err",
@ -351,6 +356,7 @@
"lists.subheading": "Tibdarin-ik·im", "lists.subheading": "Tibdarin-ik·im",
"load_pending": "{count, plural, one {# n uferdis amaynut} other {# n yiferdisen imaynuten}}", "load_pending": "{count, plural, one {# n uferdis amaynut} other {# n yiferdisen imaynuten}}",
"loading_indicator.label": "Yessalay-d …", "loading_indicator.label": "Yessalay-d …",
"media_gallery.hide": "Ffer-it",
"mute_modal.hide_from_notifications": "Ffer-it deg ulɣuten", "mute_modal.hide_from_notifications": "Ffer-it deg ulɣuten",
"mute_modal.hide_options": "Ffer tinefrunin", "mute_modal.hide_options": "Ffer tinefrunin",
"mute_modal.indefinite": "Alamma ssnesreɣ asgugem fell-as", "mute_modal.indefinite": "Alamma ssnesreɣ asgugem fell-as",
@ -405,6 +411,7 @@
"notification.status": "{name} akken i d-yessufeɣ", "notification.status": "{name} akken i d-yessufeɣ",
"notification_requests.accept": "Qbel", "notification_requests.accept": "Qbel",
"notification_requests.dismiss": "Agi", "notification_requests.dismiss": "Agi",
"notification_requests.edit_selection": "Ẓreg",
"notification_requests.exit_selection": "Immed", "notification_requests.exit_selection": "Immed",
"notification_requests.notifications_from": "Alɣuten sɣur {name}", "notification_requests.notifications_from": "Alɣuten sɣur {name}",
"notifications.clear": "Sfeḍ alɣuten", "notifications.clear": "Sfeḍ alɣuten",

View file

@ -778,6 +778,7 @@
"status.bookmark": "북마크", "status.bookmark": "북마크",
"status.cancel_reblog_private": "부스트 취소", "status.cancel_reblog_private": "부스트 취소",
"status.cannot_reblog": "이 게시물은 부스트 할 수 없습니다", "status.cannot_reblog": "이 게시물은 부스트 할 수 없습니다",
"status.continued_thread": "이어지는 글타래",
"status.copy": "게시물 링크 복사", "status.copy": "게시물 링크 복사",
"status.delete": "삭제", "status.delete": "삭제",
"status.detailed_status": "대화 자세히 보기", "status.detailed_status": "대화 자세히 보기",
@ -786,6 +787,7 @@
"status.edit": "수정", "status.edit": "수정",
"status.edited": "{date}에 마지막으로 편집됨", "status.edited": "{date}에 마지막으로 편집됨",
"status.edited_x_times": "{count, plural, other {{count}}} 번 수정됨", "status.edited_x_times": "{count, plural, other {{count}}} 번 수정됨",
"status.embed": "임베드 코드 받기",
"status.favourite": "좋아요", "status.favourite": "좋아요",
"status.favourites": "{count, plural, other {좋아요}}", "status.favourites": "{count, plural, other {좋아요}}",
"status.filter": "이 게시물을 필터", "status.filter": "이 게시물을 필터",
@ -810,6 +812,7 @@
"status.reblogs.empty": "아직 아무도 이 게시물을 부스트하지 않았습니다. 부스트 한 사람들이 여기에 표시 됩니다.", "status.reblogs.empty": "아직 아무도 이 게시물을 부스트하지 않았습니다. 부스트 한 사람들이 여기에 표시 됩니다.",
"status.redraft": "지우고 다시 쓰기", "status.redraft": "지우고 다시 쓰기",
"status.remove_bookmark": "북마크 삭제", "status.remove_bookmark": "북마크 삭제",
"status.replied_in_thread": "글타래에 답장",
"status.replied_to": "{name} 님에게", "status.replied_to": "{name} 님에게",
"status.reply": "답장", "status.reply": "답장",
"status.replyAll": "글타래에 답장", "status.replyAll": "글타래에 답장",

View file

@ -409,6 +409,7 @@
"lists.subheading": "Tavi saraksti", "lists.subheading": "Tavi saraksti",
"load_pending": "{count, plural, one {# jauna lieta} other {# jaunas lietas}}", "load_pending": "{count, plural, one {# jauna lieta} other {# jaunas lietas}}",
"loading_indicator.label": "Ielādē…", "loading_indicator.label": "Ielādē…",
"media_gallery.hide": "Paslēpt",
"moved_to_account_banner.text": "Tavs konts {disabledAccount} pašlaik ir atspējots, jo Tu pārcēlies uz kontu {movedToAccount}.", "moved_to_account_banner.text": "Tavs konts {disabledAccount} pašlaik ir atspējots, jo Tu pārcēlies uz kontu {movedToAccount}.",
"mute_modal.hide_from_notifications": "Paslēpt paziņojumos", "mute_modal.hide_from_notifications": "Paslēpt paziņojumos",
"mute_modal.hide_options": "Paslēpt iespējas", "mute_modal.hide_options": "Paslēpt iespējas",

View file

@ -34,7 +34,9 @@
"account.follow_back": "Подписаться в ответ", "account.follow_back": "Подписаться в ответ",
"account.followers": "Подписчики", "account.followers": "Подписчики",
"account.followers.empty": "На этого пользователя пока никто не подписан.", "account.followers.empty": "На этого пользователя пока никто не подписан.",
"account.followers_counter": "{count, plural, one {{counter} последователя} other {{counter} последователей}}",
"account.following": "Подписки", "account.following": "Подписки",
"account.following_counter": "{count, plural, one {{counter} последующий} other {{counter} последующие}}",
"account.follows.empty": "Этот пользователь пока ни на кого не подписался.", "account.follows.empty": "Этот пользователь пока ни на кого не подписался.",
"account.go_to_profile": "Перейти к профилю", "account.go_to_profile": "Перейти к профилю",
"account.hide_reblogs": "Скрыть продвижения от @{name}", "account.hide_reblogs": "Скрыть продвижения от @{name}",
@ -48,7 +50,7 @@
"account.moved_to": "У {name} теперь новый аккаунт:", "account.moved_to": "У {name} теперь новый аккаунт:",
"account.mute": "Игнорировать @{name}", "account.mute": "Игнорировать @{name}",
"account.mute_notifications_short": "Отключить уведомления", "account.mute_notifications_short": "Отключить уведомления",
"account.mute_short": "Глохни!", "account.mute_short": "Приглушить",
"account.muted": "Игнорируется", "account.muted": "Игнорируется",
"account.mutual": "Взаимно", "account.mutual": "Взаимно",
"account.no_bio": "Описание не предоставлено.", "account.no_bio": "Описание не предоставлено.",
@ -94,6 +96,8 @@
"block_modal.title": "Заблокировать пользователя?", "block_modal.title": "Заблокировать пользователя?",
"block_modal.you_wont_see_mentions": "Вы не увидите записи, которые упоминают его.", "block_modal.you_wont_see_mentions": "Вы не увидите записи, которые упоминают его.",
"boost_modal.combo": "{combo}, чтобы пропустить это в следующий раз", "boost_modal.combo": "{combo}, чтобы пропустить это в следующий раз",
"boost_modal.reblog": "Повысить пост?",
"boost_modal.undo_reblog": "Разгрузить пост?",
"bundle_column_error.copy_stacktrace": "Скопировать отчет об ошибке", "bundle_column_error.copy_stacktrace": "Скопировать отчет об ошибке",
"bundle_column_error.error.body": "Запрошенная страница не может быть отображена. Это может быть вызвано ошибкой в нашем коде или проблемой совместимости браузера.", "bundle_column_error.error.body": "Запрошенная страница не может быть отображена. Это может быть вызвано ошибкой в нашем коде или проблемой совместимости браузера.",
"bundle_column_error.error.title": "О нет!", "bundle_column_error.error.title": "О нет!",
@ -298,6 +302,8 @@
"filter_modal.select_filter.subtitle": "Используйте существующую категорию или создайте новую", "filter_modal.select_filter.subtitle": "Используйте существующую категорию или создайте новую",
"filter_modal.select_filter.title": "Фильтровать этот пост", "filter_modal.select_filter.title": "Фильтровать этот пост",
"filter_modal.title.status": "Фильтровать пост", "filter_modal.title.status": "Фильтровать пост",
"filter_warning.matches_filter": "Соответствует фильтру \"{title}\"",
"filtered_notifications_banner.pending_requests": "Вы можете знать {count, plural, =0 {ни один} one {один человек} other {# люди}}",
"filtered_notifications_banner.title": "Отфильтрованные уведомления", "filtered_notifications_banner.title": "Отфильтрованные уведомления",
"firehose.all": "Все", "firehose.all": "Все",
"firehose.local": "Текущий сервер", "firehose.local": "Текущий сервер",
@ -346,6 +352,14 @@
"hashtag.follow": "Подписаться на новые посты", "hashtag.follow": "Подписаться на новые посты",
"hashtag.unfollow": "Отписаться", "hashtag.unfollow": "Отписаться",
"hashtags.and_other": "...и {count, plural, other {# ещё}}", "hashtags.and_other": "...и {count, plural, other {# ещё}}",
"hints.profiles.followers_may_be_missing": "Последователи для этого профиля могут отсутствовать.",
"hints.profiles.follows_may_be_missing": "Фолловеры для этого профиля могут отсутствовать.",
"hints.profiles.posts_may_be_missing": "Некоторые сообщения из этого профиля могут отсутствовать.",
"hints.profiles.see_more_followers": "Посмотреть больше подписчиков на {domain}",
"hints.profiles.see_more_follows": "Смотрите другие материалы по теме {domain}",
"hints.profiles.see_more_posts": "Посмотреть другие сообщения на {domain}",
"hints.threads.replies_may_be_missing": "Ответы с других серверов могут отсутствовать.",
"hints.threads.see_more": "Посмотреть другие ответы на {domain}",
"home.column_settings.show_reblogs": "Показывать продвижения", "home.column_settings.show_reblogs": "Показывать продвижения",
"home.column_settings.show_replies": "Показывать ответы", "home.column_settings.show_replies": "Показывать ответы",
"home.hide_announcements": "Скрыть объявления", "home.hide_announcements": "Скрыть объявления",
@ -353,7 +367,17 @@
"home.pending_critical_update.link": "Посмотреть обновления", "home.pending_critical_update.link": "Посмотреть обновления",
"home.pending_critical_update.title": "Доступно критическое обновление безопасности!", "home.pending_critical_update.title": "Доступно критическое обновление безопасности!",
"home.show_announcements": "Показать объявления", "home.show_announcements": "Показать объявления",
"ignore_notifications_modal.disclaimer": "Mastodon не может сообщить пользователям, что вы проигнорировали их уведомления. Игнорирование уведомлений не остановит отправку самих сообщений.",
"ignore_notifications_modal.filter_instead": "Фильтр вместо",
"ignore_notifications_modal.filter_to_act_users": "Вы и далее сможете принять, отвергнуть и жаловаться на пользователей", "ignore_notifications_modal.filter_to_act_users": "Вы и далее сможете принять, отвергнуть и жаловаться на пользователей",
"ignore_notifications_modal.filter_to_avoid_confusion": "Фильтрация помогает избежать потенциальной путаницы",
"ignore_notifications_modal.filter_to_review_separately": "Вы можете просматривать отфильтрованные уведомления отдельно",
"ignore_notifications_modal.ignore": "Игнорировать уведомления",
"ignore_notifications_modal.limited_accounts_title": "Игнорировать уведомления от модерируемых аккаунтов?",
"ignore_notifications_modal.new_accounts_title": "Игнорировать уведомления от новых аккаунтов?",
"ignore_notifications_modal.not_followers_title": "Игнорировать уведомления от людей, которые не следят за вами?",
"ignore_notifications_modal.not_following_title": "Игнорировать уведомления от людей, за которыми вы не следите?",
"ignore_notifications_modal.private_mentions_title": "Игнорировать уведомления о нежелательных личных сообщениях?",
"interaction_modal.description.favourite": "С учётной записью Mastodon, вы можете добавить этот пост в избранное, чтобы сохранить его на будущее и дать автору знать, что пост вам понравился.", "interaction_modal.description.favourite": "С учётной записью Mastodon, вы можете добавить этот пост в избранное, чтобы сохранить его на будущее и дать автору знать, что пост вам понравился.",
"interaction_modal.description.follow": "С учётной записью Mastodon вы можете подписаться на {name}, чтобы получать их посты в своей домашней ленте.", "interaction_modal.description.follow": "С учётной записью Mastodon вы можете подписаться на {name}, чтобы получать их посты в своей домашней ленте.",
"interaction_modal.description.reblog": "С учётной записью Mastodon, вы можете продвинуть этот пост, чтобы поделиться им со своими подписчиками.", "interaction_modal.description.reblog": "С учётной записью Mastodon, вы можете продвинуть этот пост, чтобы поделиться им со своими подписчиками.",
@ -432,6 +456,7 @@
"lists.subheading": "Ваши списки", "lists.subheading": "Ваши списки",
"load_pending": "{count, plural, one {# новый элемент} few {# новых элемента} other {# новых элементов}}", "load_pending": "{count, plural, one {# новый элемент} few {# новых элемента} other {# новых элементов}}",
"loading_indicator.label": "Загрузка…", "loading_indicator.label": "Загрузка…",
"media_gallery.hide": "Скрыть",
"moved_to_account_banner.text": "Ваша учетная запись {disabledAccount} в настоящее время заморожена, потому что вы переехали на {movedToAccount}.", "moved_to_account_banner.text": "Ваша учетная запись {disabledAccount} в настоящее время заморожена, потому что вы переехали на {movedToAccount}.",
"mute_modal.hide_from_notifications": "Скрыть из уведомлений", "mute_modal.hide_from_notifications": "Скрыть из уведомлений",
"mute_modal.hide_options": "Скрыть параметры", "mute_modal.hide_options": "Скрыть параметры",
@ -443,6 +468,7 @@
"mute_modal.you_wont_see_mentions": "Вы не увидите постов, которые их упоминают.", "mute_modal.you_wont_see_mentions": "Вы не увидите постов, которые их упоминают.",
"mute_modal.you_wont_see_posts": "Они по-прежнему смогут видеть ваши посты, но вы не сможете видеть их посты.", "mute_modal.you_wont_see_posts": "Они по-прежнему смогут видеть ваши посты, но вы не сможете видеть их посты.",
"navigation_bar.about": "О проекте", "navigation_bar.about": "О проекте",
"navigation_bar.administration": "Администрация",
"navigation_bar.advanced_interface": "Включить многоколоночный интерфейс", "navigation_bar.advanced_interface": "Включить многоколоночный интерфейс",
"navigation_bar.blocks": "Заблокированные пользователи", "navigation_bar.blocks": "Заблокированные пользователи",
"navigation_bar.bookmarks": "Закладки", "navigation_bar.bookmarks": "Закладки",
@ -459,6 +485,7 @@
"navigation_bar.follows_and_followers": "Подписки и подписчики", "navigation_bar.follows_and_followers": "Подписки и подписчики",
"navigation_bar.lists": "Списки", "navigation_bar.lists": "Списки",
"navigation_bar.logout": "Выйти", "navigation_bar.logout": "Выйти",
"navigation_bar.moderation": "Модерация",
"navigation_bar.mutes": "Игнорируемые пользователи", "navigation_bar.mutes": "Игнорируемые пользователи",
"navigation_bar.opened_in_classic_interface": "Сообщения, учётные записи и другие специфические страницы по умолчанию открываются в классическом веб-интерфейсе.", "navigation_bar.opened_in_classic_interface": "Сообщения, учётные записи и другие специфические страницы по умолчанию открываются в классическом веб-интерфейсе.",
"navigation_bar.personal": "Личное", "navigation_bar.personal": "Личное",
@ -469,10 +496,22 @@
"navigation_bar.security": "Безопасность", "navigation_bar.security": "Безопасность",
"not_signed_in_indicator.not_signed_in": "Вам нужно войти, чтобы иметь доступ к этому ресурсу.", "not_signed_in_indicator.not_signed_in": "Вам нужно войти, чтобы иметь доступ к этому ресурсу.",
"notification.admin.report": "{name} сообщил о {target}", "notification.admin.report": "{name} сообщил о {target}",
"notification.admin.report_account": "{name} сообщил {count, plural, one {один пост} other {# постов}} от {target} для {category}",
"notification.admin.report_account_other": "{name} сообщил {count, plural, one {одно сообщение} other {# сообщений}} от {target}",
"notification.admin.report_statuses": "{name} сообщил {target} для {category}",
"notification.admin.report_statuses_other": "{name} сообщает {target}",
"notification.admin.sign_up": "{name} зарегистрирован", "notification.admin.sign_up": "{name} зарегистрирован",
"notification.admin.sign_up.name_and_others": "{name} и {count, plural, one {# другой} other {# другие}} подписались",
"notification.favourite": "{name} добавил(а) ваш пост в избранное", "notification.favourite": "{name} добавил(а) ваш пост в избранное",
"notification.favourite.name_and_others_with_link": "{name} и <a>{count, plural, one {# другие} other {# другие}}</a> отдали предпочтение вашему посту",
"notification.follow": "{name} подписался (-лась) на вас", "notification.follow": "{name} подписался (-лась) на вас",
"notification.follow_request": "{name} отправил запрос на подписку", "notification.follow_request": "{name} отправил запрос на подписку",
"notification.follow_request.name_and_others": "{name} и {count, plural, one {# другие} other {# другие}} последовали за тобой",
"notification.label.mention": "Упоминание",
"notification.label.private_mention": "Частное упоминание",
"notification.label.private_reply": "Частный ответ",
"notification.label.reply": "Ответить",
"notification.mention": "Упоминание",
"notification.moderation-warning.learn_more": "Узнать больше", "notification.moderation-warning.learn_more": "Узнать больше",
"notification.moderation_warning": "Вы получили предупреждение от модерации", "notification.moderation_warning": "Вы получили предупреждение от модерации",
"notification.moderation_warning.action_delete_statuses": "Некоторые из ваших публикаций были удалены.", "notification.moderation_warning.action_delete_statuses": "Некоторые из ваших публикаций были удалены.",
@ -483,7 +522,9 @@
"notification.moderation_warning.action_silence": "Ваша учётная запись была ограничена.", "notification.moderation_warning.action_silence": "Ваша учётная запись была ограничена.",
"notification.moderation_warning.action_suspend": "Действие вашей учётной записи приостановлено.", "notification.moderation_warning.action_suspend": "Действие вашей учётной записи приостановлено.",
"notification.own_poll": "Ваш опрос закончился", "notification.own_poll": "Ваш опрос закончился",
"notification.poll": "Голосование, в котором вы приняли участие, завершилось",
"notification.reblog": "{name} продвинул(а) ваш пост", "notification.reblog": "{name} продвинул(а) ваш пост",
"notification.reblog.name_and_others_with_link": "{name} и <a>{count, plural, one {# other} other {# others}}</a> увеличили ваш пост",
"notification.relationships_severance_event": "Потеряно соединение с {name}", "notification.relationships_severance_event": "Потеряно соединение с {name}",
"notification.relationships_severance_event.account_suspension": "Администратор {from} заблокировал {target}, что означает, что вы больше не сможете получать обновления от них или взаймодествовать с ними.", "notification.relationships_severance_event.account_suspension": "Администратор {from} заблокировал {target}, что означает, что вы больше не сможете получать обновления от них или взаймодествовать с ними.",
"notification.relationships_severance_event.domain_block": "Администратор {from} заблокировал {target} включая {followersCount} ваших подписчиков и {followingCount, plural, one {# аккаунт} few {# аккаунта} other {# аккаунтов}}, на которые вы подписаны.", "notification.relationships_severance_event.domain_block": "Администратор {from} заблокировал {target} включая {followersCount} ваших подписчиков и {followingCount, plural, one {# аккаунт} few {# аккаунта} other {# аккаунтов}}, на которые вы подписаны.",
@ -492,10 +533,19 @@
"notification.status": "{name} только что запостил", "notification.status": "{name} только что запостил",
"notification.update": "{name} изменил(а) пост", "notification.update": "{name} изменил(а) пост",
"notification_requests.accept": "Принять", "notification_requests.accept": "Принять",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Принять запрос} other {Принять запросы}}",
"notification_requests.confirm_accept_multiple.title": "Принимать запросы на уведомления?",
"notification_requests.confirm_dismiss_multiple.title": "Отклонять запросы на уведомления?",
"notification_requests.dismiss": "Отклонить", "notification_requests.dismiss": "Отклонить",
"notification_requests.edit_selection": "Редактировать",
"notification_requests.exit_selection": "Готово",
"notification_requests.explainer_for_limited_account": "Уведомления от этой учетной записи были отфильтрованы, поскольку учетная запись была ограничена модератором.",
"notification_requests.explainer_for_limited_remote_account": "Уведомления от этой учетной записи были отфильтрованы, поскольку учетная запись или ее сервер были ограничены модератором.",
"notification_requests.maximize": "Развернуть", "notification_requests.maximize": "Развернуть",
"notification_requests.minimize_banner": "Минимизация баннера отфильтрованных уведомлений",
"notification_requests.notifications_from": "Уведомления от {name}", "notification_requests.notifications_from": "Уведомления от {name}",
"notification_requests.title": "Отфильтрованные уведомления", "notification_requests.title": "Отфильтрованные уведомления",
"notification_requests.view": "Просмотр уведомлений",
"notifications.clear": "Очистить уведомления", "notifications.clear": "Очистить уведомления",
"notifications.clear_confirmation": "Вы уверены, что хотите очистить все уведомления?", "notifications.clear_confirmation": "Вы уверены, что хотите очистить все уведомления?",
"notifications.clear_title": "Сбросить уведомления?", "notifications.clear_title": "Сбросить уведомления?",
@ -530,7 +580,14 @@
"notifications.permission_denied": "Уведомления на рабочем столе недоступны, так как вы запретили их отправку в браузере. Проверьте настройки для сайта, чтобы включить их обратно.", "notifications.permission_denied": "Уведомления на рабочем столе недоступны, так как вы запретили их отправку в браузере. Проверьте настройки для сайта, чтобы включить их обратно.",
"notifications.permission_denied_alert": "Уведомления на рабочем столе недоступны, так как вы ранее отклонили запрос на их отправку.", "notifications.permission_denied_alert": "Уведомления на рабочем столе недоступны, так как вы ранее отклонили запрос на их отправку.",
"notifications.permission_required": "Чтобы включить уведомления на рабочем столе, необходимо разрешить их в браузере.", "notifications.permission_required": "Чтобы включить уведомления на рабочем столе, необходимо разрешить их в браузере.",
"notifications.policy.accept": "Принять",
"notifications.policy.accept_hint": "Показать в уведомлениях",
"notifications.policy.drop": "Игнорируем", "notifications.policy.drop": "Игнорируем",
"notifications.policy.drop_hint": "Отправить в пустоту, чтобы никогда больше не увидеть",
"notifications.policy.filter": "Фильтр",
"notifications.policy.filter_hint": "Отправка в папку фильтрованных уведомлений",
"notifications.policy.filter_limited_accounts_hint": "Ограничено модераторами сервера",
"notifications.policy.filter_limited_accounts_title": "Модерируемые аккаунты",
"notifications.policy.filter_new_accounts.hint": "Создано в течение последних {days, plural, one {один день} few {# дней} many {# дней} other {# дня}}", "notifications.policy.filter_new_accounts.hint": "Создано в течение последних {days, plural, one {один день} few {# дней} many {# дней} other {# дня}}",
"notifications.policy.filter_new_accounts_title": "Новые учётные записи", "notifications.policy.filter_new_accounts_title": "Новые учётные записи",
"notifications.policy.filter_not_followers_title": "Люди, не подписанные на вас", "notifications.policy.filter_not_followers_title": "Люди, не подписанные на вас",
@ -538,6 +595,7 @@
"notifications.policy.filter_not_following_title": "Люди, на которых вы не подписаны", "notifications.policy.filter_not_following_title": "Люди, на которых вы не подписаны",
"notifications.policy.filter_private_mentions_hint": "Фильтруется, если только это не ответ на ваше собственное упоминание или если вы подписаны на отправителя", "notifications.policy.filter_private_mentions_hint": "Фильтруется, если только это не ответ на ваше собственное упоминание или если вы подписаны на отправителя",
"notifications.policy.filter_private_mentions_title": "Нежелательные личные упоминания", "notifications.policy.filter_private_mentions_title": "Нежелательные личные упоминания",
"notifications.policy.title": "………Управлять уведомлениями от…",
"notifications_permission_banner.enable": "Включить уведомления", "notifications_permission_banner.enable": "Включить уведомления",
"notifications_permission_banner.how_to_control": "Получайте уведомления даже когда Mastodon закрыт, включив уведомления на рабочем столе. А чтобы лишний шум не отвлекал, вы можете настроить какие уведомления вы хотите получать, нажав на кнопку {icon} выше.", "notifications_permission_banner.how_to_control": "Получайте уведомления даже когда Mastodon закрыт, включив уведомления на рабочем столе. А чтобы лишний шум не отвлекал, вы можете настроить какие уведомления вы хотите получать, нажав на кнопку {icon} выше.",
"notifications_permission_banner.title": "Будьте в курсе происходящего", "notifications_permission_banner.title": "Будьте в курсе происходящего",
@ -666,6 +724,7 @@
"report_notification.categories.legal": "Правовая информация", "report_notification.categories.legal": "Правовая информация",
"report_notification.categories.legal_sentence": "срамной контент", "report_notification.categories.legal_sentence": "срамной контент",
"report_notification.categories.other": "Прочее", "report_notification.categories.other": "Прочее",
"report_notification.categories.other_sentence": "другое",
"report_notification.categories.spam": "Спам", "report_notification.categories.spam": "Спам",
"report_notification.categories.spam_sentence": "спам", "report_notification.categories.spam_sentence": "спам",
"report_notification.categories.violation": "Нарушение правил", "report_notification.categories.violation": "Нарушение правил",
@ -696,8 +755,11 @@
"server_banner.about_active_users": "Люди, заходившие на этот сервер за последние 30 дней (ежемесячные активные пользователи)", "server_banner.about_active_users": "Люди, заходившие на этот сервер за последние 30 дней (ежемесячные активные пользователи)",
"server_banner.active_users": "активные пользователи", "server_banner.active_users": "активные пользователи",
"server_banner.administered_by": "Управляется:", "server_banner.administered_by": "Управляется:",
"server_banner.is_one_of_many": "{domain} - это один из многих независимых серверов Mastodon, которые вы можете использовать для участия в fediverse.",
"server_banner.server_stats": "Статистика сервера:", "server_banner.server_stats": "Статистика сервера:",
"sign_in_banner.create_account": "Создать учётную запись", "sign_in_banner.create_account": "Создать учётную запись",
"sign_in_banner.follow_anyone": "Следите за любым человеком в федеральной вселенной и смотрите все в хронологическом порядке. Никаких алгоритмов, рекламы или клик бейта.",
"sign_in_banner.mastodon_is": "Mastodon - лучший способ быть в курсе всего происходящего.",
"sign_in_banner.sign_in": "Войти", "sign_in_banner.sign_in": "Войти",
"sign_in_banner.sso_redirect": "Войдите или Зарегистрируйтесь", "sign_in_banner.sso_redirect": "Войдите или Зарегистрируйтесь",
"status.admin_account": "Открыть интерфейс модератора для @{name}", "status.admin_account": "Открыть интерфейс модератора для @{name}",
@ -707,6 +769,7 @@
"status.bookmark": "Сохранить в закладки", "status.bookmark": "Сохранить в закладки",
"status.cancel_reblog_private": "Не продвигать", "status.cancel_reblog_private": "Не продвигать",
"status.cannot_reblog": "Этот пост не может быть продвинут", "status.cannot_reblog": "Этот пост не может быть продвинут",
"status.continued_thread": "Продолжение темы",
"status.copy": "Скопировать ссылку на пост", "status.copy": "Скопировать ссылку на пост",
"status.delete": "Удалить", "status.delete": "Удалить",
"status.detailed_status": "Подробный просмотр обсуждения", "status.detailed_status": "Подробный просмотр обсуждения",
@ -715,6 +778,7 @@
"status.edit": "Изменить", "status.edit": "Изменить",
"status.edited": "Дата последнего изменения: {date}", "status.edited": "Дата последнего изменения: {date}",
"status.edited_x_times": "{count, plural, one {{count} изменение} many {{count} изменений} other {{count} изменения}}", "status.edited_x_times": "{count, plural, one {{count} изменение} many {{count} изменений} other {{count} изменения}}",
"status.embed": "Получить код для встраивания",
"status.favourite": "Избранное", "status.favourite": "Избранное",
"status.filter": "Фильтровать этот пост", "status.filter": "Фильтровать этот пост",
"status.history.created": "{name} создал {date}", "status.history.created": "{name} создал {date}",
@ -737,6 +801,7 @@
"status.reblogs.empty": "Никто ещё не продвинул этот пост. Как только кто-то это сделает, они появятся здесь.", "status.reblogs.empty": "Никто ещё не продвинул этот пост. Как только кто-то это сделает, они появятся здесь.",
"status.redraft": "Создать заново", "status.redraft": "Создать заново",
"status.remove_bookmark": "Убрать из закладок", "status.remove_bookmark": "Убрать из закладок",
"status.replied_in_thread": "Ответил в теме",
"status.replied_to": "Ответил(а) {name}", "status.replied_to": "Ответил(а) {name}",
"status.reply": "Ответить", "status.reply": "Ответить",
"status.replyAll": "Ответить всем", "status.replyAll": "Ответить всем",

View file

@ -38,13 +38,20 @@ const scroll = (
const isScrollBehaviorSupported = const isScrollBehaviorSupported =
'scrollBehavior' in document.documentElement.style; 'scrollBehavior' in document.documentElement.style;
export const scrollRight = (node: Element, position: number) => { export const scrollRight = (node: Element, position: number) =>
if (isScrollBehaviorSupported) requestIdleCallback(() => {
if (isScrollBehaviorSupported) {
node.scrollTo({ left: position, behavior: 'smooth' }); node.scrollTo({ left: position, behavior: 'smooth' });
else scroll(node, 'scrollLeft', position); } else {
}; scroll(node, 'scrollLeft', position);
}
});
export const scrollTop = (node: Element) => { export const scrollTop = (node: Element) =>
if (isScrollBehaviorSupported) node.scrollTo({ top: 0, behavior: 'smooth' }); requestIdleCallback(() => {
else scroll(node, 'scrollTop', 0); if (isScrollBehaviorSupported) {
}; node.scrollTo({ top: 0, behavior: 'smooth' });
} else {
scroll(node, 'scrollTop', 0);
}
});

View file

@ -209,7 +209,6 @@ const KNOWN_EVENT_TYPES = [
'notification', 'notification',
'conversation', 'conversation',
'filters_changed', 'filters_changed',
'encrypted_message',
'announcement', 'announcement',
'announcement.delete', 'announcement.delete',
'announcement.reaction', 'announcement.reaction',

View file

@ -8,6 +8,14 @@ import { render as rtlRender } from '@testing-library/react';
import { IdentityContext } from './identity_context'; import { IdentityContext } from './identity_context';
beforeEach(() => {
global.requestIdleCallback = jest
.fn()
.mockImplementation((fn: () => void) => {
fn();
});
});
function render( function render(
ui: React.ReactElement, ui: React.ReactElement,
{ {

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M800-600v-120H680v-80h120q33 0 56.5 23.5T880-720v120h-80Zm-720 0v-120q0-33 23.5-56.5T160-800h120v80H160v120H80Zm600 440v-80h120v-120h80v120q0 33-23.5 56.5T800-160H680Zm-520 0q-33 0-56.5-23.5T80-240v-120h80v120h120v80H160Zm80-160v-320h480v320H240Z"/></svg>

After

Width:  |  Height:  |  Size: 352 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M800-600v-120H680v-80h120q33 0 56.5 23.5T880-720v120h-80Zm-720 0v-120q0-33 23.5-56.5T160-800h120v80H160v120H80Zm600 440v-80h120v-120h80v120q0 33-23.5 56.5T800-160H680Zm-520 0q-33 0-56.5-23.5T80-240v-120h80v120h120v80H160Zm80-160v-320h480v320H240Zm80-80h320v-160H320v160Zm0 0v-160 160Z"/></svg>

After

Width:  |  Height:  |  Size: 390 B

View file

@ -1,3 +1,5 @@
@use 'sass:color';
// Dependent colors // Dependent colors
$black: #000000; $black: #000000;
$white: #ffffff; $white: #ffffff;
@ -47,11 +49,19 @@ $account-background-color: $white !default;
// Invert darkened and lightened colors // Invert darkened and lightened colors
@function darken($color, $amount) { @function darken($color, $amount) {
@return hsl(hue($color), saturation($color), lightness($color) + $amount); @return hsl(
hue($color),
color.channel($color, 'saturation', $space: hsl),
color.channel($color, 'lightness', $space: hsl) + $amount
);
} }
@function lighten($color, $amount) { @function lighten($color, $amount) {
@return hsl(hue($color), saturation($color), lightness($color) - $amount); @return hsl(
hue($color),
color.channel($color, 'saturation', $space: hsl),
color.channel($color, 'lightness', $space: hsl) - $amount
);
} }
$emojis-requiring-inversion: 'chains'; $emojis-requiring-inversion: 'chains';

View file

@ -2032,13 +2032,14 @@ body > [data-popper-placement] {
display: block; display: block;
position: relative; position: relative;
border-radius: var(--avatar-border-radius); border-radius: var(--avatar-border-radius);
background-color: var(--surface-background-color);
img { img {
display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
border-radius: var(--avatar-border-radius); border-radius: var(--avatar-border-radius);
display: inline-block; // to not show broken images
} }
&-inline { &-inline {
@ -5763,9 +5764,23 @@ a.status-card {
height: 100%; height: 100%;
position: relative; position: relative;
&__close, &__buttons {
&__zoom-button { position: absolute;
inset-inline-end: 8px;
top: 8px;
z-index: 100;
display: flex;
gap: 8px;
align-items: center;
.icon-button {
color: rgba($white, 0.7); color: rgba($white, 0.7);
padding: 8px;
.icon {
width: 24px;
height: 24px;
}
&:hover, &:hover,
&:focus, &:focus,
@ -5779,6 +5794,7 @@ a.status-card {
} }
} }
} }
}
.media-modal__closer { .media-modal__closer {
position: absolute; position: absolute;
@ -5936,28 +5952,6 @@ a.status-card {
} }
} }
.media-modal__close {
position: absolute;
inset-inline-end: 8px;
top: 8px;
z-index: 100;
}
.media-modal__zoom-button {
position: absolute;
inset-inline-end: 64px;
top: 8px;
z-index: 100;
pointer-events: auto;
transition: opacity 0.3s linear;
will-change: opacity;
}
.media-modal__zoom-button--hidden {
pointer-events: none;
opacity: 0;
}
.onboarding-modal, .onboarding-modal,
.error-modal, .error-modal,
.embed-modal { .embed-modal {

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.1002 20.2C2.46686 20.2 1.9252 19.9833 1.4752 19.55C1.04186 19.1 0.825195 18.5583 0.825195 17.925V6.07499C0.825195 5.44165 1.04186 4.90832 1.4752 4.47499C1.9252 4.02499 2.46686 3.79999 3.1002 3.79999H20.9002C21.5335 3.79999 22.0669 4.02499 22.5002 4.47499C22.9502 4.90832 23.1752 5.44165 23.1752 6.07499V17.925C23.1752 18.5583 22.9502 19.1 22.5002 19.55C22.0669 19.9833 21.5335 20.2 20.9002 20.2H3.1002ZM3.1002 17.925H20.9002V6.07499H3.1002V17.925Z" fill="black"/>
<path d="M8.12522 16V9.85782H6.25043V8H10V16H8.12522ZM11.1461 16V14.1422H13.0209V16H11.1461ZM15.1252 16V9.85782H13.2313V8H17V16H15.1252ZM11.1461 12.8578V11H13.0209V12.8578H11.1461Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 776 B

View file

@ -8,44 +8,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
dereference_object! dereference_object!
case @object['type']
when 'EncryptedMessage'
create_encrypted_message
else
create_status create_status
end end
end
private private
def create_encrypted_message
return reject_payload! if non_matching_uri_hosts?(@account.uri, object_uri) || @options[:delivered_to_account_id].blank?
target_account = Account.find(@options[:delivered_to_account_id])
target_device = target_account.devices.find_by(device_id: @object.dig('to', 'deviceId'))
return if target_device.nil?
target_device.encrypted_messages.create!(
from_account: @account,
from_device_id: @object.dig('attributedTo', 'deviceId'),
type: @object['messageType'],
body: @object['cipherText'],
digest: @object.dig('digest', 'digestValue'),
message_franking: message_franking.to_token
)
end
def message_franking
MessageFranking.new(
hmac: @object.dig('digest', 'digestValue'),
original_franking: @object['messageFranking'],
source_account_id: @account.id,
target_account_id: @options[:delivered_to_account_id],
timestamp: Time.now.utc
)
end
def create_status def create_status
return reject_payload! if unsupported_object_type? || non_matching_uri_hosts?(@account.uri, object_uri) || tombstone_exists? || !related_to_local_activity? return reject_payload! if unsupported_object_type? || non_matching_uri_hosts?(@account.uri, object_uri) || tombstone_exists? || !related_to_local_activity?

View file

@ -618,7 +618,7 @@ class FeedManager
arr = crutches[:active_mentions][s.id] || [] arr = crutches[:active_mentions][s.id] || []
arr.push(s.account_id) arr.push(s.account_id)
if s.reblog? if s.reblog? && s.reblog.present?
arr.push(s.reblog.account_id) arr.push(s.reblog.account_id)
arr.concat(crutches[:active_mentions][s.reblog_of_id] || []) arr.concat(crutches[:active_mentions][s.reblog_of_id] || [])
end end

View file

@ -20,8 +20,6 @@ class InlineRenderer
serializer = REST::AnnouncementSerializer serializer = REST::AnnouncementSerializer
when :reaction when :reaction
serializer = REST::ReactionSerializer serializer = REST::ReactionSerializer
when :encrypted_message
serializer = REST::EncryptedMessageSerializer
else else
return return
end end

View file

@ -77,7 +77,7 @@ class Request
@url = Addressable::URI.parse(url).normalize @url = Addressable::URI.parse(url).normalize
@http_client = options.delete(:http_client) @http_client = options.delete(:http_client)
@allow_local = options.delete(:allow_local) @allow_local = options.delete(:allow_local)
@full_path = options.delete(:with_query_string) @full_path = !options.delete(:omit_query_string)
@options = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket) @options = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket)
@options = @options.merge(timeout_class: PerOperationWithDeadline, timeout_options: TIMEOUT) @options = @options.merge(timeout_class: PerOperationWithDeadline, timeout_options: TIMEOUT)
@options = @options.merge(proxy_url) if use_proxy? @options = @options.merge(proxy_url) if use_proxy?

View file

@ -1,13 +0,0 @@
# frozen_string_literal: true
class Vacuum::SystemKeysVacuum
def perform
vacuum_expired_system_keys!
end
private
def vacuum_expired_system_keys!
SystemKey.expired.delete_all
end
end

View file

@ -56,9 +56,11 @@ class AdminMailer < ApplicationMailer
def new_critical_software_updates def new_critical_software_updates
@software_updates = SoftwareUpdate.where(urgent: true).to_a.sort_by(&:gem_version) @software_updates = SoftwareUpdate.where(urgent: true).to_a.sort_by(&:gem_version)
headers['Priority'] = 'urgent' headers(
headers['X-Priority'] = '1' 'Importance' => 'high',
headers['Importance'] = 'high' 'Priority' => 'urgent',
'X-Priority' => '1'
)
locale_for_account(@me) do locale_for_account(@me) do
mail subject: default_i18n_subject(instance: @instance) mail subject: default_i18n_subject(instance: @instance)

View file

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

View file

@ -6,7 +6,10 @@ class NotificationMailer < ApplicationMailer
:routing :routing
before_action :process_params before_action :process_params
before_action :set_status, only: [:mention, :favourite, :reblog] with_options only: %i(mention favourite reblog) do
before_action :set_status
after_action :thread_by_conversation!
end
before_action :set_account, only: [:follow, :favourite, :reblog, :follow_request] before_action :set_account, only: [:follow, :favourite, :reblog, :follow_request]
after_action :set_list_headers! after_action :set_list_headers!
@ -18,7 +21,6 @@ class NotificationMailer < ApplicationMailer
return unless @user.functional? && @status.present? return unless @user.functional? && @status.present?
locale_for_account(@me) do locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail subject: default_i18n_subject(name: @status.account.acct) mail subject: default_i18n_subject(name: @status.account.acct)
end end
end end
@ -35,7 +37,6 @@ class NotificationMailer < ApplicationMailer
return unless @user.functional? && @status.present? return unless @user.functional? && @status.present?
locale_for_account(@me) do locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail subject: default_i18n_subject(name: @account.acct) mail subject: default_i18n_subject(name: @account.acct)
end end
end end
@ -44,7 +45,6 @@ class NotificationMailer < ApplicationMailer
return unless @user.functional? && @status.present? return unless @user.functional? && @status.present?
locale_for_account(@me) do locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail subject: default_i18n_subject(name: @account.acct) mail subject: default_i18n_subject(name: @account.acct)
end end
end end
@ -76,17 +76,21 @@ class NotificationMailer < ApplicationMailer
end end
def set_list_headers! def set_list_headers!
headers['List-ID'] = "<#{@type}.#{@me.username}.#{Rails.configuration.x.local_domain}>" headers(
headers['List-Unsubscribe'] = "<#{@unsubscribe_url}>" 'List-ID' => "<#{@type}.#{@me.username}.#{Rails.configuration.x.local_domain}>",
headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click' 'List-Unsubscribe-Post' => 'List-Unsubscribe=One-Click',
'List-Unsubscribe' => "<#{@unsubscribe_url}>"
)
end end
def thread_by_conversation(conversation) def thread_by_conversation!
return if conversation.nil? return if @status.conversation.nil?
msg_id = "<conversation-#{conversation.id}.#{conversation.created_at.strftime('%Y-%m-%d')}@#{Rails.configuration.x.local_domain}>" conversation_message_id = "<conversation-#{@status.conversation.id}.#{@status.conversation.created_at.to_date}@#{Rails.configuration.x.local_domain}>"
headers['In-Reply-To'] = msg_id headers(
headers['References'] = msg_id 'In-Reply-To' => conversation_message_id,
'References' => conversation_message_id
)
end end
end end

View file

@ -44,7 +44,6 @@
# hide_collections :boolean # hide_collections :boolean
# avatar_storage_schema_version :integer # avatar_storage_schema_version :integer
# header_storage_schema_version :integer # header_storage_schema_version :integer
# devices_url :string
# suspension_origin :integer # suspension_origin :integer
# sensitized_at :datetime # sensitized_at :datetime
# trendable :boolean # trendable :boolean
@ -56,11 +55,12 @@
class Account < ApplicationRecord class Account < ApplicationRecord
self.ignored_columns += %w( self.ignored_columns += %w(
subscription_expires_at devices_url
secret hub_url
remote_url remote_url
salmon_url salmon_url
hub_url secret
subscription_expires_at
trust_level trust_level
) )

View file

@ -7,9 +7,6 @@ module Account::Associations
# Local users # Local users
has_one :user, inverse_of: :account, dependent: :destroy has_one :user, inverse_of: :account, dependent: :destroy
# E2EE
has_many :devices, dependent: :destroy, inverse_of: :account
# Timelines # Timelines
has_many :statuses, inverse_of: :account, dependent: :destroy has_many :statuses, inverse_of: :account, dependent: :destroy
has_many :favourites, inverse_of: :account, dependent: :destroy has_many :favourites, inverse_of: :account, dependent: :destroy

View file

@ -3,6 +3,11 @@
module Reviewable module Reviewable
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do
scope :reviewed, -> { where.not(reviewed_at: nil) }
scope :unreviewed, -> { where(reviewed_at: nil) }
end
def requires_review? def requires_review?
reviewed_at.nil? reviewed_at.nil?
end end

View file

@ -1,36 +0,0 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: devices
#
# id :bigint(8) not null, primary key
# access_token_id :bigint(8)
# account_id :bigint(8)
# device_id :string default(""), not null
# name :string default(""), not null
# fingerprint_key :text default(""), not null
# identity_key :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class Device < ApplicationRecord
belongs_to :access_token, class_name: 'Doorkeeper::AccessToken'
belongs_to :account
has_many :one_time_keys, dependent: :destroy, inverse_of: :device
has_many :encrypted_messages, dependent: :destroy, inverse_of: :device
validates :name, :fingerprint_key, :identity_key, presence: true
validates :fingerprint_key, :identity_key, ed25519_key: true
before_save :invalidate_associations, if: -> { device_id_changed? || fingerprint_key_changed? || identity_key_changed? }
private
def invalidate_associations
one_time_keys.destroy_all
encrypted_messages.destroy_all
end
end

View file

@ -1,49 +0,0 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: encrypted_messages
#
# id :bigint(8) not null, primary key
# device_id :bigint(8)
# from_account_id :bigint(8)
# from_device_id :string default(""), not null
# type :integer default(0), not null
# body :text default(""), not null
# digest :text default(""), not null
# message_franking :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class EncryptedMessage < ApplicationRecord
self.inheritance_column = nil
include Paginable
include Redisable
scope :up_to, ->(id) { where(arel_table[:id].lteq(id)) }
belongs_to :device
belongs_to :from_account, class_name: 'Account'
around_create Mastodon::Snowflake::Callbacks
after_commit :push_to_streaming_api
private
def push_to_streaming_api
return if destroyed? || !subscribed_to_timeline?
PushEncryptedMessageWorker.perform_async(id)
end
def subscribed_to_timeline?
redis.exists?("subscribed:#{streaming_channel}")
end
def streaming_channel
"timeline:#{device.account_id}:#{device.device_id}"
end
end

View file

@ -1,19 +0,0 @@
# frozen_string_literal: true
class MessageFranking
attr_reader :hmac, :source_account_id, :target_account_id,
:timestamp, :original_franking
def initialize(attributes = {})
@hmac = attributes[:hmac]
@source_account_id = attributes[:source_account_id]
@target_account_id = attributes[:target_account_id]
@timestamp = attributes[:timestamp]
@original_franking = attributes[:original_franking]
end
def to_token
crypt = ActiveSupport::MessageEncryptor.new(SystemKey.current_key, serializer: Oj)
crypt.encrypt_and_sign(self)
end
end

View file

@ -1,22 +0,0 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: one_time_keys
#
# id :bigint(8) not null, primary key
# device_id :bigint(8)
# key_id :string default(""), not null
# key :text default(""), not null
# signature :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class OneTimeKey < ApplicationRecord
belongs_to :device
validates :key_id, :key, :signature, presence: true
validates :key, ed25519_key: true
validates :signature, ed25519_signature: { message: :key, verify_key: ->(one_time_key) { one_time_key.device.fingerprint_key } }
end

View file

@ -34,8 +34,6 @@ class PreviewCardProvider < ApplicationRecord
scope :trendable, -> { where(trendable: true) } scope :trendable, -> { where(trendable: true) }
scope :not_trendable, -> { where(trendable: false) } scope :not_trendable, -> { where(trendable: false) }
scope :reviewed, -> { where.not(reviewed_at: nil) }
scope :pending_review, -> { where(reviewed_at: nil) }
def self.matching_domain(domain) def self.matching_domain(domain)
segments = domain.split('.') segments = domain.split('.')

View file

@ -1,41 +0,0 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: system_keys
#
# id :bigint(8) not null, primary key
# key :binary
# created_at :datetime not null
# updated_at :datetime not null
#
class SystemKey < ApplicationRecord
ROTATION_PERIOD = 1.week.freeze
before_validation :set_key
scope :expired, ->(now = Time.now.utc) { where(arel_table[:created_at].lt(now - (ROTATION_PERIOD * 3))) }
class << self
def current_key
previous_key = order(id: :asc).last
if previous_key && previous_key.created_at >= ROTATION_PERIOD.ago
previous_key.key
else
create.key
end
end
end
private
def set_key
return if key.present?
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
self.key = cipher.random_key
end
end

View file

@ -50,8 +50,6 @@ class Tag < ApplicationRecord
validate :validate_name_change, if: -> { !new_record? && name_changed? } validate :validate_name_change, if: -> { !new_record? && name_changed? }
validate :validate_display_name_change, if: -> { !new_record? && display_name_changed? } validate :validate_display_name_change, if: -> { !new_record? && display_name_changed? }
scope :reviewed, -> { where.not(reviewed_at: nil) }
scope :unreviewed, -> { where(reviewed_at: nil) }
scope :pending_review, -> { unreviewed.where.not(requested_review_at: nil) } scope :pending_review, -> { unreviewed.where.not(requested_review_at: nil) }
scope :usable, -> { where(usable: [true, nil]) } scope :usable, -> { where(usable: [true, nil]) }
scope :not_usable, -> { where(usable: false) } scope :not_usable, -> { where(usable: false) }
@ -127,7 +125,7 @@ class Tag < ApplicationRecord
query = Tag.matches_name(stripped_term) query = Tag.matches_name(stripped_term)
query = query.merge(Tag.listable) if options[:exclude_unlistable] query = query.merge(Tag.listable) if options[:exclude_unlistable]
query = query.merge(matching_name(stripped_term).or(where.not(reviewed_at: nil))) if options[:exclude_unreviewed] query = query.merge(matching_name(stripped_term).or(reviewed)) if options[:exclude_unreviewed]
query.order(Arel.sql('length(name) ASC, name ASC')) query.order(Arel.sql('length(name) ASC, name ASC'))
.limit(limit) .limit(limit)

View file

@ -41,7 +41,7 @@ class Trends::PreviewCardProviderFilter
when 'rejected' when 'rejected'
PreviewCardProvider.not_trendable PreviewCardProvider.not_trendable
when 'pending_review' when 'pending_review'
PreviewCardProvider.pending_review PreviewCardProvider.unreviewed
else else
raise Mastodon::InvalidParameterError, "Unknown status: #{value}" raise Mastodon::InvalidParameterError, "Unknown status: #{value}"
end end

View file

@ -26,16 +26,5 @@ class ActivityPub::ActivityPresenter < ActiveModelSerializers::Model
end end
end end
end end
def from_encrypted_message(encrypted_message)
new.tap do |presenter|
presenter.id = ActivityPub::TagManager.instance.generate_uri_for(nil)
presenter.type = 'Create'
presenter.actor = ActivityPub::TagManager.instance.uri_for(encrypted_message.source_account)
presenter.published = Time.now.utc
presenter.to = ActivityPub::TagManager.instance.uri_for(encrypted_message.target_account)
presenter.virtual_object = encrypted_message
end
end
end end
end end

View file

@ -5,8 +5,6 @@ class ActivityPub::ActivitySerializer < ActivityPub::Serializer
case model.class.name case model.class.name
when 'Status' when 'Status'
ActivityPub::NoteSerializer ActivityPub::NoteSerializer
when 'DeliverToDeviceService::EncryptedMessage'
ActivityPub::EncryptedMessageSerializer
else else
super super
end end

View file

@ -7,7 +7,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
context :security context :security
context_extensions :manually_approves_followers, :featured, :also_known_as, context_extensions :manually_approves_followers, :featured, :also_known_as,
:moved_to, :property_value, :discoverable, :olm, :suspended, :moved_to, :property_value, :discoverable, :suspended,
:memorial, :indexable, :attribution_domains :memorial, :indexable, :attribution_domains
attributes :id, :type, :following, :followers, attributes :id, :type, :following, :followers,
@ -21,7 +21,6 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
has_many :virtual_tags, key: :tag has_many :virtual_tags, key: :tag
has_many :virtual_attachments, key: :attachment has_many :virtual_attachments, key: :attachment
attribute :devices, unless: :instance_actor?
attribute :moved_to, if: :moved? attribute :moved_to, if: :moved?
attribute :also_known_as, if: :also_known_as? attribute :also_known_as, if: :also_known_as?
attribute :suspended, if: :suspended? attribute :suspended, if: :suspended?
@ -72,10 +71,6 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
object.instance_actor? ? instance_actor_inbox_url : account_inbox_url(object) object.instance_actor? ? instance_actor_inbox_url : account_inbox_url(object)
end end
def devices
account_collection_url(object, :devices)
end
def outbox def outbox
object.instance_actor? ? instance_actor_outbox_url : account_outbox_url(object) object.instance_actor? ? instance_actor_outbox_url : account_outbox_url(object)
end end

View file

@ -14,8 +14,6 @@ class ActivityPub::CollectionSerializer < ActivityPub::Serializer
case model.class.name case model.class.name
when 'Status' when 'Status'
ActivityPub::NoteSerializer ActivityPub::NoteSerializer
when 'Device'
ActivityPub::DeviceSerializer
when 'FeaturedTag' when 'FeaturedTag'
ActivityPub::HashtagSerializer ActivityPub::HashtagSerializer
when 'ActivityPub::CollectionPresenter' when 'ActivityPub::CollectionPresenter'

View file

@ -1,52 +0,0 @@
# frozen_string_literal: true
class ActivityPub::DeviceSerializer < ActivityPub::Serializer
context_extensions :olm
include RoutingHelper
class FingerprintKeySerializer < ActivityPub::Serializer
attributes :type, :public_key_base64
def type
'Ed25519Key'
end
def public_key_base64
object.fingerprint_key
end
end
class IdentityKeySerializer < ActivityPub::Serializer
attributes :type, :public_key_base64
def type
'Curve25519Key'
end
def public_key_base64
object.identity_key
end
end
attributes :device_id, :type, :name, :claim
has_one :fingerprint_key, serializer: FingerprintKeySerializer
has_one :identity_key, serializer: IdentityKeySerializer
def type
'Device'
end
def claim
account_claim_url(object.account, id: object.device_id)
end
def fingerprint_key
object
end
def identity_key
object
end
end

View file

@ -1,61 +0,0 @@
# frozen_string_literal: true
class ActivityPub::EncryptedMessageSerializer < ActivityPub::Serializer
context :security
context_extensions :olm
class DeviceSerializer < ActivityPub::Serializer
attributes :type, :device_id
def type
'Device'
end
def device_id
object
end
end
class DigestSerializer < ActivityPub::Serializer
attributes :type, :digest_algorithm, :digest_value
def type
'Digest'
end
def digest_algorithm
'http://www.w3.org/2000/09/xmldsig#hmac-sha256'
end
def digest_value
object
end
end
attributes :type, :message_type, :cipher_text, :message_franking
has_one :attributed_to, serializer: DeviceSerializer
has_one :to, serializer: DeviceSerializer
has_one :digest, serializer: DigestSerializer
def type
'EncryptedMessage'
end
def attributed_to
object.source_device.device_id
end
def to
object.target_device_id
end
def message_type
object.type
end
def cipher_text
object.body
end
end

View file

@ -1,35 +0,0 @@
# frozen_string_literal: true
class ActivityPub::OneTimeKeySerializer < ActivityPub::Serializer
context :security
context_extensions :olm
class SignatureSerializer < ActivityPub::Serializer
attributes :type, :signature_value
def type
'Ed25519Signature'
end
def signature_value
object.signature
end
end
attributes :key_id, :type, :public_key_base64
has_one :signature, serializer: SignatureSerializer
def type
'Curve25519Key'
end
def public_key_base64
object.key
end
def signature
object
end
end

View file

@ -2,12 +2,40 @@
class OEmbedSerializer < ActiveModel::Serializer class OEmbedSerializer < ActiveModel::Serializer
INLINE_STYLES = { INLINE_STYLES = {
blockquote: 'max-width: 540px; min-width: 270px; background:#FCF8FF; border: 1px solid #C9C4DA; border-radius: 8px; overflow: hidden; margin: 0; padding: 0;', blockquote: <<~CSS.squish,
a: "color: #1C1A25; text-decoration: none; display: flex; align-items: center; justify-content: center; flex-direction: column; padding: 24px; font-size: 14px; line-height: 20px; letter-spacing: 0.25px; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Roboto, sans-serif;", # rubocop:disable Layout/LineLength background: #FCF8FF;
div0: 'margin-top: 16px; color: #787588;', border-radius: 8px;
div1: 'font-weight: 500;', border: 1px solid #C9C4DA;
margin: 0;
max-width: 540px;
min-width: 270px;
overflow: hidden;
padding: 0;
CSS
status_link: <<~CSS.squish,
align-items: center;
color: #1C1A25;
display: flex;
flex-direction: column;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Roboto, sans-serif;
font-size: 14px;
justify-content: center;
letter-spacing: 0.25px;
line-height: 20px;
padding: 24px;
text-decoration: none;
CSS
div_account: <<~CSS.squish,
color: #787588;
margin-top: 16px;
CSS
div_view: <<~CSS.squish,
font-weight: 500;
CSS
}.freeze }.freeze
DEFAULT_WIDTH = 400
include RoutingHelper include RoutingHelper
include ActionView::Helpers::TagHelper include ActionView::Helpers::TagHelper
@ -46,10 +74,10 @@ class OEmbedSerializer < ActiveModel::Serializer
def html def html
<<~HTML.squish <<~HTML.squish
<blockquote class="mastodon-embed" data-embed-url="#{embed_short_account_status_url(object.account, object)}" style="#{INLINE_STYLES[:blockquote]}"> <blockquote class="mastodon-embed" data-embed-url="#{embed_short_account_status_url(object.account, object)}" style="#{INLINE_STYLES[:blockquote]}">
<a href="#{short_account_status_url(object.account, object)}" target="_blank" style="#{INLINE_STYLES[:a]}"> <a href="#{short_account_status_url(object.account, object)}" target="_blank" style="#{INLINE_STYLES[:status_link]}">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 79 75"><path d="M74.7135 16.6043C73.6199 8.54587 66.5351 2.19527 58.1366 0.964691C56.7196 0.756754 51.351 0 38.9148 0H38.822C26.3824 0 23.7135 0.756754 22.2966 0.964691C14.1319 2.16118 6.67571 7.86752 4.86669 16.0214C3.99657 20.0369 3.90371 24.4888 4.06535 28.5726C4.29578 34.4289 4.34049 40.275 4.877 46.1075C5.24791 49.9817 5.89495 53.8251 6.81328 57.6088C8.53288 64.5968 15.4938 70.4122 22.3138 72.7848C29.6155 75.259 37.468 75.6697 44.9919 73.971C45.8196 73.7801 46.6381 73.5586 47.4475 73.3063C49.2737 72.7302 51.4164 72.086 52.9915 70.9542C53.0131 70.9384 53.0308 70.9178 53.0433 70.8942C53.0558 70.8706 53.0628 70.8445 53.0637 70.8179V65.1661C53.0634 65.1412 53.0574 65.1167 53.0462 65.0944C53.035 65.0721 53.0189 65.0525 52.9992 65.0371C52.9794 65.0218 52.9564 65.011 52.9318 65.0056C52.9073 65.0002 52.8819 65.0003 52.8574 65.0059C48.0369 66.1472 43.0971 66.7193 38.141 66.7103C29.6118 66.7103 27.3178 62.6981 26.6609 61.0278C26.1329 59.5842 25.7976 58.0784 25.6636 56.5486C25.6622 56.5229 25.667 56.4973 25.6775 56.4738C25.688 56.4502 25.7039 56.4295 25.724 56.4132C25.7441 56.397 25.7678 56.3856 25.7931 56.3801C25.8185 56.3746 25.8448 56.3751 25.8699 56.3816C30.6101 57.5151 35.4693 58.0873 40.3455 58.086C41.5183 58.086 42.6876 58.086 43.8604 58.0553C48.7647 57.919 53.9339 57.6701 58.7591 56.7361C58.8794 56.7123 58.9998 56.6918 59.103 56.6611C66.7139 55.2124 73.9569 50.665 74.6929 39.1501C74.7204 38.6967 74.7892 34.4016 74.7892 33.9312C74.7926 32.3325 75.3085 22.5901 74.7135 16.6043ZM62.9996 45.3371H54.9966V25.9069C54.9966 21.8163 53.277 19.7302 49.7793 19.7302C45.9343 19.7302 44.0083 22.1981 44.0083 27.0727V37.7082H36.0534V27.0727C36.0534 22.1981 34.124 19.7302 30.279 19.7302C26.8019 19.7302 25.0651 21.8163 25.0617 25.9069V45.3371H17.0656V25.3172C17.0656 21.2266 18.1191 17.9769 20.2262 15.568C22.3998 13.1648 25.2509 11.9308 28.7898 11.9308C32.8859 11.9308 35.9812 13.492 38.0447 16.6111L40.036 19.9245L42.0308 16.6111C44.0943 13.492 47.1896 11.9308 51.2788 11.9308C54.8143 11.9308 57.6654 13.1648 59.8459 15.568C61.9529 17.9746 63.0065 21.2243 63.0065 25.3172L62.9996 45.3371Z" fill="currentColor"/></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 79 75"><path d="M74.7135 16.6043C73.6199 8.54587 66.5351 2.19527 58.1366 0.964691C56.7196 0.756754 51.351 0 38.9148 0H38.822C26.3824 0 23.7135 0.756754 22.2966 0.964691C14.1319 2.16118 6.67571 7.86752 4.86669 16.0214C3.99657 20.0369 3.90371 24.4888 4.06535 28.5726C4.29578 34.4289 4.34049 40.275 4.877 46.1075C5.24791 49.9817 5.89495 53.8251 6.81328 57.6088C8.53288 64.5968 15.4938 70.4122 22.3138 72.7848C29.6155 75.259 37.468 75.6697 44.9919 73.971C45.8196 73.7801 46.6381 73.5586 47.4475 73.3063C49.2737 72.7302 51.4164 72.086 52.9915 70.9542C53.0131 70.9384 53.0308 70.9178 53.0433 70.8942C53.0558 70.8706 53.0628 70.8445 53.0637 70.8179V65.1661C53.0634 65.1412 53.0574 65.1167 53.0462 65.0944C53.035 65.0721 53.0189 65.0525 52.9992 65.0371C52.9794 65.0218 52.9564 65.011 52.9318 65.0056C52.9073 65.0002 52.8819 65.0003 52.8574 65.0059C48.0369 66.1472 43.0971 66.7193 38.141 66.7103C29.6118 66.7103 27.3178 62.6981 26.6609 61.0278C26.1329 59.5842 25.7976 58.0784 25.6636 56.5486C25.6622 56.5229 25.667 56.4973 25.6775 56.4738C25.688 56.4502 25.7039 56.4295 25.724 56.4132C25.7441 56.397 25.7678 56.3856 25.7931 56.3801C25.8185 56.3746 25.8448 56.3751 25.8699 56.3816C30.6101 57.5151 35.4693 58.0873 40.3455 58.086C41.5183 58.086 42.6876 58.086 43.8604 58.0553C48.7647 57.919 53.9339 57.6701 58.7591 56.7361C58.8794 56.7123 58.9998 56.6918 59.103 56.6611C66.7139 55.2124 73.9569 50.665 74.6929 39.1501C74.7204 38.6967 74.7892 34.4016 74.7892 33.9312C74.7926 32.3325 75.3085 22.5901 74.7135 16.6043ZM62.9996 45.3371H54.9966V25.9069C54.9966 21.8163 53.277 19.7302 49.7793 19.7302C45.9343 19.7302 44.0083 22.1981 44.0083 27.0727V37.7082H36.0534V27.0727C36.0534 22.1981 34.124 19.7302 30.279 19.7302C26.8019 19.7302 25.0651 21.8163 25.0617 25.9069V45.3371H17.0656V25.3172C17.0656 21.2266 18.1191 17.9769 20.2262 15.568C22.3998 13.1648 25.2509 11.9308 28.7898 11.9308C32.8859 11.9308 35.9812 13.492 38.0447 16.6111L40.036 19.9245L42.0308 16.6111C44.0943 13.492 47.1896 11.9308 51.2788 11.9308C54.8143 11.9308 57.6654 13.1648 59.8459 15.568C61.9529 17.9746 63.0065 21.2243 63.0065 25.3172L62.9996 45.3371Z" fill="currentColor"/></svg>
<div style="#{INLINE_STYLES[:div0]}">Post by @#{object.account.pretty_acct}@#{provider_name}</div> <div style="#{INLINE_STYLES[:div_account]}">Post by @#{object.account.pretty_acct}@#{provider_name}</div>
<div style="#{INLINE_STYLES[:div1]}">View on Mastodon</div> <div style="#{INLINE_STYLES[:div_view]}">View on Mastodon</div>
</a> </a>
</blockquote> </blockquote>
<script data-allowed-prefixes="#{root_url}" async src="#{full_asset_url('embed.js', skip_pipeline: true)}"></script> <script data-allowed-prefixes="#{root_url}" async src="#{full_asset_url('embed.js', skip_pipeline: true)}"></script>
@ -57,10 +85,10 @@ class OEmbedSerializer < ActiveModel::Serializer
end end
def width def width
instance_options[:width] (instance_options[:width] || DEFAULT_WIDTH).to_i
end end
def height def height
instance_options[:height] instance_options[:height].presence&.to_i
end end
end end

View file

@ -1,19 +0,0 @@
# frozen_string_literal: true
class REST::EncryptedMessageSerializer < ActiveModel::Serializer
attributes :id, :account_id, :device_id,
:type, :body, :digest, :message_franking,
:created_at
def id
object.id.to_s
end
def account_id
object.from_account_id.to_s
end
def device_id
object.from_device_id
end
end

View file

@ -1,9 +0,0 @@
# frozen_string_literal: true
class REST::Keys::ClaimResultSerializer < ActiveModel::Serializer
attributes :account_id, :device_id, :key_id, :key, :signature
def account_id
object.account.id.to_s
end
end

View file

@ -1,6 +0,0 @@
# frozen_string_literal: true
class REST::Keys::DeviceSerializer < ActiveModel::Serializer
attributes :device_id, :name, :identity_key,
:fingerprint_key
end

View file

@ -1,11 +0,0 @@
# frozen_string_literal: true
class REST::Keys::QueryResultSerializer < ActiveModel::Serializer
attributes :account_id
has_many :devices, serializer: REST::Keys::DeviceSerializer
def account_id
object.account.id.to_s
end
end

View file

@ -49,7 +49,7 @@ class ActivityPub::FetchRepliesService < BaseService
rescue Mastodon::UnexpectedResponseError => e rescue Mastodon::UnexpectedResponseError => e
raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present? raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present?
fetch_resource_without_id_validation(collection_or_uri, nil, true, request_options: { with_query_string: true }) fetch_resource_without_id_validation(collection_or_uri, nil, true, request_options: { omit_query_string: false })
end end
end end

View file

@ -108,7 +108,6 @@ class ActivityPub::ProcessAccountService < BaseService
def set_immediate_attributes! def set_immediate_attributes!
@account.featured_collection_url = @json['featured'] || '' @account.featured_collection_url = @json['featured'] || ''
@account.devices_url = @json['devices'] || ''
@account.display_name = @json['name'] || '' @account.display_name = @json['name'] || ''
@account.note = @json['summary'] || '' @account.note = @json['summary'] || ''
@account.locked = @json['manuallyApprovesFollowers'] || false @account.locked = @json['manuallyApprovesFollowers'] || false

View file

@ -13,7 +13,6 @@ class DeleteAccountService < BaseService
conversation_mutes conversation_mutes
conversations conversations
custom_filters custom_filters
devices
domain_blocks domain_blocks
featured_tags featured_tags
follow_requests follow_requests
@ -40,7 +39,6 @@ class DeleteAccountService < BaseService
conversation_mutes conversation_mutes
conversations conversations
custom_filters custom_filters
devices
domain_blocks domain_blocks
featured_tags featured_tags
follow_requests follow_requests

View file

@ -1,78 +0,0 @@
# frozen_string_literal: true
class DeliverToDeviceService < BaseService
include Payloadable
class EncryptedMessage < ActiveModelSerializers::Model
attributes :source_account, :target_account, :source_device,
:target_device_id, :type, :body, :digest,
:message_franking
end
def call(source_account, source_device, options = {})
@source_account = source_account
@source_device = source_device
@target_account = Account.find(options[:account_id])
@target_device_id = options[:device_id]
@body = options[:body]
@type = options[:type]
@hmac = options[:hmac]
set_message_franking!
if @target_account.local?
deliver_to_local!
else
deliver_to_remote!
end
end
private
def set_message_franking!
@message_franking = message_franking.to_token
end
def deliver_to_local!
target_device = @target_account.devices.find_by!(device_id: @target_device_id)
target_device.encrypted_messages.create!(
from_account: @source_account,
from_device_id: @source_device.device_id,
type: @type,
body: @body,
digest: @hmac,
message_franking: @message_franking
)
end
def deliver_to_remote!
ActivityPub::DeliveryWorker.perform_async(
Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_encrypted_message(encrypted_message), ActivityPub::ActivitySerializer)),
@source_account.id,
@target_account.inbox_url
)
end
def message_franking
MessageFranking.new(
source_account_id: @source_account.id,
target_account_id: @target_account.id,
hmac: @hmac,
timestamp: Time.now.utc
)
end
def encrypted_message
EncryptedMessage.new(
source_account: @source_account,
target_account: @target_account,
source_device: @source_device,
target_device_id: @target_device_id,
type: @type,
body: @body,
digest: @hmac,
message_franking: @message_franking
)
end
end

View file

@ -1,79 +0,0 @@
# frozen_string_literal: true
class Keys::ClaimService < BaseService
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
class Result < ActiveModelSerializers::Model
attributes :account, :device_id, :key_id,
:key, :signature
def initialize(account, device_id, key_attributes = {})
super(
account: account,
device_id: device_id,
key_id: key_attributes[:key_id],
key: key_attributes[:key],
signature: key_attributes[:signature],
)
end
end
def call(source_account, target_account_id, device_id)
@source_account = source_account
@target_account = Account.find(target_account_id)
@device_id = device_id
if @target_account.local?
claim_local_key!
else
claim_remote_key!
end
rescue ActiveRecord::RecordNotFound
nil
end
private
def claim_local_key!
device = @target_account.devices.find_by(device_id: @device_id)
key = nil
ApplicationRecord.transaction do
key = device.one_time_keys.order(Arel.sql('random()')).first!
key.destroy!
end
@result = Result.new(@target_account, @device_id, key)
end
def claim_remote_key!
query_result = QueryService.new.call(@target_account)
device = query_result.find(@device_id)
return unless device.present? && device.valid_claim_url?
json = fetch_resource_with_post(device.claim_url)
return unless json.present? && json['publicKeyBase64'].present?
@result = Result.new(@target_account, @device_id, key_id: json['id'], key: json['publicKeyBase64'], signature: json.dig('signature', 'signatureValue'))
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e
Rails.logger.debug { "Claiming one-time key for #{@target_account.acct}:#{@device_id} failed: #{e}" }
nil
end
def fetch_resource_with_post(uri)
build_post_request(uri).perform do |response|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response)
body_to_json(response.body_with_limit) if response.code == 200
end
end
def build_post_request(uri)
Request.new(:post, uri).tap do |request|
request.on_behalf_of(@source_account)
request.add_headers(HEADERS)
end
end
end

View file

@ -1,79 +0,0 @@
# frozen_string_literal: true
class Keys::QueryService < BaseService
include JsonLdHelper
class Result < ActiveModelSerializers::Model
attributes :account, :devices
def initialize(account, devices)
super(
account: account,
devices: devices || [],
)
end
def find(device_id)
@devices.find { |device| device.device_id == device_id }
end
end
class Device < ActiveModelSerializers::Model
attributes :device_id, :name, :identity_key, :fingerprint_key
def initialize(attributes = {})
super(
device_id: attributes[:device_id],
name: attributes[:name],
identity_key: attributes[:identity_key],
fingerprint_key: attributes[:fingerprint_key],
)
@claim_url = attributes[:claim_url]
end
def valid_claim_url?
return false if @claim_url.blank?
begin
parsed_url = Addressable::URI.parse(@claim_url).normalize
rescue Addressable::URI::InvalidURIError
return false
end
%w(http https).include?(parsed_url.scheme) && parsed_url.host.present?
end
end
def call(account)
@account = account
if @account.local?
query_local_devices!
else
query_remote_devices!
end
Result.new(@account, @devices)
end
private
def query_local_devices!
@devices = @account.devices.map { |device| Device.new(device) }
end
def query_remote_devices!
return if @account.devices_url.blank?
json = fetch_resource(@account.devices_url)
return if json['items'].blank?
@devices = as_array(json['items']).map do |device|
Device.new(device_id: device['id'], name: device['name'], identity_key: device.dig('identityKey', 'publicKeyBase64'), fingerprint_key: device.dig('fingerprintKey', 'publicKeyBase64'), claim_url: device['claim'])
end
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e
Rails.logger.debug { "Querying devices for #{@account.acct} failed: #{e}" }
nil
end
end

View file

@ -1,19 +0,0 @@
# frozen_string_literal: true
class Ed25519KeyValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
key = Base64.decode64(value)
record.errors.add(attribute, I18n.t('crypto.errors.invalid_key')) unless verified?(key)
end
private
def verified?(key)
Ed25519.validate_key_bytes(key)
rescue ArgumentError
false
end
end

View file

@ -1,29 +0,0 @@
# frozen_string_literal: true
class Ed25519SignatureValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
verify_key = Ed25519::VerifyKey.new(Base64.decode64(option_to_value(record, :verify_key)))
signature = Base64.decode64(value)
message = option_to_value(record, :message)
record.errors.add(attribute, I18n.t('crypto.errors.invalid_signature')) unless verified?(verify_key, signature, message)
end
private
def verified?(verify_key, signature, message)
verify_key.verify(signature, message)
rescue Ed25519::VerifyError, ArgumentError
false
end
def option_to_value(record, key)
if options[key].is_a?(Proc)
options[key].call(record)
else
record.public_send(options[key])
end
end
end

View file

@ -18,7 +18,7 @@
- if status.application - if status.application
= status.application.name = status.application.name
· ·
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do = link_to ActivityPub::TagManager.instance.url_for(status.proper), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
- if status.edited? - if status.edited?
· ·

View file

@ -12,7 +12,7 @@
%li= filter_link_to t('generic.all'), status: nil %li= filter_link_to t('generic.all'), status: nil
%li= filter_link_to t('admin.trends.approved'), status: 'approved' %li= filter_link_to t('admin.trends.approved'), status: 'approved'
%li= filter_link_to t('admin.trends.rejected'), status: 'rejected' %li= filter_link_to t('admin.trends.rejected'), status: 'rejected'
%li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{PreviewCardProvider.pending_review.count})"], ' '), status: 'pending_review' %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{PreviewCardProvider.unreviewed.count})"], ' '), status: 'pending_review'
.back-link .back-link
= link_to admin_trends_links_path do = link_to admin_trends_links_path do
= material_symbol 'chevron_left' = material_symbol 'chevron_left'

View file

@ -1,16 +0,0 @@
# frozen_string_literal: true
class PushEncryptedMessageWorker
include Sidekiq::Worker
include Redisable
def perform(encrypted_message_id)
encrypted_message = EncryptedMessage.find(encrypted_message_id)
message = InlineRenderer.render(encrypted_message, nil, :encrypted_message)
timeline_id = "timeline:#{encrypted_message.device.account_id}:#{encrypted_message.device.device_id}"
redis.publish(timeline_id, Oj.dump(event: :encrypted_message, payload: message))
rescue ActiveRecord::RecordNotFound
true
end
end

View file

@ -118,8 +118,7 @@ Doorkeeper.configure do
:'admin:write:domain_blocks', :'admin:write:domain_blocks',
:'admin:write:ip_blocks', :'admin:write:ip_blocks',
:'admin:write:email_domain_blocks', :'admin:write:email_domain_blocks',
:'admin:write:canonical_email_blocks', :'admin:write:canonical_email_blocks'
:crypto
# Change the way client credentials are retrieved from the request object. # Change the way client credentials are retrieved from the request object.
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then

View file

@ -19,7 +19,6 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'CLI' inflect.acronym 'CLI'
inflect.acronym 'DeepL' inflect.acronym 'DeepL'
inflect.acronym 'DSL' inflect.acronym 'DSL'
inflect.acronym 'Ed25519'
inflect.acronym 'JsonLd' inflect.acronym 'JsonLd'
inflect.acronym 'OEmbed' inflect.acronym 'OEmbed'
inflect.acronym 'OStatus' inflect.acronym 'OStatus'

View file

@ -15,6 +15,12 @@ ko:
user/invite_request: user/invite_request:
text: 이유 text: 이유
errors: errors:
attributes:
domain:
invalid: 올바른 도메인 네임이 아닙니다
messages:
invalid_domain_on_line: "%{value}는 올바른 도메인 네임이 아닙니다"
too_many_lines: "%{limit}줄 제한을 초과합니다"
models: models:
account: account:
attributes: attributes:

View file

@ -15,6 +15,12 @@ ru:
user/invite_request: user/invite_request:
text: Причина text: Причина
errors: errors:
attributes:
domain:
invalid: не является действующим доменным именем
messages:
invalid_domain_on_line: "%{value} Не является действительным доменным именем"
too_many_lines: Превышает предел %{limit} строк
models: models:
account: account:
attributes: attributes:

View file

@ -953,7 +953,6 @@ an:
crypto: crypto:
errors: errors:
invalid_key: no ye una clau Ed25519 u Curve25519 valida invalid_key: no ye una clau Ed25519 u Curve25519 valida
invalid_signature: no ye una sinyatura Ed25519 valida
date: date:
formats: formats:
default: "%d %b %Y" default: "%d %b %Y"

View file

@ -1178,7 +1178,6 @@ ar:
crypto: crypto:
errors: errors:
invalid_key: ليس بمفتاح Ed25519 أو Curve25519 صالح invalid_key: ليس بمفتاح Ed25519 أو Curve25519 صالح
invalid_signature: ليس بتوقيع Ed25519 صالح
date: date:
formats: formats:
default: "%d %b %Y" default: "%d %b %Y"

View file

@ -486,7 +486,6 @@ ast:
crypto: crypto:
errors: errors:
invalid_key: nun ye una clave ed25519 o curve25519 válida invalid_key: nun ye una clave ed25519 o curve25519 válida
invalid_signature: nun ye una clave ed25519 válida
datetime: datetime:
distance_in_words: distance_in_words:
about_x_hours: "%{count} h" about_x_hours: "%{count} h"

View file

@ -1189,7 +1189,6 @@ be:
crypto: crypto:
errors: errors:
invalid_key: гэта не сапраўдны Ed25519 або Curve25519 ключ invalid_key: гэта не сапраўдны Ed25519 або Curve25519 ключ
invalid_signature: гэта не сапраўдная Ed25519 сігнатура
date: date:
formats: formats:
default: "%d.%m.%Y" default: "%d.%m.%Y"

View file

@ -1115,7 +1115,6 @@ bg:
crypto: crypto:
errors: errors:
invalid_key: не е валиден ключ Ed25519 или Curve25519 invalid_key: не е валиден ключ Ed25519 или Curve25519
invalid_signature: не е валиден подпис Ed25519
date: date:
formats: formats:
default: "%b %d, %Y" default: "%b %d, %Y"

View file

@ -1171,7 +1171,6 @@ ca:
crypto: crypto:
errors: errors:
invalid_key: no és una clau Ed25519 o Curve25519 vàlida invalid_key: no és una clau Ed25519 o Curve25519 vàlida
invalid_signature: no és una signatura Ed25519 vàlida
date: date:
formats: formats:
default: "%b %d, %Y" default: "%b %d, %Y"

View file

@ -608,7 +608,6 @@ ckb:
crypto: crypto:
errors: errors:
invalid_key: کلیلی باوڕپێکراو Ed25519 یان Curve25519 دروست نییە invalid_key: کلیلی باوڕپێکراو Ed25519 یان Curve25519 دروست نییە
invalid_signature: واژووی Ed25519 بڕوادار نییە
date: date:
formats: formats:
default: "%b %d, %Y" default: "%b %d, %Y"

View file

@ -569,7 +569,6 @@ co:
crypto: crypto:
errors: errors:
invalid_key: ùn hè micca una chjave Ed25519 o Curve25519 valida invalid_key: ùn hè micca una chjave Ed25519 o Curve25519 valida
invalid_signature: ùn hè micca una firma Ed25519 valida
date: date:
formats: formats:
default: "%d %b %Y" default: "%d %b %Y"

View file

@ -1149,7 +1149,6 @@ cs:
crypto: crypto:
errors: errors:
invalid_key: není platný klíč Ed25519 nebo Curve25519 invalid_key: není platný klíč Ed25519 nebo Curve25519
invalid_signature: není platný podpis typu Ed25519
date: date:
formats: formats:
default: "%d. %b %Y" default: "%d. %b %Y"

Some files were not shown because too many files have changed in this diff Show more