mirror of
https://git.kescher.at/CatCatNya/catstodon.git
synced 2024-11-25 03:58:07 +01:00
Merge commit '7ed9c590b98610f8d68deab9ef8df260eec6d8f0' into glitch-soc/merge-upstream
This commit is contained in:
commit
221bba1897
310 changed files with 2257 additions and 2167 deletions
1
Gemfile
1
Gemfile
|
@ -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'
|
||||||
|
|
18
Gemfile.lock
18
Gemfile.lock
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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 = []
|
||||||
|
|
|
@ -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?
|
||||||
|
|
||||||
|
|
|
@ -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']
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
31
app/javascript/hooks/useSearchParam.ts
Normal file
31
app/javascript/hooks/useSearchParam.ts
Normal 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;
|
||||||
|
}
|
|
@ -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({
|
||||||
|
|
|
@ -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() : {}),
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,9 +250,12 @@ 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}
|
||||||
containerStyle={containerStyle}
|
containerStyle={containerStyle}
|
||||||
|
@ -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}
|
||||||
|
|
|
@ -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,52 +366,37 @@ 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 (
|
||||||
<>
|
<div
|
||||||
<IconButton
|
className='zoomable-image'
|
||||||
className={`media-modal__zoom-button ${zoomButtonShouldHide}`}
|
ref={this.setContainerRef}
|
||||||
title={zoomButtonTitle}
|
style={{ overflow, cursor, userSelect: 'none' }}
|
||||||
icon={this.state.zoomState}
|
>
|
||||||
iconComponent={this.state.zoomState === 'compress' ? FullscreenExitIcon : RectangleIcon}
|
<img
|
||||||
onClick={this.handleZoomClick}
|
role='presentation'
|
||||||
size={40}
|
ref={this.setImageRef}
|
||||||
|
alt={alt}
|
||||||
|
title={alt}
|
||||||
|
lang={lang}
|
||||||
|
src={src}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
style={{
|
style={{
|
||||||
fontSize: '30px', /* Fontawesome's fa-compress fa-expand is larger than fa-close */
|
transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`,
|
||||||
|
transformOrigin: '0 0',
|
||||||
}}
|
}}
|
||||||
|
draggable={false}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
onMouseDown={this.handleMouseDown}
|
||||||
/>
|
/>
|
||||||
<div
|
</div>
|
||||||
className='zoomable-image'
|
|
||||||
ref={this.setContainerRef}
|
|
||||||
style={{ overflow }}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
role='presentation'
|
|
||||||
ref={this.setImageRef}
|
|
||||||
alt={alt}
|
|
||||||
title={alt}
|
|
||||||
lang={lang}
|
|
||||||
src={src}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
style={{
|
|
||||||
transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`,
|
|
||||||
transformOrigin: '0 0',
|
|
||||||
}}
|
|
||||||
draggable={false}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
onMouseDown={this.handleMouseDown}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(ZoomableImage);
|
export default ZoomableImage;
|
||||||
|
|
|
@ -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": "احذفه مِن الفواصل المرجعية",
|
||||||
|
|
|
@ -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}",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 n’est 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 n’est 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",
|
||||||
|
|
|
@ -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 n’est 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 n’est 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",
|
||||||
|
|
|
@ -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}",
|
||||||
|
|
|
@ -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": "סנן הודעה זו",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": "글타래에 답장",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": "Ответить всем",
|
||||||
|
|
|
@ -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(() => {
|
||||||
node.scrollTo({ left: position, behavior: 'smooth' });
|
if (isScrollBehaviorSupported) {
|
||||||
else scroll(node, 'scrollLeft', position);
|
node.scrollTo({ left: position, behavior: 'smooth' });
|
||||||
};
|
} 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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,
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 |
1
app/javascript/material-icons/400-24px/fit_screen.svg
Normal file
1
app/javascript/material-icons/400-24px/fit_screen.svg
Normal 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 |
|
@ -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';
|
||||||
|
|
|
@ -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,19 +5764,34 @@ a.status-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&__close,
|
&__buttons {
|
||||||
&__zoom-button {
|
position: absolute;
|
||||||
color: rgba($white, 0.7);
|
inset-inline-end: 8px;
|
||||||
|
top: 8px;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
&:hover,
|
.icon-button {
|
||||||
&:focus,
|
color: rgba($white, 0.7);
|
||||||
&:active {
|
padding: 8px;
|
||||||
color: $white;
|
|
||||||
background-color: rgba($white, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
.icon {
|
||||||
background-color: rgba($white, 0.3);
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
color: $white;
|
||||||
|
background-color: rgba($white, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: rgba($white, 0.3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
|
4
app/javascript/svg-icons/actual_size.svg
Normal file
4
app/javascript/svg-icons/actual_size.svg
Normal 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 |
|
@ -8,44 +8,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
|
|
||||||
dereference_object!
|
dereference_object!
|
||||||
|
|
||||||
case @object['type']
|
create_status
|
||||||
when 'EncryptedMessage'
|
|
||||||
create_encrypted_message
|
|
||||||
else
|
|
||||||
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?
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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('.')
|
||||||
|
|
|
@ -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
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -1,6 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class REST::Keys::DeviceSerializer < ActiveModel::Serializer
|
|
||||||
attributes :device_id, :name, :identity_key,
|
|
||||||
:fingerprint_key
|
|
||||||
end
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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?
|
||||||
·
|
·
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue