Merge pull request #1158 from ThibG/glitch-soc/merge-upstream

Merge upstream changes
main
ThibG 5 years ago committed by GitHub
commit f1597e1ab9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -112,6 +112,7 @@ ENV NODE_ENV="production"
# Tell rails to serve static files
ENV RAILS_SERVE_STATIC_FILES="true"
ENV BIND="0.0.0.0"
# Set the run user
USER mastodon

@ -5,7 +5,7 @@ ruby '>= 2.4.0', '< 2.7.0'
gem 'pkg-config', '~> 1.3'
gem 'puma', '~> 3.12'
gem 'puma', '~> 4.0'
gem 'rails', '~> 5.2.3'
gem 'thor', '~> 0.20'
@ -59,6 +59,7 @@ gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.2', require: 'mime/types/columnar'
gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
gem 'nokogiri', '~> 1.10'
gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.7'
@ -67,7 +68,7 @@ gem 'ox', '~> 2.11'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 2.0'
gem 'premailer-rails'
gem 'rack-attack', '~> 6.0'
gem 'rack-attack', '~> 6.1'
gem 'rack-cors', '~> 1.0', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6'

@ -12,6 +12,13 @@ GIT
specs:
http_parser.rb (0.6.1)
GIT
remote: https://github.com/witgo/nilsimsa
revision: fd184883048b922b176939f851338d0a4971a532
ref: fd184883048b922b176939f851338d0a4971a532
specs:
nilsimsa (1.1.2)
GEM
remote: https://rubygems.org/
specs:
@ -423,12 +430,13 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (3.1.1)
puma (3.12.1)
puma (4.0.1)
nio4r (~> 2.0)
pundit (2.0.1)
activesupport (>= 3.0.0)
raabro (1.1.6)
rack (2.0.7)
rack-attack (6.0.0)
rack-attack (6.1.0)
rack (>= 1.0, < 3)
rack-cors (1.0.3)
rack-protection (2.0.5)
@ -534,7 +542,7 @@ GEM
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
rubocop-rails (2.2.0)
rubocop-rails (2.2.1)
rack (>= 1.1)
rubocop (>= 0.72.0)
ruby-progressbar (1.10.1)
@ -588,7 +596,7 @@ GEM
stoplight (2.1.3)
streamio-ffmpeg (3.0.2)
multi_json (~> 1.8)
strong_migrations (0.4.0)
strong_migrations (0.4.1)
activerecord (>= 5)
temple (0.8.1)
terminal-table (1.8.0)
@ -708,6 +716,7 @@ DEPENDENCIES
microformats (~> 4.1)
mime-types (~> 3.2)
net-ldap (~> 0.10)
nilsimsa!
nokogiri (~> 1.10)
nsa (~> 0.2)
oj (~> 3.7)
@ -727,9 +736,9 @@ DEPENDENCIES
private_address_check (~> 0.5)
pry-byebug (~> 3.7)
pry-rails (~> 0.3)
puma (~> 3.12)
puma (~> 4.0)
pundit (~> 2.0)
rack-attack (~> 6.0)
rack-attack (~> 6.1)
rack-cors (~> 1.0)
rails (~> 5.2.3)
rails-controller-testing (~> 1.0)

@ -4,13 +4,17 @@ class AboutController < ApplicationController
before_action :set_pack
layout 'public'
before_action :set_instance_presenter, only: [:show, :more, :terms]
before_action :set_body_classes, only: :show
before_action :set_instance_presenter
before_action :set_expires_in
def show
@hide_navbar = true
end
skip_before_action :check_user_permissions, only: [:more, :terms]
def more; end
def show; end
def more
flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
end
def terms; end
@ -32,4 +36,12 @@ class AboutController < ApplicationController
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def set_body_classes
@hide_navbar = true
end
def set_expires_in
expires_in 0, public: true
end
end

@ -4,16 +4,17 @@ class AccountsController < ApplicationController
PAGE_SIZE = 20
include AccountControllerConcern
include SignatureAuthentication
before_action :set_cache_headers
before_action :set_body_classes
def show
respond_to do |format|
format.html do
use_pack 'public'
mark_cacheable! unless user_signed_in?
expires_in 0, public: true unless user_signed_in?
@body_classes = 'with-modals'
@pinned_statuses = []
@endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
@ -32,30 +33,26 @@ class AccountsController < ApplicationController
end
end
format.atom do
mark_cacheable!
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id])
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? || entry.status.local_only? }))
end
format.rss do
mark_cacheable!
expires_in 0, public: true
@statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status)
render xml: RSS::AccountSerializer.render(@account, @statuses)
end
format.json do
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
end
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
end
end
end
private
def set_body_classes
@body_classes = 'with-modals'
end
def show_pinned_statuses?
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
end
@ -137,4 +134,12 @@ class AccountsController < ApplicationController
filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a
end
end
def restrict_fields_to
if signed_request_account.present? || public_fetch_mode?
# Return all fields
else
%i(id type preferred_username inbox public_key endpoints)
end
end
end

@ -0,0 +1,9 @@
# frozen_string_literal: true
class ActivityPub::BaseController < Api::BaseController
private
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode?
end
end

@ -1,30 +1,21 @@
# frozen_string_literal: true
class ActivityPub::CollectionsController < Api::BaseController
class ActivityPub::CollectionsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
before_action :set_account
before_action :require_signature!, if: :authorized_fetch_mode?
before_action :set_size
before_action :set_statuses
before_action :set_cache_headers
def show
render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(
collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
skip_activities: true
)
end
expires_in 3.minutes, public: public_fetch_mode?
render json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true
end
private
def set_account
@account = Account.find_local!(params[:account_username])
end
def set_statuses
@statuses = scope_for_collection
@statuses = cache_collection(@statuses, Status)

@ -3,38 +3,42 @@
class ActivityPub::InboxesController < Api::BaseController
include SignatureVerification
include JsonLdHelper
include AccountOwnedConcern
before_action :set_account
before_action :skip_unknown_actor_delete
before_action :require_signature!
def create
if unknown_deleted_account?
head 202
elsif signed_request_account
upgrade_account
process_payload
head 202
else
render plain: signature_verification_failure_reason, status: 401
end
upgrade_account
process_payload
head 202
end
private
def skip_unknown_actor_delete
head 202 if unknown_deleted_account?
end
def unknown_deleted_account?
json = Oj.load(body, mode: :strict)
json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
json.is_a?(Hash) && json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
rescue Oj::ParseError
false
end
def set_account
@account = Account.find_local!(params[:account_username]) if params[:account_username]
def account_required?
params[:account_username].present?
end
def body
return @body if defined?(@body)
@body = request.body.read.force_encoding('UTF-8')
@body = request.body.read
@body.force_encoding('UTF-8') if @body.present?
request.body.rewind if request.body.respond_to?(:rewind)
@body
end
@ -44,7 +48,6 @@ class ActivityPub::InboxesController < Api::BaseController
ResolveAccountWorker.perform_async(signed_request_account.acct)
end
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
DeliveryFailureTracker.track_inverse_success!(signed_request_account)
end

@ -1,26 +1,22 @@
# frozen_string_literal: true
class ActivityPub::OutboxesController < Api::BaseController
class ActivityPub::OutboxesController < ActivityPub::BaseController
LIMIT = 20
include SignatureVerification
include AccountOwnedConcern
before_action :set_account
before_action :require_signature!, if: :authorized_fetch_mode?
before_action :set_statuses
before_action :set_cache_headers
def show
expires_in 1.minute, public: true unless page_requested?
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private
def set_account
@account = Account.find_local!(params[:account_username])
end
def outbox_presenter
if page_requested?
ActivityPub::CollectionPresenter.new(

@ -0,0 +1,70 @@
# frozen_string_literal: true
class ActivityPub::RepliesController < ActivityPub::BaseController
include SignatureAuthentication
include Authorization
include AccountOwnedConcern
DESCENDANTS_LIMIT = 60
before_action :require_signature!, if: :authorized_fetch_mode?
before_action :set_status
before_action :set_cache_headers
before_action :set_replies
def index
expires_in 0, public: public_fetch_mode?
render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true
end
private
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
end
def set_replies
@replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end
def replies_collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: account_status_replies_url(@account, @status, page_params),
type: :unordered,
part_of: account_status_replies_url(@account, @status),
next: next_page,
items: @replies.map { |status| status.local ? status : status.id }
)
return page if page_requested?
ActivityPub::CollectionPresenter.new(
id: account_status_replies_url(@account, @status),
type: :unordered,
first: page
)
end
def page_requested?
params[:page] == 'true'
end
def next_page
account_status_replies_url(
@account,
@status,
page: true,
min_id: @replies&.last&.id,
other_accounts: !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
)
end
def page_params
params_slice(:other_accounts, :min_id).merge(page: true)
end
end

@ -2,8 +2,8 @@
module Admin
class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :require_remote_account!, only: [:redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
def index
@ -19,18 +19,6 @@ module Admin
@warnings = @account.targeted_account_warnings.latest.custom
end
def subscribe
authorize @account, :subscribe?
Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end
def unsubscribe
authorize @account, :unsubscribe?
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end
def memorialize
authorize @account, :memorialize?
@account.memorialize!

@ -31,6 +31,7 @@ module Admin
@profile_directory = Setting.profile_directory
@timeline_preview = Setting.timeline_preview
@keybase_integration = Setting.enable_keybase
@spam_check_enabled = Setting.spam_check_enabled
end
private

@ -17,7 +17,7 @@ module Admin
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
@domain_block.save
flash[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety
flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety
@domain_block.errors[:domain].clear
render :new
else

@ -1,10 +1,9 @@
# frozen_string_literal: true
class Api::ProofsController < Api::BaseController
before_action :set_account
include AccountOwnedConcern
before_action :set_provider
before_action :check_account_approval
before_action :check_account_suspension
def index
render json: @account, serializer: @provider.serializer_class
@ -16,15 +15,7 @@ class Api::ProofsController < Api::BaseController
@provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound)
end
def set_account
@account = Account.find_local!(params[:username])
end
def check_account_approval
not_found if @account.user_pending?
end
def check_account_suspension
gone if @account.suspended?
def username_param
params[:username]
end
end

@ -1,73 +0,0 @@
# frozen_string_literal: true
class Api::PushController < Api::BaseController
include SignatureVerification
def update
response, status = process_push_request
render plain: response, status: status
end
private
def process_push_request
case hub_mode
when 'subscribe'
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain)
when 'unsubscribe'
Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback)
else
["Unknown mode: #{hub_mode}", 422]
end
end
def hub_mode
params['hub.mode']
end
def hub_topic
params['hub.topic']
end
def hub_callback
params['hub.callback']
end
def hub_lease_seconds
params['hub.lease_seconds']
end
def hub_secret
params['hub.secret']
end
def account_from_topic
if hub_topic.present? && local_domain? && account_feed_path?
Account.find_local(hub_topic_params[:username])
end
end
def hub_topic_params
@_hub_topic_params ||= Rails.application.routes.recognize_path(hub_topic_uri.path)
end
def hub_topic_uri
@_hub_topic_uri ||= Addressable::URI.parse(hub_topic).normalize
end
def local_domain?
TagManager.instance.web_domain?(hub_topic_domain)
end
def verified_domain
return signed_request_account.domain if signed_request_account
end
def hub_topic_domain
hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '')
end
def account_feed_path?
hub_topic_params[:controller] == 'accounts' && hub_topic_params[:action] == 'show' && hub_topic_params[:format] == 'atom'
end
end

@ -1,37 +0,0 @@
# frozen_string_literal: true
class Api::SalmonController < Api::BaseController
include SignatureVerification
before_action :set_account
respond_to :txt
def update
if verify_payload?
process_salmon
head 202
elsif payload.present?
render plain: signature_verification_failure_reason, status: 401
else
head 400
end
end
private
def set_account
@account = Account.find(params[:id])
end
def payload
@_payload ||= request.body.read
end
def verify_payload?
payload.present? && VerifySalmonService.new.call(payload)
end
def process_salmon
SalmonWorker.perform_async(@account.id, payload.force_encoding('UTF-8'))
end
end

@ -1,51 +0,0 @@
# frozen_string_literal: true
class Api::SubscriptionsController < Api::BaseController
before_action :set_account
respond_to :txt
def show
if subscription.valid?(params['hub.topic'])
@account.update(subscription_expires_at: future_expires)
render plain: encoded_challenge, status: 200
else
head 404
end
end
def update
if subscription.verify(body, request.headers['HTTP_X_HUB_SIGNATURE'])
ProcessingWorker.perform_async(@account.id, body.force_encoding('UTF-8'))
end
head 200
end
private
def subscription
@_subscription ||= @account.subscription(
api_subscription_url(@account.id)
)
end
def body
@_body ||= request.body.read
end
def encoded_challenge
HTMLEntities.new.encode(params['hub.challenge'])
end
def future_expires
Time.now.utc + lease_seconds_or_default
end
def lease_seconds_or_default
(params['hub.lease_seconds'] || 1.day).to_i.seconds
end
def set_account
@account = Account.find(params[:id])
end
end

@ -1,31 +0,0 @@
# frozen_string_literal: true
class Api::V1::FollowsController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }
before_action :require_user!
respond_to :json
def create
raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
@account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
if @account.nil?
username, domain = target_uri.split('@')
@account = Account.find_remote!(username, domain)
end
render json: @account, serializer: REST::AccountSerializer
end
private
def target_uri
follow_params[:uri].strip.gsub(/\A@/, '')
end
def follow_params
params.permit(:uri)
end
end

@ -37,6 +37,14 @@ class ApplicationController < ActionController::Base
Rails.env.production?
end
def authorized_fetch_mode?
ENV['AUTHORIZED_FETCH'] == 'true'
end
def public_fetch_mode?
!authorized_fetch_mode?
end
def store_current_location
store_location_for(:user, request.url) unless request.format == :json
end
@ -153,7 +161,7 @@ class ApplicationController < ActionController::Base
end
def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
end
def use_seamless_external_login?
@ -228,10 +236,6 @@ class ApplicationController < ActionController::Base
end
def set_cache_headers
response.headers['Vary'] = 'Accept'
end
def mark_cacheable!
expires_in 0, public: true
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
end
end

@ -3,24 +3,19 @@
module AccountControllerConcern
extend ActiveSupport::Concern
include AccountOwnedConcern
FOLLOW_PER_PAGE = 12
included do
layout 'public'
before_action :set_account
before_action :check_account_approval
before_action :check_account_suspension
before_action :set_instance_presenter
before_action :set_link_headers
before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
end
private
def set_account
@account = Account.find_local!(username_param)
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
@ -29,27 +24,15 @@ module AccountControllerConcern
response.headers['Link'] = LinkHeader.new(
[
webfinger_account_link,
atom_account_url_link,
actor_url_link,
]
)
end
def username_param
params[:account_username]
end
def webfinger_account_link
[
webfinger_account_url,
[%w(rel lrdd), %w(type application/xrd+xml)],
]
end
def atom_account_url_link
[
account_url(@account, format: 'atom'),
[%w(rel alternate), %w(type application/atom+xml)],
[%w(rel lrdd), %w(type application/jrd+json)],
]
end
@ -63,15 +46,4 @@ module AccountControllerConcern
def webfinger_account_url
webfinger_url(resource: @account.to_webfinger_s)
end
def check_account_approval
not_found if @account.user_pending?
end
def check_account_suspension
if @account.suspended?
expires_in(3.minutes, public: true)
gone
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module AccountOwnedConcern
extend ActiveSupport::Concern
included do
before_action :set_account, if: :account_required?
before_action :check_account_approval, if: :account_required?
before_action :check_account_suspension, if: :account_required?
end
private
def account_required?
true
end
def set_account
@account = Account.find_local!(username_param)
end
def username_param
params[:account_username]
end
def check_account_approval
not_found if @account.local? && @account.user_pending?
end
def check_account_suspension
expires_in(3.minutes, public: true) && gone if @account.suspended?
end
end

@ -5,12 +5,22 @@
module SignatureVerification
extend ActiveSupport::Concern
include DomainControlHelper
def require_signature!
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
end
def signed_request?
request.headers['Signature'].present?
end
def signature_verification_failure_reason
return @signature_verification_failure_reason if defined?(@signature_verification_failure_reason)
@signature_verification_failure_reason
end
def signature_verification_failure_code
@signature_verification_failure_code || 401
end
def signed_request_account
@ -123,6 +133,13 @@ module SignatureVerification
end
def account_from_key_id(key_id)
domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
if domain_not_allowed?(domain)
@signature_verification_failure_code = 403
return
end
if key_id.start_with?('acct:')
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)

@ -0,0 +1,87 @@
# frozen_string_literal: true
module StatusControllerConcern
extend ActiveSupport::Concern
ANCESTORS_LIMIT = 40
DESCENDANTS_LIMIT = 60
DESCENDANTS_DEPTH_LIMIT = 20
def create_descendant_thread(starting_depth, statuses)
depth = starting_depth + statuses.size
if depth < DESCENDANTS_DEPTH_LIMIT
{
statuses: statuses,
starting_depth: starting_depth,
}
else
next_status = statuses.pop
{
statuses: statuses,
starting_depth: starting_depth,
next_status: next_status,
}
end
end
def set_ancestors
@ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
@next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
end
def set_descendants
@max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i
@since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i
descendants = cache_collection(
@status.descendants(
DESCENDANTS_LIMIT,
current_account,
@max_descendant_thread_id,
@since_descendant_thread_id,
DESCENDANTS_DEPTH_LIMIT
),
Status
)
@descendant_threads = []
if descendants.present?
statuses = [descendants.first]
starting_depth = 0
descendants.drop(1).each_with_index do |descendant, index|
if descendants[index].id == descendant.in_reply_to_id
statuses << descendant
else
@descendant_threads << create_descendant_thread(starting_depth, statuses)
# The thread is broken, assume it's a reply to the root status
starting_depth = 0
# ... unless we can find its ancestor in one of the already-processed threads
@descendant_threads.reverse_each do |descendant_thread|
statuses = descendant_thread[:statuses]
index = statuses.find_index do |thread_status|
thread_status.id == descendant.in_reply_to_id
end
if index.present?
starting_depth = descendant_thread[:starting_depth] + index + 1
break
end
end
statuses = [descendant]
end
end
@descendant_threads << create_descendant_thread(starting_depth, statuses)
end
@max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT
end
end

@ -6,6 +6,7 @@ class CustomCssController < ApplicationController
before_action :set_cache_headers
def show
expires_in 3.minutes, public: true
render plain: Setting.custom_css || '', content_type: 'text/css'
end
end

@ -7,9 +7,8 @@ class EmojisController < ApplicationController
def show
respond_to do |format|
format.json do
render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
end
expires_in 3.minutes, public: true
render json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter
end
end
end

@ -2,14 +2,16 @@
class FollowerAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
def index
respond_to do |format|
format.html do
use_pack 'public'
mark_cacheable! unless user_signed_in?
expires_in 0, public: true unless user_signed_in?
next if @account.user_hides_network?
@ -18,9 +20,9 @@ class FollowerAccountsController < ApplicationController
end
format.json do
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network?
expires_in 3.minutes, public: true if params[:page].blank?
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
@ -36,6 +38,10 @@ class FollowerAccountsController < ApplicationController
@follows ||= Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
end
def page_requested?
params[:page].present?
end
def page_url(page)
account_followers_url(@account, page: page) unless page.nil?
end
@ -43,7 +49,7 @@ class FollowerAccountsController < ApplicationController
def collection_presenter
options = { type: :ordered }
options[:size] = @account.followers_count unless Setting.hide_followers_count || @account.user&.setting_hide_followers_count
if params[:page].present?
if page_requested?
ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account, page: params.fetch(:page, 1)),
items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) },

@ -2,14 +2,16 @@
class FollowingAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
def index
respond_to do |format|
format.html do
use_pack 'public'
mark_cacheable! unless user_signed_in?
expires_in 0, public: true unless user_signed_in?
next if @account.user_hides_network?
@ -18,9 +20,9 @@ class FollowingAccountsController < ApplicationController
end
format.json do
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network?
expires_in 3.minutes, public: true if params[:page].blank?
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
@ -36,12 +38,16 @@ class FollowingAccountsController < ApplicationController
@follows ||= Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
end
def page_requested?
params[:page].present?
end
def page_url(page)
account_following_index_url(@account, page: page) unless page.nil?
end
def collection_presenter
if params[:page].present?
if page_requested?
ActivityPub::CollectionPresenter.new(
id: account_following_index_url(@account, page: params.fetch(:page, 1)),
type: :ordered,

@ -23,7 +23,7 @@ class HomeController < ApplicationController
when 'statuses'
status = Status.find_by(id: matches[2])
if status && (status.public_visibility? || status.unlisted_visibility?)
if status&.distributable?
redirect_to(ActivityPub::TagManager.instance.url_for(status))
return
end
@ -64,7 +64,7 @@ class HomeController < ApplicationController
if request.path.start_with?('/web')
new_user_session_path
elsif single_user_mode?
short_account_path(Account.local.without_suspended.first)
short_account_path(Account.local.without_suspended.where('id > 0').first)
else
about_path
end

@ -0,0 +1,20 @@
# frozen_string_literal: true
class InstanceActorsController < ApplicationController
include AccountControllerConcern
def show
expires_in 10.minutes, public: true
render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
end
private
def set_account
@account = Account.find(-99)
end
def restrict_fields_to
%i(id type preferred_username inbox public_key endpoints url manually_approves_followers)
end
end

@ -2,6 +2,7 @@
class IntentsController < ApplicationController
before_action :check_uri
rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri
def show

@ -4,6 +4,7 @@ class ManifestsController < ApplicationController
skip_before_action :store_current_location
def show
expires_in 3.minutes, public: true
render json: InstancePresenter.new, serializer: ManifestSerializer
end
end

@ -31,7 +31,6 @@ class MediaController < ApplicationController
def verify_permitted_status!
authorize @media_attachment.status, :show?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404 instead of a 403 error code
raise ActiveRecord::RecordNotFound
end

@ -9,20 +9,16 @@ class PublicTimelinesController < ApplicationController
before_action :set_instance_presenter
def show
respond_to do |format|
format.html do
@initial_state_json = ActiveModelSerializers::SerializableResource.new(
InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token),
serializer: InitialStateSerializer
).to_json
end
end
@initial_state_json = ActiveModelSerializers::SerializableResource.new(
InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token),
serializer: InitialStateSerializer
).to_json
end
private
def check_enabled
raise ActiveRecord::RecordNotFound unless Setting.timeline_preview
not_found unless Setting.timeline_preview
end
def set_body_classes

@ -1,11 +1,11 @@
# frozen_string_literal: true
class RemoteFollowController < ApplicationController
include AccountOwnedConcern
layout 'modal'
before_action :set_account
before_action :set_pack
before_action :gone, if: :suspended_account?
before_action :set_body_classes
def new
@ -37,14 +37,6 @@ class RemoteFollowController < ApplicationController
use_pack 'modal'
end
def set_account
@account = Account.find_local!(params[:account_username])
end
def suspended_account?
@account.suspended?
end
def set_body_classes
@body_classes = 'modal-layout'
@hide_header = true

@ -1,39 +0,0 @@
# frozen_string_literal: true
class RemoteUnfollowsController < ApplicationController
layout 'modal'
before_action :authenticate_user!
before_action :set_body_classes
def create
@account = unfollow_attempt.try(:target_account)
if @account.nil?
render :error
else
render :success
end
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
render :error
end
private
def unfollow_attempt
username, domain = acct_without_prefix.split('@')
UnfollowService.new.call(current_account, Account.find_remote!(username, domain))
end
def acct_without_prefix
acct_params.gsub(/\Aacct:/, '')
end
def acct_params
params.fetch(:acct, '')
end
def set_body_classes
@body_classes = 'modal-layout'
end
end

@ -11,7 +11,7 @@ module Settings
def create
if current_user.validate_and_consume_otp!(confirmation_params[:code])
flash[:notice] = I18n.t('two_factor_authentication.enabled_success')
flash.now[:notice] = I18n.t('two_factor_authentication.enabled_success')
current_user.otp_required_for_login = true
@recovery_codes = current_user.generate_otp_backup_codes!

@ -6,7 +6,7 @@ module Settings
def create
@recovery_codes = current_user.generate_otp_backup_codes!
current_user.save!
flash[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated')
flash.now[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated')
render :index
end
end

@ -1,24 +1,22 @@
# frozen_string_literal: true
class StatusesController < ApplicationController
include StatusControllerConcern
include SignatureAuthentication
include Authorization
ANCESTORS_LIMIT = 40
DESCENDANTS_LIMIT = 60
DESCENDANTS_DEPTH_LIMIT = 20
include AccountOwnedConcern
layout 'public'
before_action :set_account
before_action :require_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status
before_action :set_instance_presenter
before_action :set_link_headers
before_action :check_account_suspension
before_action :redirect_to_original, only: [:show]
before_action :set_referrer_policy_header, only: [:show]
before_action :redirect_to_original, only: :show
before_action :set_referrer_policy_header, only: :show
before_action :set_cache_headers
before_action :set_replies, only: [:replies]
before_action :set_body_classes
before_action :set_autoplay, only: :embed
content_security_policy only: :embed do |p|
p.frame_ancestors(false)
@ -30,27 +28,20 @@ class StatusesController < ApplicationController
use_pack 'public'
expires_in 10.seconds, public: true if current_account.nil?
@body_classes = 'with-modals'
set_ancestors
set_descendants
render 'stream_entries/show'
end
format.json do
render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
end
expires_in 3.minutes, public: @status.distributable? && public_fetch_mode?
render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
end
end
end
def activity
render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
end
expires_in 3.minutes, public: @status.distributable? && public_fetch_mode?
render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
end
def embed
@ -59,130 +50,24 @@ class StatusesController < ApplicationController
expires_in 180, public: true
response.headers['X-Frame-Options'] = 'ALLOWALL'
@autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay])
render 'stream_entries/embed', layout: 'embedded'
end
def replies
render json: replies_collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json',
skip_activities: true
render layout: 'embedded'
end
private
def replies_collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: replies_account_status_url(@account, @status, page_params),
type: :unordered,
part_of: replies_account_status_url(@account, @status),
next: next_page,
items: @replies.map { |status| status.local ? status : status.id }
)
if page_requested?
page
else
ActivityPub::CollectionPresenter.new(
id: replies_account_status_url(@account, @status),
type: :unordered,
first: page
)
end
end
def create_descendant_thread(starting_depth, statuses)
depth = starting_depth + statuses.size
if depth < DESCENDANTS_DEPTH_LIMIT
{ statuses: statuses, starting_depth: starting_depth }
else
next_status = statuses.pop
{ statuses: statuses, starting_depth: starting_depth, next_status: next_status }
end
end
def set_account
@account = Account.find_local!(params[:account_username])
end
def set_ancestors
@ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
@next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
end
def set_descendants
@max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i
@since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i
descendants = cache_collection(
@status.descendants(
DESCENDANTS_LIMIT,
current_account,
@max_descendant_thread_id,
@since_descendant_thread_id,
DESCENDANTS_DEPTH_LIMIT
),
Status
)
@descendant_threads = []
if descendants.present?
statuses = [descendants.first]
starting_depth = 0
descendants.drop(1).each_with_index do |descendant, index|
if descendants[index].id == descendant.in_reply_to_id
statuses << descendant
else
@descendant_threads << create_descendant_thread(starting_depth, statuses)
# The thread is broken, assume it's a reply to the root status
starting_depth = 0
# ... unless we can find its ancestor in one of the already-processed threads
@descendant_threads.reverse_each do |descendant_thread|
statuses = descendant_thread[:statuses]
index = statuses.find_index do |thread_status|
thread_status.id == descendant.in_reply_to_id
end
if index.present?
starting_depth = descendant_thread[:starting_depth] + index + 1
break
end
end
statuses = [descendant]
end
end
@descendant_threads << create_descendant_thread(starting_depth, statuses)
end
@max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT
def set_body_classes
@body_classes = 'with-modals'
end
def set_link_headers
response.headers['Link'] = LinkHeader.new(
[
[account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]],
]
)
response.headers['Link'] = LinkHeader.new([[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]])
end
def set_status
@status = @account.statuses.find(params[:id])
@stream_entry = @status.stream_entry
@type = @stream_entry.activity_type.downcase
@status = @account.statuses.find(params[:id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404
raise ActiveRecord::RecordNotFound
end
@ -190,39 +75,15 @@ class StatusesController < ApplicationController
@instance_presenter = InstancePresenter.new
end
def check_account_suspension
gone if @account.suspended?
end
def redirect_to_original
redirect_to ::TagManager.instance.url_for(@status.reblog) if @status.reblog?
redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog?
end
def set_referrer_policy_header
return if @status.public_visibility? || @status.unlisted_visibility?
response.headers['Referrer-Policy'] = 'origin'
end
def page_requested?
params[:page] == 'true'
end
def set_replies
@replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end
def next_page
last_reply = @replies.last
return if last_reply.nil?
same_account = last_reply.account_id == @account.id
return unless same_account || @replies.size == DESCENDANTS_LIMIT
same_account = false unless @replies.size == DESCENDANTS_LIMIT
replies_account_status_url(@account, @status, page: true, min_id: last_reply.id, other_accounts: !same_account)
response.headers['Referrer-Policy'] = 'origin' unless @status.distributable?
end
def page_params
{ page: true, other_accounts: params[:other_accounts], min_id: params[:min_id] }.compact
def set_autoplay
@autoplay = truthy_param?(:autoplay)
end
end

@ -1,66 +0,0 @@
# frozen_string_literal: true
class StreamEntriesController < ApplicationController
include Authorization
include SignatureVerification
layout 'public'
before_action :set_account
before_action :set_stream_entry
before_action :set_link_headers
before_action :check_account_suspension
before_action :set_cache_headers
def show
respond_to do |format|
format.html do
use_pack 'public'
expires_in 5.minutes, public: true unless @stream_entry.hidden?
redirect_to short_account_status_url(params[:account_username], @stream_entry.activity)
end
format.atom do
expires_in 3.minutes, public: true unless @stream_entry.hidden?
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
end
end
end
def embed
redirect_to embed_short_account_status_url(@account, @stream_entry.activity), status: 301
end
private
def set_account
@account = Account.find_local!(params[:account_username])
end
def set_link_headers
response.headers['Link'] = LinkHeader.new(
[
[account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
[ActivityPub::TagManager.instance.uri_for(@stream_entry.activity), [%w(rel alternate), %w(type application/activity+json)]],
]
)
end
def set_stream_entry
@stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id])
@type = 'status'
raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil?
authorize @stream_entry.activity, :show? if @stream_entry.hidden? || @stream_entry.local_only?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404
raise ActiveRecord::RecordNotFound
end
def check_account_suspension
gone if @account.suspended?
end
end

@ -1,19 +1,23 @@
# frozen_string_literal: true
class TagsController < ApplicationController
include SignatureVerification
PAGE_SIZE = 20
layout 'public'
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_tag
before_action :set_body_classes
before_action :set_instance_presenter
def show
@tag = Tag.find_normalized!(params[:id])
respond_to do |format|
format.html do
use_pack 'about'
expires_in 0, public: true
@initial_state_json = ActiveModelSerializers::SerializableResource.new(
InitialStatePresenter.new(settings: {}, token: current_session&.token),
serializer: InitialStateSerializer
@ -21,6 +25,8 @@ class TagsController < ApplicationController
end
format.rss do
expires_in 0, public: true
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none)).limit(PAGE_SIZE)
@statuses = cache_collection(@statuses, Status)
@ -28,19 +34,22 @@ class TagsController < ApplicationController
end
format.json do
expires_in 3.minutes, public: public_fetch_mode?
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
@statuses = cache_collection(@statuses, Status)
render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
end
end
private
def set_tag
@tag = Tag.find_normalized!(params[:id])
end
def set_body_classes
@body_classes = 'with-modals'
end

@ -13,7 +13,7 @@ module WellKnown
format.xml { render content_type: 'application/xrd+xml' }
end
expires_in(3.days, public: true)
expires_in 3.days, public: true
end
end
end

@ -19,7 +19,7 @@ module WellKnown
end
end
expires_in(3.days, public: true)
expires_in 3.days, public: true
rescue ActiveRecord::RecordNotFound
head 404
end
@ -27,12 +27,9 @@ module WellKnown
private
def username_from_resource
resource_user = resource_param
resource_user = resource_param
username, domain = resource_user.split('@')
if Rails.configuration.x.alternate_domains.include?(domain)
resource_user = "#{username}@#{Rails.configuration.x.local_domain}"
end
resource_user = "#{username}@#{Rails.configuration.x.local_domain}" if Rails.configuration.x.alternate_domains.include?(domain)
WebfingerResource.new(resource_user).username
end

@ -89,7 +89,7 @@ module Admin::ActionLogsHelper
when 'DomainBlock', 'EmailDomainBlock'
link_to record.domain, "https://#{record.domain}"
when 'Status'
link_to record.account.acct, TagManager.instance.url_for(record)
link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record)
when 'AccountWarning'
link_to record.target_account.acct, admin_account_path(record.target_account_id)
end

@ -0,0 +1,17 @@
# frozen_string_literal: true
module DomainControlHelper
def domain_not_allowed?(uri_or_domain)
return if uri_or_domain.blank?
domain = begin
if uri_or_domain.include?('://')
Addressable::URI.parse(uri_or_domain).domain
else
uri_or_domain
end
end
DomainBlock.blocked?(domain)
end
end

@ -21,7 +21,7 @@ module HomeHelper
end
end
else
link_to(path || TagManager.instance.url_for(account), class: 'account__display-name') do
link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do
content_tag(:div, class: 'account__avatar-wrapper') do
content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url)})")
end +

@ -16,13 +16,15 @@ module JsonLdHelper
# The url attribute can be a string, an array of strings, or an array of objects.
# The objects could include a mimeType. Not-included mimeType means it's text/html.
def url_to_href(value, preferred_type = nil)
single_value = if value.is_a?(Array) && !value.first.is_a?(String)
value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) }
elsif value.is_a?(Array)
value.first
else
value
end
single_value = begin
if value.is_a?(Array) && !value.first.is_a?(String)
value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) }
elsif value.is_a?(Array)
value.first
else
value
end
end
if single_value.nil? || single_value.is_a?(String)
single_value
@ -64,7 +66,9 @@ module JsonLdHelper
def fetch_resource(uri, id, on_behalf_of = nil)
unless id
json = fetch_resource_without_id_validation(uri, on_behalf_of)
return unless json
uri = json['id']
end
@ -73,25 +77,20 @@ module JsonLdHelper
end
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false)
on_behalf_of ||= Account.representative
build_request(uri, on_behalf_of).perform do |response|
unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
raise Mastodon::UnexpectedResponseError, response
end
return body_to_json(response.body_with_limit) if response.code == 200
end
# If request failed, retry without doing it on behalf of a user
return if on_behalf_of.nil?
build_request(uri).perform do |response|
unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
raise Mastodon::UnexpectedResponseError, response
end
response.code == 200 ? body_to_json(response.body_with_limit) : nil
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
body_to_json(response.body_with_limit) if response.code == 200
end
end
def body_to_json(body, compare_id: nil)
json = body.is_a?(String) ? Oj.load(body, mode: :strict) : body
return if compare_id.present? && json['id'] != compare_id
json
rescue Oj::ParseError
nil
@ -105,35 +104,34 @@ module JsonLdHelper
end
end
private
def response_successful?(response)
(200...300).cover?(response.code)
end
def response_error_unsalvageable?(response)
(400...500).cover?(response.code) && response.code != 429
response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
end
def build_request(uri, on_behalf_of = nil)
request = Request.new(:get, uri)
request.on_behalf_of(on_behalf_of) if on_behalf_of
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
request
Request.new(:get, uri).tap do |request|
request.on_behalf_of(on_behalf_of) if on_behalf_of
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
end
end
def load_jsonld_context(url, _options = {}, &_block)
json = Rails.cache.fetch("jsonld:context:#{url}", expires_in: 30.days, raw: true) do
request = Request.new(:get, url)
request.add_headers('Accept' => 'application/ld+json')
request.perform do |res|
raise JSON::LD::JsonLdError::LoadingDocumentFailed unless res.code == 200 && res.mime_type == 'application/ld+json'
res.body_with_limit
end
end
doc = JSON::LD::API::RemoteDocument.new(url, json)
block_given? ? yield(doc) : doc
end
end

@ -1,6 +1,6 @@
# frozen_string_literal: true
module StreamEntriesHelper
module StatusesHelper
EMBEDDED_CONTROLLER = 'statuses'
EMBEDDED_ACTION = 'embed'
@ -115,11 +115,13 @@ module StreamEntriesHelper
def status_text_summary(status)
return if status.spoiler_text.blank?
I18n.t('statuses.content_warning', warning: status.spoiler_text)
end
def poll_summary(status)
return unless status.preloadable_poll
status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n")
end

@ -47,7 +47,7 @@ const getProfileAvatarAnimationHandler = (swapTo) => {
return ({ target }) => {
const swapSrc = target.getAttribute(swapTo);
//only change the img source if autoplay is off and the image src is actually different
if(target.getAttribute('data-autoplay') === 'false' && target.src !== swapSrc) {
if(target.getAttribute('data-autoplay') !== 'true' && target.src !== swapSrc) {
target.src = swapSrc;
}
};

@ -147,6 +147,10 @@
min-height: 100%;
}
.flash-message {
margin-bottom: 10px;
}
@media screen and (max-width: 738px) {
grid-template-columns: minmax(0, 50%) minmax(0, 50%);

@ -14,7 +14,7 @@
@import 'widgets';
@import 'forms';
@import 'accounts';
@import 'stream_entries';
@import 'statuses';
@import 'components/index';
@import 'polls';
@import 'about';

@ -122,11 +122,11 @@ class DropdownMenu extends React.PureComponent {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, href = '#' } = option;
const { text, href = '#', target = '_blank', method } = option;
return (
<li className='dropdown-menu__item' key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleItemKeyDown} data-index={i}>
<a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleItemKeyDown} data-index={i}>
{text}
</a>
</li>

@ -35,6 +35,7 @@ export default class ScrollableList extends PureComponent {
alwaysPrepend: PropTypes.bool,
emptyMessage: PropTypes.node,
children: PropTypes.node,
bindToDocument: PropTypes.bool,
};
static defaultProps = {
@ -50,7 +51,9 @@ export default class ScrollableList extends PureComponent {
handleScroll = throttle(() => {
if (this.node) {
const { scrollTop, scrollHeight, clientHeight } = this.node;
const scrollTop = this.getScrollTop();
const scrollHeight = this.getScrollHeight();
const clientHeight = this.getClientHeight();
const offset = scrollHeight - scrollTop - clientHeight;
if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
@ -80,9 +83,14 @@ export default class ScrollableList extends PureComponent {
scrollToTopOnMouseIdle = false;
setScrollTop = newScrollTop => {
if (this.node.scrollTop !== newScrollTop) {
if (this.getScrollTop() !== newScrollTop) {
this.lastScrollWasSynthetic = true;
this.node.scrollTop = newScrollTop;
if (this.props.bindToDocument) {
document.scrollingElement.scrollTop = newScrollTop;
} else {
this.node.scrollTop = newScrollTop;
}
}
};
@ -100,7 +108,7 @@ export default class ScrollableList extends PureComponent {
this.clearMouseIdleTimer();
this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
if (!this.mouseMovedRecently && this.node.scrollTop === 0) {
if (!this.mouseMovedRecently && this.getScrollTop() === 0) {
// Only set if we just started moving and are scrolled to the top.
this.scrollToTopOnMouseIdle = true;
}
@ -135,15 +143,27 @@ export default class ScrollableList extends PureComponent {
}
getScrollPosition = () => {
if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) {
return { height: this.node.scrollHeight, top: this.node.scrollTop };
if (this.node && (this.getScrollTop() > 0 || this.mouseMovedRecently)) {
return { height: this.getScrollHeight(), top: this.getScrollTop() };
} else {
return null;
}
}
getScrollTop = () => {
return this.props.bindToDocument ? document.scrollingElement.scrollTop : this.node.scrollTop;
}
getScrollHeight = () => {
return this.props.bindToDocument ? document.scrollingElement.scrollHeight : this.node.scrollHeight;
}
getClientHeight = () => {
return this.props.bindToDocument ? document.scrollingElement.clientHeight : this.node.clientHeight;
}
updateScrollBottom = (snapshot) => {
const newScrollTop = this.node.scrollHeight - snapshot;
const newScrollTop = this.getScrollHeight() - snapshot;
this.setScrollTop(newScrollTop);
}
@ -153,8 +173,8 @@ export default class ScrollableList extends PureComponent {
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
if (someItemInserted && (this.node.scrollTop > 0 || this.mouseMovedRecently)) {
return this.node.scrollHeight - this.node.scrollTop;
if (someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently)) {
return this.getScrollHeight() - this.getScrollTop();
} else {
return null;
}
@ -164,7 +184,7 @@ export default class ScrollableList extends PureComponent {
// Reset the scroll position when a new child comes in in order not to
// jerk the scrollbar around if you're already scrolled down the page.
if (snapshot !== null) {
this.setScrollTop(this.node.scrollHeight - snapshot);
this.setScrollTop(this.getScrollHeight() - snapshot);
}
}
@ -197,13 +217,23 @@ export default class ScrollableList extends PureComponent {
}
attachScrollListener () {
this.node.addEventListener('scroll', this.handleScroll);
this.node.addEventListener('wheel', this.handleWheel);
if (this.props.bindToDocument) {
document.addEventListener('scroll', this.handleScroll);
document.addEventListener('wheel', this.handleWheel);
} else {
this.node.addEventListener('scroll', this.handleScroll);
this.node.addEventListener('wheel', this.handleWheel);
}
}
detachScrollListener () {
this.node.removeEventListener('scroll', this.handleScroll);
this.node.removeEventListener('wheel', this.handleWheel);
if (this.props.bindToDocument) {
document.removeEventListener('scroll', this.handleScroll);
document.removeEventListener('wheel', this.handleWheel);
} else {
this.node.removeEventListener('scroll', this.handleScroll);
this.node.removeEventListener('wheel', this.handleWheel);
}
}
getFirstChildKey (props) {

@ -8,6 +8,7 @@ import Video from '../features/video';
import Card from '../features/status/components/card';
import Poll from 'mastodon/components/poll';
import ModalRoot from '../components/modal_root';
import { getScrollbarWidth } from '../features/ui/components/modal_root';
import MediaModal from '../features/ui/components/media_modal';
import { List as ImmutableList, fromJS } from 'immutable';
@ -31,6 +32,8 @@ export default class MediaContainer extends PureComponent {
handleOpenMedia = (media, index) => {
document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
this.setState({ media, index });
}
@ -38,11 +41,15 @@ export default class MediaContainer extends PureComponent {
const media = ImmutableList([video]);
document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
this.setState({ media, time });
}
handleCloseMedia = () => {
document.body.classList.remove('with-modals--active');
document.documentElement.style.marginRight = 0;
this.setState({ media: null, index: null, time: null });
}

@ -44,6 +44,7 @@ class AccountTimeline extends ImmutablePureComponent {
withReplies: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
multiColumn: PropTypes.bool,
};
componentWillMount () {
@ -77,7 +78,7 @@ class AccountTimeline extends ImmutablePureComponent {
}
render () {
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount } = this.props;
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount, multiColumn } = this.props;
if (!isAccount) {
return (
@ -112,6 +113,7 @@ class AccountTimeline extends ImmutablePureComponent {
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
/>
</Column>
);

@ -32,6 +32,7 @@ class Blocks extends ImmutablePureComponent {
accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
intl: PropTypes.object.isRequired,
multiColumn: PropTypes.bool,
};
componentWillMount () {
@ -43,7 +44,7 @@ class Blocks extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, accountIds, shouldUpdateScroll, hasMore } = this.props;
const { intl, accountIds, shouldUpdateScroll, hasMore, multiColumn } = this.props;
if (!accountIds) {
return (
@ -64,6 +65,7 @@ class Blocks extends ImmutablePureComponent {
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} />

@ -126,6 +126,7 @@ class CommunityTimeline extends React.PureComponent {
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>
);

@ -15,6 +15,7 @@ const messages = defineMessages({
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
});
export default @injectIntl
@ -42,6 +43,8 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.logout), href: '/auth/sign_out', target: null, method: 'delete' });
return (
<div className='compose__action-bar'>

@ -33,6 +33,7 @@ class Blocks extends ImmutablePureComponent {
hasMore: PropTypes.bool,
domains: ImmutablePropTypes.orderedSet,
intl: PropTypes.object.isRequired,
multiColumn: PropTypes.bool,
};
componentWillMount () {
@ -44,7 +45,7 @@ class Blocks extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, domains, shouldUpdateScroll, hasMore } = this.props;
const { intl, domains, shouldUpdateScroll, hasMore, multiColumn } = this.props;
if (!domains) {
return (
@ -65,6 +66,7 @@ class Blocks extends ImmutablePureComponent {
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{domains.map(domain =>
<DomainContainer key={domain} domain={domain} />

@ -95,6 +95,7 @@ class Favourites extends ImmutablePureComponent {
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
/>
</Column>
);

@ -23,6 +23,7 @@ class Favourites extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
};
componentWillMount () {
@ -36,7 +37,7 @@ class Favourites extends ImmutablePureComponent {
}
render () {
const { shouldUpdateScroll, accountIds } = this.props;
const { shouldUpdateScroll, accountIds, multiColumn } = this.props;
if (!accountIds) {
return (
@ -56,6 +57,7 @@ class Favourites extends ImmutablePureComponent {
scrollKey='favourites'
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />

@ -32,6 +32,7 @@ class FollowRequests extends ImmutablePureComponent {
hasMore: PropTypes.bool,
accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
multiColumn: PropTypes.bool,
};
componentWillMount () {
@ -43,7 +44,7 @@ class FollowRequests extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, shouldUpdateScroll, accountIds, hasMore } = this.props;
const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn } = this.props;
if (!accountIds) {
return (
@ -64,6 +65,7 @@ class FollowRequests extends ImmutablePureComponent {
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountAuthorizeContainer key={id} id={id} />

@ -36,6 +36,7 @@ class Followers extends ImmutablePureComponent {
hasMore: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
multiColumn: PropTypes.bool,
};
componentWillMount () {
@ -55,7 +56,7 @@ class Followers extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount } = this.props;
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn } = this.props;
if (!isAccount) {
return (
@ -87,6 +88,7 @@ class Followers extends ImmutablePureComponent {
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
alwaysPrepend
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{blockedBy ? [] : accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />

@ -36,6 +36,7 @@ class Following extends ImmutablePureComponent {
hasMore: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
multiColumn: PropTypes.bool,
};
componentWillMount () {
@ -55,7 +56,7 @@ class Following extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount } = this.props;
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn } = this.props;
if (!isAccount) {
return (
@ -87,6 +88,7 @@ class Following extends ImmutablePureComponent {
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
alwaysPrepend
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{blockedBy ? [] : accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />

@ -157,6 +157,7 @@ class HashtagTimeline extends React.PureComponent {
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>
);

@ -119,6 +119,7 @@ class HomeTimeline extends React.PureComponent {
timelineId='home'
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>
);

@ -184,6 +184,7 @@ class ListTimeline extends React.PureComponent {
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>
);

@ -40,6 +40,7 @@ class Lists extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired,
lists: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
multiColumn: PropTypes.bool,
};
componentWillMount () {
@ -47,7 +48,7 @@ class Lists extends ImmutablePureComponent {
}
render () {
const { intl, shouldUpdateScroll, lists } = this.props;
const { intl, shouldUpdateScroll, lists, multiColumn } = this.props;
if (!lists) {
return (
@ -70,6 +71,7 @@ class Lists extends ImmutablePureComponent {
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
prepend={<ColumnSubheading text={intl.formatMessage(messages.subheading)} />}
bindToDocument={!multiColumn}
>
{lists.map(list =>
<ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />

@ -32,6 +32,7 @@ class Mutes extends ImmutablePureComponent {
hasMore: PropTypes.bool,
accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
multiColumn: PropTypes.bool,
};
componentWillMount () {
@ -43,7 +44,7 @@ class Mutes extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, shouldUpdateScroll, hasMore, accountIds } = this.props;
const { intl, shouldUpdateScroll, hasMore, accountIds, multiColumn } = this.props;
if (!accountIds) {
return (
@ -64,6 +65,7 @@ class Mutes extends ImmutablePureComponent {
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} />

@ -191,6 +191,7 @@ class Notifications extends React.PureComponent {
onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
>
{scrollableContent}
</ScrollableList>

@ -28,6 +28,7 @@ class PinnedStatuses extends ImmutablePureComponent {
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
hasMore: PropTypes.bool.isRequired,
multiColumn: PropTypes.bool,
};
componentWillMount () {
@ -43,7 +44,7 @@ class PinnedStatuses extends ImmutablePureComponent {
}
render () {
const { intl, shouldUpdateScroll, statusIds, hasMore } = this.props;
const { intl, shouldUpdateScroll, statusIds, hasMore, multiColumn } = this.props;
return (
<Column icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}>
@ -53,6 +54,7 @@ class PinnedStatuses extends ImmutablePureComponent {
scrollKey='pinned_statuses'
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>
);

@ -126,6 +126,7 @@ class PublicTimeline extends React.PureComponent {
scrollKey={`public_timeline-${columnId}`}
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>
);

@ -23,6 +23,7 @@ class Reblogs extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
};
componentWillMount () {
@ -36,7 +37,7 @@ class Reblogs extends ImmutablePureComponent {
}
render () {
const { shouldUpdateScroll, accountIds } = this.props;
const { shouldUpdateScroll, accountIds, multiColumn } = this.props;
if (!accountIds) {
return (
@ -56,6 +57,7 @@ class Reblogs extends ImmutablePureComponent {
scrollKey='reblogs'
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />

@ -32,6 +32,28 @@ const MODAL_COMPONENTS = {
'LIST_ADDER':ListAdder,
};
let cachedScrollbarWidth = null;
export const getScrollbarWidth = () => {
if (cachedScrollbarWidth !== null) {
return cachedScrollbarWidth;
}
const outer = document.createElement('div');
outer.style.visibility = 'hidden';
outer.style.overflow = 'scroll';
document.body.appendChild(outer);
const inner = document.createElement('div');
outer.appendChild(inner);
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
cachedScrollbarWidth = scrollbarWidth;
outer.parentNode.removeChild(outer);
return scrollbarWidth;
};
export default class ModalRoot extends React.PureComponent {
static propTypes = {
@ -47,8 +69,10 @@ export default class ModalRoot extends React.PureComponent {
componentDidUpdate (prevProps, prevState, { visible }) {
if (visible) {
document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
} else {
document.body.classList.remove('with-modals--active');
document.documentElement.style.marginRight = 0;
}
}

@ -110,12 +110,25 @@ class SwitchingColumnsArea extends React.PureComponent {
componentWillMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
if (this.state.mobile || forceSingleColumn) {
document.body.classList.toggle('layout-single-column', true);
document.body.classList.toggle('layout-multiple-columns', false);
} else {
document.body.classList.toggle('layout-single-column', false);
document.body.classList.toggle('layout-multiple-columns', true);
}
}
componentDidUpdate (prevProps) {
componentDidUpdate (prevProps, prevState) {
if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
this.node.handleChildrenContentChange();
}
if (prevState.mobile !== this.state.mobile && !forceSingleColumn) {
document.body.classList.toggle('layout-single-column', this.state.mobile);
document.body.classList.toggle('layout-multiple-columns', !this.state.mobile);
}
}
componentWillUnmount () {

@ -156,6 +156,7 @@
"home.column_settings.basic": "أساسية",
"home.column_settings.show_reblogs": "عرض الترقيات",
"home.column_settings.show_replies": "عرض الردود",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# يوم} other {# أيام}}",
"intervals.full.hours": "{number, plural, one {# ساعة} other {# ساعات}}",
"intervals.full.minutes": "{number, plural, one {# دقيقة} other {# دقائق}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "عنوان القائمة الجديدة",
"lists.search": "إبحث في قائمة الحسابات التي تُتابِعها",
"lists.subheading": "قوائمك",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "تحميل...",
"media_gallery.toggle_visible": "عرض / إخفاء",
"missing_indicator.label": "تعذر العثور عليه",
@ -314,6 +316,7 @@
"search_results.accounts": "أشخاص",
"search_results.hashtags": "الوُسوم",
"search_results.statuses": "التبويقات",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {result} و {results}}",
"status.admin_account": "افتح الواجهة الإدارية لـ @{name}",
"status.admin_status": "افتح هذا المنشور على واجهة الإشراف",

@ -64,7 +64,7 @@
"column_header.show_settings": "Show settings",
"column_header.unpin": "Desfixar",
"column_subheading.settings": "Axustes",
"community.column_settings.media_only": "Media Only",
"community.column_settings.media_only": "Media only",
"compose_form.direct_message_warning": "Esti toot namái va unviase a los usuarios mentaos.",
"compose_form.direct_message_warning_learn_more": "Learn more",
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
@ -156,6 +156,7 @@
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Amosar toots compartíos",
"home.column_settings.show_replies": "Amosar rempuestes",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Títulu nuevu de la llista",
"lists.search": "Guetar ente la xente que sigues",
"lists.subheading": "Les tos llistes",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Cargando...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Nun s'alcontró",
@ -314,6 +316,7 @@
"search_results.accounts": "Xente",
"search_results.hashtags": "Etiquetes",
"search_results.statuses": "Toots",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface",

@ -64,7 +64,7 @@
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.settings": "Settings",
"community.column_settings.media_only": "Media Only",
"community.column_settings.media_only": "Media only",
"compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
"compose_form.direct_message_warning_learn_more": "Learn more",
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
@ -156,6 +156,7 @@
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Зареждане...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
@ -314,6 +316,7 @@
"search_results.accounts": "People",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface",

@ -1,13 +1,13 @@
{
"account.add_or_remove_from_list": "তালিকাতে আরো যুক্ত বা মুছে ফেলুন",
"account.badges.bot": "রোবট",
"account.block": "@{name} বন্ধ করুন",
"account.block": "@{name} কে বন্ধ করুন",
"account.block_domain": "{domain} থেকে সব সরিয়ে ফেলুন",
"account.blocked": "বন্ধ করা হয়েছে",
"account.direct": "@{name}কে সরকারি লিখুন",
"account.direct": "@{name} এর কাছে সরকারি লেখা পাঠাতে",
"account.domain_blocked": "ওয়েবসাইট সরিয়ে ফেলা হয়েছে",
"account.edit_profile": "নিজের পাতা সম্পাদনা করুন",
"account.endorse": "নিজের পাতায় দেখান",
"account.edit_profile": "নিজের পাতা সম্পাদনা করতে",
"account.endorse": "আপনার নিজের পাতায় দেখাতে",
"account.follow": "অনুসরণ করুন",
"account.followers": "অনুসরণকারক",
"account.followers.empty": "এই ব্যবহারকারীকে কেও এখনো অনুসরণ করে না।",
@ -18,21 +18,21 @@
"account.link_verified_on": "এই লিংকের মালিকানা চেক করা হয়েছে {date} তারিকে",
"account.locked_info": "এই নিবন্ধনের গোপনীয়তার ক্ষেত্র তালা দেওয়া আছে। নিবন্ধনকারী অনুসরণ করার অনুমতি যাদেরকে দেবেন, শুধু তারাই অনুসরণ করতে পারবেন।",
"account.media": "ছবি বা ভিডিও",
"account.mention": "@{name} কে উল্লেখ করুন",
"account.mention": "@{name} কে উল্লেখ করতে",
"account.moved_to": "{name} চলে গেছে এখানে:",
"account.mute": "@{name}র কার্যক্রম সরিয়ে ফেলুন",
"account.mute": "@{name} সব কার্যক্রম আপনার সময়রেখা থেকে সরিয়ে ফেলতে",
"account.mute_notifications": "@{name}র প্রজ্ঞাপন আপনার কাছ থেকে সরিয়ে ফেলুন",
"account.muted": "সরানো আছে",
"account.posts": "টুট",
"account.posts_with_replies": "টুট এবং মতামত",
"account.report": "@{name}কে রিপোর্ট করে দিন",
"account.report": "@{name} কে রিপোর্ট করে",
"account.requested": "অনুমতির অপেক্ষায় আছে। অনুসরণ করার অনুরোধ বাতিল করতে এখানে ক্লিক করুন",
"account.share": "@{name}র পাতা অন্যদের দেখান",
"account.show_reblogs": "@{name}র সমর্থনগুলো দেখুন",
"account.unblock": "@{name}র কার্যকলাপ আবার দেখুন",
"account.unblock_domain": "{domain}থেকে আবার দেখুন",
"account.unendorse": "নিজের পাতায় এটা দেখতে চান না",
"account.unfollow": "অনুসরণ বন্ধ করুন",
"account.unendorse": "আপনার নিজের পাতায় এটা না দেখতে",
"account.unfollow": "অনুসরণ না করতে",
"account.unmute": "@{name}র কার্যকলাপ আবার দেখুন",
"account.unmute_notifications": "@{name}র প্রজ্ঞাপন দেওয়ার অনুমতি দিন",
"alert.unexpected.message": "অপ্রত্যাশিত একটি সমস্যা হয়েছে।",
@ -42,7 +42,7 @@
"bundle_column_error.retry": "আবার চেষ্টা করুন",
"bundle_column_error.title": "নেটওয়ার্কের সমস্যা হচ্ছে",
"bundle_modal_error.close": "বন্ধ করুন",
"bundle_modal_error.message": "এই অংশটি দেখতে যেয়ে কোনো সমস্যা হয়েছে।",
"bundle_modal_error.message": "এই অংশটি দেখতে যেয়ে কোনো সমস্যা হয়েছে।",
"bundle_modal_error.retry": "আবার চেষ্টা করুন",
"column.blocks": "যাদের বন্ধ করে রাখা হয়েছে",
"column.community": "স্থানীয় সময়সারি",
@ -77,12 +77,12 @@
"compose_form.poll.remove_option": "এই বিকল্পটি মুছে ফেলুন",
"compose_form.publish": "টুট",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Mark media as sensitive",
"compose_form.sensitive.hide": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করতে",
"compose_form.sensitive.marked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়েছে",
"compose_form.sensitive.unmarked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়নি",
"compose_form.spoiler.marked": "লেখাটি সাবধানতার পেছনে লুকানো আছে",
"compose_form.spoiler.unmarked": "লেখাটি লুকানো নেই",
"compose_form.spoiler_placeholder": "আপনার সাবধানতা এখানে লিখুন",
"compose_form.spoiler_placeholder": "আপনার লেখা দেখার সাবধানবাণী লিখুন",
"confirmation_modal.cancel": "বাতিল করুন",
"confirmations.block.block_and_report": "বন্ধ করুন এবং রিপোর্ট করুন",
"confirmations.block.confirm": "বন্ধ করুন",
@ -99,7 +99,7 @@
"confirmations.redraft.message": "আপনি কি নিশ্চিত এটি মুছে ফেলে এবং আবার সম্পাদন করতে চান ? এটাতে যা পছন্দিত, সমর্থন বা মতামত আছে সেগুলো নতুন লেখার সাথে যুক্ত থাকবে না।",
"confirmations.reply.confirm": "মতামত",
"confirmations.reply.message": "এখন মতামত লিখতে গেলে আপনার এখন যেটা লিখছেন সেটা মুছে যাবে। আপনি নি নিশ্চিত এটা করতে চান ?",
"confirmations.unfollow.confirm": "অনুসরণ বন্ধ করুন",
"confirmations.unfollow.confirm": "অনুসরণ করা বাতিল করতে",
"confirmations.unfollow.message": "আপনি কি নিশ্চিত {name} কে আর অনুসরণ করতে চান না ?",
"embed.instructions": "এই লেখাটি আপনার ওয়েবসাইটে যুক্ত করতে নিচের কোডটি বেবহার করুন।",
"embed.preview": "সেটা দেখতে এরকম হবে:",
@ -137,11 +137,11 @@
"follow_request.authorize": "অনুমতি দিন",
"follow_request.reject": "প্রত্যাখ্যান করুন",
"getting_started.developers": "তৈরিকারকদের জন্য",
"getting_started.directory": "নিজস্ব পাতার তালিকা",
"getting_started.directory": "নিজস্ব-পাতাগুলির তালিকা",
"getting_started.documentation": "নথিপত্র",
"getting_started.heading": "শুরু করা",
"getting_started.invite": "অন্যদের আমন্ত্রণ করুন",
"getting_started.open_source_notice": "মাস্টাডন একটি মুক্ত সফটওয়্যার। আপনি তৈরিতে সাহায্য করতে পারেন অথবা সমস্যা রিপোর্ট করতে পারেন গিটহাবে {github}।",
"getting_started.open_source_notice": "মাস্টাডন একটি মুক্ত সফটওয়্যার। তৈরিতে সাহায্য করতে বা কোনো সমস্যা সম্পর্কে জানাতে আমাদের গিটহাবে যেতে পারেন {github}।",
"getting_started.security": "নিরাপত্তা",
"getting_started.terms": "ব্যবহারের নিয়মাবলী",
"hashtag.column_header.tag_mode.all": "এবং {additional}",
@ -152,10 +152,11 @@
"hashtag.column_settings.tag_mode.all": "এগুলো সব",
"hashtag.column_settings.tag_mode.any": "এর ভেতরে যেকোনোটা",
"hashtag.column_settings.tag_mode.none": "এগুলোর একটাও না",
"hashtag.column_settings.tag_toggle": "আরো ট্যাগ এই কলামে যুক্ত করুন",
"hashtag.column_settings.tag_toggle": "আরো ট্যাগ এই কলামে যুক্ত করতে",
"home.column_settings.basic": "সাধারণ",
"home.column_settings.show_reblogs": "সমর্থনগুলো দেখান",
"home.column_settings.show_replies": "মতামত দেখান",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# ঘটা} other {# ঘটা}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -195,7 +196,7 @@
"keyboard_shortcuts.local": "স্থানীয় সময়রেখাতে যেতে",
"keyboard_shortcuts.mention": "লেখককে উল্লেখ করতে",
"keyboard_shortcuts.muted": "বন্ধ করা ব্যবহারকারীদের তালিকা খুলতে",
"keyboard_shortcuts.my_profile": "নিজের পাতা দেখতে",
"keyboard_shortcuts.my_profile": "আপনার নিজের পাতা দেখতে",
"keyboard_shortcuts.notifications": "প্রজ্ঞাপনের কলাম খুলতে",
"keyboard_shortcuts.pinned": "পিন দেওয়া টুটের তালিকা খুলতে",
"keyboard_shortcuts.profile": "লেখকের পাতা দেখতে",
@ -204,14 +205,14 @@
"keyboard_shortcuts.search": "খোঁজার অংশে ফোকাস করতে",
"keyboard_shortcuts.start": "\"প্রথম শুরুর\" কলাম বের করতে",
"keyboard_shortcuts.toggle_hidden": "CW লেখা দেখতে বা লুকাতে",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
"keyboard_shortcuts.toggle_sensitivity": "ভিডিও/ছবি দেখতে বা বন্ধ করতে",
"keyboard_shortcuts.toot": "নতুন একটা টুট লেখা শুরু করতে",
"keyboard_shortcuts.unfocus": "লেখা বা খোঁজার জায়গায় ফোকাস না করতে",
"keyboard_shortcuts.up": "তালিকার উপরের দিকে যেতে",
"lightbox.close": "বন্ধ",
"lightbox.next": "পরবর্তী",
"lightbox.previous": "পূর্ববর্তী",
"lightbox.view_context": "View context",
"lightbox.view_context": "প্রসঙ্গটি দেখতে",
"lists.account.add": "তালিকাতে যুক্ত করতে",
"lists.account.remove": "তালিকা থেকে বাদ দিতে",
"lists.delete": "তালিকা মুছে ফেলতে",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "তালিকার নতুন শিরোনাম দিতে",
"lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন",
"lists.subheading": "আপনার তালিকা",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "আসছে...",
"media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান",
"missing_indicator.label": "খুঁজে পাওয়া যায়নি",
@ -230,14 +232,14 @@
"navigation_bar.blocks": "বন্ধ করা ব্যবহারকারী",
"navigation_bar.community_timeline": "স্থানীয় সময়রেখা",
"navigation_bar.compose": "নতুন টুট লিখুন",
"navigation_bar.direct": "সরাসরি লেখা",
"navigation_bar.direct": "সরাসরি লেখাগুলি",
"navigation_bar.discover": "ঘুরে দেখুন",
"navigation_bar.domain_blocks": "বন্ধ করা ওয়েবসাইট",
"navigation_bar.edit_profile": "নিজের পাতা সম্পাদনা করুন",
"navigation_bar.edit_profile": "নিজের পাতা সম্পাদনা করতে",
"navigation_bar.favourites": "পছন্দের",
"navigation_bar.filters": "বন্ধ করা শব্দ",
"navigation_bar.follow_requests": "অনুসরণের অনুরোধগুলি",
"navigation_bar.follows_and_followers": "Follows and followers",
"navigation_bar.follows_and_followers": "যাদেরকে অনুসরণ করেন এবং যারা তাকে অনুসরণ করে",
"navigation_bar.info": "এই সার্ভার সম্পর্কে",
"navigation_bar.keyboard_shortcuts": "হটকীগুলি",
"navigation_bar.lists": "তালিকাগুলো",
@ -246,7 +248,7 @@
"navigation_bar.personal": "নিজস্ব",
"navigation_bar.pins": "পিন দেওয়া টুট",
"navigation_bar.preferences": "পছন্দসমূহ",
"navigation_bar.profile_directory": "Profile directory",
"navigation_bar.profile_directory": "নিজস্ব পাতার তালিকা",
"navigation_bar.public_timeline": "যুক্তবিশ্বের সময়রেখা",
"navigation_bar.security": "নিরাপত্তা",
"notification.favourite": "{name} আপনার কার্যক্রম পছন্দ করেছেন",
@ -256,18 +258,18 @@
"notification.reblog": "{name} আপনার কার্যক্রমে সমর্থন দেখিয়েছেন",
"notifications.clear": "প্রজ্ঞাপনগুলো মুছে ফেলতে",
"notifications.clear_confirmation": "আপনি কি নির্চিত প্রজ্ঞাপনগুলো মুছে ফেলতে চান ?",
"notifications.column_settings.alert": "কম্পিউটারে প্রজ্ঞাপন",
"notifications.column_settings.alert": "কম্পিউটারে প্রজ্ঞাপনগুলি",
"notifications.column_settings.favourite": "পছন্দের:",
"notifications.column_settings.filter_bar.advanced": "সব শ্রেণীগুলো দেখতে",
"notifications.column_settings.filter_bar.category": "দ্রুত ছাঁকনি বার",
"notifications.column_settings.filter_bar.show": "দেখতে",
"notifications.column_settings.filter_bar.advanced": "সব শ্রেণীগুলো দেখানো",
"notifications.column_settings.filter_bar.category": "সংক্ষিপ্ত ছাঁকনি অংশ",
"notifications.column_settings.filter_bar.show": "দেখানো",
"notifications.column_settings.follow": "নতুন অনুসরণকারীরা:",
"notifications.column_settings.mention": "প্রজ্ঞাপনগুলো:",
"notifications.column_settings.poll": "নির্বাচনের ফলাফল:",
"notifications.column_settings.push": "পুশ প্রজ্ঞাপন",
"notifications.column_settings.push": "পুশ প্রজ্ঞাপনগুলি",
"notifications.column_settings.reblog": "সমর্থনগুলো:",
"notifications.column_settings.show": "কলামে দেখান",
"notifications.column_settings.sound": "শব্দ বাজাতে",
"notifications.column_settings.show": "কলামে দেখান",
"notifications.column_settings.sound": "শব্দ বাজানো",
"notifications.filter.all": "সব",
"notifications.filter.boosts": "সমর্থনগুলো",
"notifications.filter.favourites": "পছন্দের গুলো",
@ -276,7 +278,7 @@
"notifications.filter.polls": "নির্বাচনের ফলাফল",
"notifications.group": "{count} প্রজ্ঞাপন",
"poll.closed": "বন্ধ",
"poll.refresh": "আবার সতেজ করতে",
"poll.refresh": "বদলেছে কিনা দেখতে",
"poll.total_votes": "{count, plural, one {# ভোট} other {# ভোট}}",
"poll.vote": "ভোট",
"poll_button.add_poll": "একটা নির্বাচন যোগ করতে",
@ -314,6 +316,7 @@
"search_results.accounts": "মানুষ",
"search_results.hashtags": "হ্যাশট্যাগগুলি",
"search_results.statuses": "টুট",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {ফলাফল} other {ফলাফল}}",
"status.admin_account": "@{name} র জন্য পরিচালনার ইন্টারফেসে ঢুকুন",
"status.admin_status": "যায় লেখাটি পরিচালনার ইন্টারফেসে খুলুন",
@ -323,7 +326,7 @@
"status.copy": "লেখাটির লিংক কপি করতে",
"status.delete": "মুছে ফেলতে",
"status.detailed_status": "বিস্তারিত কথোপকথনের হিসেবে দেখতে",
"status.direct": "@{name} কে সরাসরি পাঠান",
"status.direct": "@{name} কে সরাসরি লেখা পাঠাতে",
"status.embed": "এমবেড করতে",
"status.favourite": "পছন্দের করতে",
"status.filtered": "ছাঁকনিদিত",
@ -344,7 +347,7 @@
"status.redraft": "মুছে আবার নতুন করে লিখতে",
"status.reply": "মতামত জানাতে",
"status.replyAll": "লেখাযুক্ত সবার কাছে মতামত জানাতে",
"status.report": "@{name}কে রিপোর্ট করতে",
"status.report": "@{name} কে রিপোর্ট করতে",
"status.sensitive_warning": "সংবেদনশীল কিছু",
"status.share": "অন্যদের জানান",
"status.show_less": "কম দেখতে",
@ -354,7 +357,7 @@
"status.show_thread": "আলোচনা দেখতে",
"status.unmute_conversation": "আলোচনার প্রজ্ঞাপন চালু করতে",
"status.unpin": "নিজের পাতা থেকে পিন করে রাখাটির পিন খুলতে",
"suggestions.dismiss": "সাহায্যের জন্য পরামর্শগুলো সরাতে",
"suggestions.dismiss": "সাহায্যের পরামর্শগুলো সরাতে",
"suggestions.header": "আপনি হয়তোবা এগুলোতে আগ্রহী হতে পারেন…",
"tabs_bar.federated_timeline": "যুক্তবিশ্ব",
"tabs_bar.home": "বাড়ি",
@ -369,7 +372,7 @@
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} কথা বলছে",
"ui.beforeunload": "যে পর্যন্ত এটা লেখা হয়েছে, মাস্টাডন থেকে চলে গেলে এটা মুছে যাবে।",
"upload_area.title": "টেনে এখানে ছেড়ে দিলে এখানে যুক্ত করা যাবে",
"upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের: JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "যা যুক্ত করতে চাচ্ছেন সেটি বেশি বড়, এখানকার সর্বাধিকের মেমোরির উপরে চলে গেছে।",
"upload_error.poll": "নির্বাচনক্ষেত্রে কোনো ফাইল যুক্ত করা যাবেনা।",
"upload_form.description": "যারা দেখতে পায়না তাদের জন্য এটা বর্ণনা করতে",

@ -156,6 +156,7 @@
"home.column_settings.basic": "Bàsic",
"home.column_settings.show_reblogs": "Mostrar impulsos",
"home.column_settings.show_replies": "Mostrar respostes",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# dia} other {# dies}}",
"intervals.full.hours": "{number, plural, one {# hora} other {# hores}}",
"intervals.full.minutes": "{number, plural, one {# minut} other {# minuts}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Nova llista",
"lists.search": "Cercar entre les persones que segueixes",
"lists.subheading": "Les teves llistes",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Carregant...",
"media_gallery.toggle_visible": "Alternar visibilitat",
"missing_indicator.label": "No trobat",
@ -314,6 +316,7 @@
"search_results.accounts": "Gent",
"search_results.hashtags": "Etiquetes",
"search_results.statuses": "Toots",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
"status.admin_account": "Obre l'interfície de moderació per a @{name}",
"status.admin_status": "Obre aquest toot a la interfície de moderació",

@ -156,6 +156,7 @@
"home.column_settings.basic": "Bàsichi",
"home.column_settings.show_reblogs": "Vede e spartere",
"home.column_settings.show_replies": "Vede e risposte",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# ghjornu} other {# ghjorni}}",
"intervals.full.hours": "{number, plural, one {# ora} other {# ore}}",
"intervals.full.minutes": "{number, plural, one {# minuta} other {# minute}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Titulu di a lista",
"lists.search": "Circà indè i vostr'abbunamenti",
"lists.subheading": "E vo liste",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Caricamentu...",
"media_gallery.toggle_visible": "Cambià a visibilità",
"missing_indicator.label": "Micca trovu",
@ -314,6 +316,7 @@
"search_results.accounts": "Ghjente",
"search_results.hashtags": "Hashtag",
"search_results.statuses": "Statuti",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {risultatu} other {risultati}}",
"status.admin_account": "Apre l'interfaccia di muderazione per @{name}",
"status.admin_status": "Apre stu statutu in l'interfaccia di muderazione",

@ -156,6 +156,7 @@
"home.column_settings.basic": "Základní",
"home.column_settings.show_reblogs": "Zobrazit boosty",
"home.column_settings.show_replies": "Zobrazit odpovědi",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# den} few {# dny} many {# dne} other {# dní}}",
"intervals.full.hours": "{number, plural, one {# hodina} few {# hodiny} many {# hodiny} other {# hodin}}",
"intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minuty} other {# minut}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Název nového seznamu",
"lists.search": "Hledejte mezi lidmi, které sledujete",
"lists.subheading": "Vaše seznamy",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Načítám…",
"media_gallery.toggle_visible": "Přepínat viditelnost",
"missing_indicator.label": "Nenalezeno",
@ -314,6 +316,7 @@
"search_results.accounts": "Lidé",
"search_results.hashtags": "Hashtagy",
"search_results.statuses": "Tooty",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {výsledek} few {výsledky} many {výsledku} other {výsledků}}",
"status.admin_account": "Otevřít moderátorské rozhraní pro uživatele @{name}",
"status.admin_status": "Otevřít tento toot v moderátorském rozhraní",

@ -156,6 +156,7 @@
"home.column_settings.basic": "Syml",
"home.column_settings.show_reblogs": "Dangos bŵstiau",
"home.column_settings.show_replies": "Dangos ymatebion",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# ddydd} other {# o ddyddiau}}",
"intervals.full.hours": "{number, plural, one {# awr} other {# o oriau}}",
"intervals.full.minutes": "{number, plural, one {# funud} other {# o funudau}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Teitl rhestr newydd",
"lists.search": "Chwilio ymysg pobl yr ydych yn ei ddilyn",
"lists.subheading": "Eich rhestrau",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Llwytho...",
"media_gallery.toggle_visible": "Toglo gwelededd",
"missing_indicator.label": "Heb ei ganfod",
@ -314,6 +316,7 @@
"search_results.accounts": "Pobl",
"search_results.hashtags": "Hanshnodau",
"search_results.statuses": "Tŵtiau",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"status.admin_account": "Agor rhyngwyneb goruwchwylio ar gyfer @{name}",
"status.admin_status": "Agor y tŵt yn y rhyngwyneb goruwchwylio",

@ -156,6 +156,7 @@
"home.column_settings.basic": "Grundlæggende",
"home.column_settings.show_reblogs": "Vis fremhævelser",
"home.column_settings.show_replies": "Vis svar",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Ny liste titel",
"lists.search": "Søg iblandt folk du følger",
"lists.subheading": "Dine lister",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Indlæser...",
"media_gallery.toggle_visible": "Ændre synlighed",
"missing_indicator.label": "Ikke fundet",
@ -314,6 +316,7 @@
"search_results.accounts": "Folk",
"search_results.hashtags": "Emnetags",
"search_results.statuses": "Trut",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, et {result} andre {results}}",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface",

@ -156,6 +156,7 @@
"home.column_settings.basic": "Einfach",
"home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
"home.column_settings.show_replies": "Antworten anzeigen",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# Tag} other {# Tage}}",
"intervals.full.hours": "{number, plural, one {# Stunde} other {# Stunden}}",
"intervals.full.minutes": "{number, plural, one {# Minute} other {# Minuten}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Neuer Titel der Liste",
"lists.search": "Suche nach Leuten denen du folgst",
"lists.subheading": "Deine Listen",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Wird geladen …",
"media_gallery.toggle_visible": "Sichtbarkeit umschalten",
"missing_indicator.label": "Nicht gefunden",
@ -314,6 +316,7 @@
"search_results.accounts": "Personen",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Beiträge",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}",
"status.admin_account": "Öffne Moderationsoberfläche für @{name}",
"status.admin_status": "Öffne Beitrag in der Moderationsoberfläche",

@ -158,6 +158,15 @@
],
"path": "app/javascript/mastodon/components/load_more.json"
},
{
"descriptors": [
{
"defaultMessage": "{count, plural, one {# new item} other {# new items}}",
"id": "load_pending"
}
],
"path": "app/javascript/mastodon/components/load_pending.json"
},
{
"descriptors": [
{
@ -735,7 +744,7 @@
{
"descriptors": [
{
"defaultMessage": "Media Only",
"defaultMessage": "Media only",
"id": "community.column_settings.media_only"
}
],
@ -1004,6 +1013,10 @@
"defaultMessage": "Toots",
"id": "search_results.statuses"
},
{
"defaultMessage": "Searching toots by their content is not enabled on this Mastodon server.",
"id": "search_results.statuses_fts_disabled"
},
{
"defaultMessage": "Hashtags",
"id": "search_results.hashtags"
@ -1412,10 +1425,6 @@
},
{
"descriptors": [
{
"defaultMessage": "Basic",
"id": "home.column_settings.basic"
},
{
"defaultMessage": "Show boosts",
"id": "home.column_settings.show_reblogs"
@ -1797,6 +1806,14 @@
"defaultMessage": "Push notifications",
"id": "notifications.column_settings.push"
},
{
"defaultMessage": "Basic",
"id": "home.column_settings.basic"
},
{
"defaultMessage": "Update in real-time",
"id": "home.column_settings.update_live"
},
{
"defaultMessage": "Quick filter bar",
"id": "notifications.column_settings.filter_bar.category"

@ -156,6 +156,7 @@
"home.column_settings.basic": "Βασικά",
"home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων",
"home.column_settings.show_replies": "Εμφάνιση απαντήσεων",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# μέρα} other {# μέρες}}",
"intervals.full.hours": "{number, plural, one {# ώρα} other {# ώρες}}",
"intervals.full.minutes": "{number, plural, one {# λεπτό} other {# λεπτά}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Τίτλος νέας λίστα",
"lists.search": "Αναζήτησε μεταξύ των ανθρώπων που ακουλουθείς",
"lists.subheading": "Οι λίστες σου",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Φορτώνει...",
"media_gallery.toggle_visible": "Εναλλαγή ορατότητας",
"missing_indicator.label": "Δε βρέθηκε",
@ -314,6 +316,7 @@
"search_results.accounts": "Άνθρωποι",
"search_results.hashtags": "Ταμπέλες",
"search_results.statuses": "Τουτ",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, zero {αποτελέσματα} one {αποτέλεσμα} other {αποτελέσματα}}",
"status.admin_account": "Άνοιγμα λειτουργίας διαμεσολάβησης για τον/την @{name}",
"status.admin_status": "Άνοιγμα αυτής της δημοσίευσης στη λειτουργία διαμεσολάβησης",

@ -160,6 +160,7 @@
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -225,6 +226,7 @@
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
@ -319,6 +321,7 @@
"search_results.accounts": "People",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface",

@ -156,6 +156,7 @@
"home.column_settings.basic": "Bazaj agordoj",
"home.column_settings.show_reblogs": "Montri diskonigojn",
"home.column_settings.show_replies": "Montri respondojn",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# tago} other {# tagoj}}",
"intervals.full.hours": "{number, plural, one {# horo} other {# horoj}}",
"intervals.full.minutes": "{number, plural, one {# minuto} other {# minutoj}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Titolo de la nova listo",
"lists.search": "Serĉi inter la homoj, kiujn vi sekvas",
"lists.subheading": "Viaj listoj",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Ŝargado…",
"media_gallery.toggle_visible": "Baskuligi videblecon",
"missing_indicator.label": "Ne trovita",
@ -314,6 +316,7 @@
"search_results.accounts": "Homoj",
"search_results.hashtags": "Kradvortoj",
"search_results.statuses": "Mesaĝoj",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {rezulto} other {rezultoj}}",
"status.admin_account": "Malfermi la kontrolan interfacon por @{name}",
"status.admin_status": "Malfermi ĉi tiun mesaĝon en la kontrola interfaco",

@ -98,7 +98,7 @@
"confirmations.redraft.confirm": "Borrar y volver a borrador",
"confirmations.redraft.message": "Estás seguro de que quieres borrar este estado y volverlo a borrador? Perderás todas las respuestas, impulsos y favoritos asociados a él, y las respuestas a la publicación original quedarán huérfanos.",
"confirmations.reply.confirm": "Responder",
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.reply.message": "Responder sobrescribirá el mensaje que estás escribiendo. ¿Estás seguro de que deseas continuar?",
"confirmations.unfollow.confirm": "Dejar de seguir",
"confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?",
"embed.instructions": "Añade este toot a tu sitio web con el siguiente código.",
@ -149,33 +149,34 @@
"hashtag.column_header.tag_mode.none": "sin {additional}",
"hashtag.column_settings.select.no_options_message": "No se encontraron sugerencias",
"hashtag.column_settings.select.placeholder": "Introduzca hashtags…",
"hashtag.column_settings.tag_mode.all": "All of these",
"hashtag.column_settings.tag_mode.all": "Cualquiera de estos",
"hashtag.column_settings.tag_mode.any": "Cualquiera de estos",
"hashtag.column_settings.tag_mode.none": "Ninguno de estos",
"hashtag.column_settings.tag_toggle": "Include additional tags in this column",
"home.column_settings.basic": "Básico",
"home.column_settings.show_reblogs": "Mostrar retoots",
"home.column_settings.show_replies": "Mostrar respuestas",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# día} other {# días}}",
"intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
"intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
"introduction.federation.action": "Siguiente",
"introduction.federation.federated.headline": "Federado",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.federated.text": "Los mensajes públicos de otros servidores del fediverso aparecerán en la cronología federada.",
"introduction.federation.home.headline": "Inicio",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.home.text": "Los posts de personas que sigues aparecerán en tu cronología. ¡Puedes seguir a cualquiera en cualquier servidor!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.federation.local.text": "Los posts públicos de personas en el mismo servidor que aparecerán en la cronología local.",
"introduction.interactions.action": "¡Terminar tutorial!",
"introduction.interactions.favourite.headline": "Favorito",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.favourite.text": "Puedes guardar un toot para más tarde, y hacer saber al autor que te gustó, dándole a favorito.",
"introduction.interactions.reblog.headline": "Retootear",
"introduction.interactions.reblog.text": "Puedes compartir los toots de otras personas con tus seguidores retooteando los mismos.",
"introduction.interactions.reply.headline": "Responder",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.interactions.reply.text": "Puedes responder a tus propios toots y los de otras personas, que se encadenarán juntos en una conversación.",
"introduction.welcome.action": "¡Vamos!",
"introduction.welcome.headline": "Primeros pasos",
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
"introduction.welcome.text": "¡Bienvenido al fediverso! En unos momentos, podrás transmitir mensajes y hablar con tus amigos a través de una amplia variedad de servidores. Pero este servidor, {domain}, es especial, alberga tu perfil, así que recuerda su nombre.",
"keyboard_shortcuts.back": "volver atrás",
"keyboard_shortcuts.blocked": "abrir una lista de usuarios bloqueados",
"keyboard_shortcuts.boost": "retootear",
@ -184,7 +185,7 @@
"keyboard_shortcuts.description": "Descripción",
"keyboard_shortcuts.direct": "abrir la columna de mensajes directos",
"keyboard_shortcuts.down": "mover hacia abajo en la lista",
"keyboard_shortcuts.enter": "to open status",
"keyboard_shortcuts.enter": "abrir estado",
"keyboard_shortcuts.favourite": "añadir a favoritos",
"keyboard_shortcuts.favourites": "abrir la lista de favoritos",
"keyboard_shortcuts.federated": "abrir el timeline federado",
@ -204,7 +205,7 @@
"keyboard_shortcuts.search": "para poner el foco en la búsqueda",
"keyboard_shortcuts.start": "abrir la columna \"comenzar\"",
"keyboard_shortcuts.toggle_hidden": "mostrar/ocultar texto tras aviso de contenido (CW)",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
"keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar medios",
"keyboard_shortcuts.toot": "para comenzar un nuevo toot",
"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",
@ -216,11 +217,12 @@
"lists.account.remove": "Quitar de lista",
"lists.delete": "Borrar lista",
"lists.edit": "Editar lista",
"lists.edit.submit": "Change title",
"lists.edit.submit": "Cambiar título",
"lists.new.create": "Añadir lista",
"lists.new.title_placeholder": "Título de la nueva lista",
"lists.search": "Buscar entre la gente a la que sigues",
"lists.subheading": "Tus listas",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Cargando…",
"media_gallery.toggle_visible": "Cambiar visibilidad",
"missing_indicator.label": "No encontrado",
@ -237,7 +239,7 @@
"navigation_bar.favourites": "Favoritos",
"navigation_bar.filters": "Palabras silenciadas",
"navigation_bar.follow_requests": "Solicitudes para seguirte",
"navigation_bar.follows_and_followers": "Follows and followers",
"navigation_bar.follows_and_followers": "Siguiendo y seguidores",
"navigation_bar.info": "Información adicional",
"navigation_bar.keyboard_shortcuts": "Atajos",
"navigation_bar.lists": "Listas",
@ -246,41 +248,41 @@
"navigation_bar.personal": "Personal",
"navigation_bar.pins": "Toots fijados",
"navigation_bar.preferences": "Preferencias",
"navigation_bar.profile_directory": "Profile directory",
"navigation_bar.profile_directory": "Directorio de perfiles",
"navigation_bar.public_timeline": "Historia federada",
"navigation_bar.security": "Seguridad",
"notification.favourite": "{name} marcó tu estado como favorito",
"notification.follow": "{name} te empezó a seguir",
"notification.mention": "{name} te ha mencionado",
"notification.poll": "A poll you have voted in has ended",
"notification.poll": "Una encuesta en la que has votado ha terminado",
"notification.reblog": "{name} ha retooteado tu estado",
"notifications.clear": "Limpiar notificaciones",
"notifications.clear_confirmation": "¿Seguro que quieres limpiar permanentemente todas tus notificaciones?",
"notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
"notifications.column_settings.filter_bar.show": "Mostrar",
"notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.poll": "Poll results:",
"notifications.column_settings.poll": "Resultados de la votación:",
"notifications.column_settings.push": "Notificaciones push",
"notifications.column_settings.reblog": "Retoots:",
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.filter.polls": "Poll results",
"notifications.filter.all": "Todos",
"notifications.filter.boosts": "Retoots",
"notifications.filter.favourites": "Favoritos",
"notifications.filter.follows": "Seguidores",
"notifications.filter.mentions": "Menciones",
"notifications.filter.polls": "Resultados de la votación",
"notifications.group": "{count} notificaciones",
"poll.closed": "Closed",
"poll.refresh": "Refresh",
"poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
"poll.vote": "Vote",
"poll_button.add_poll": "Add a poll",
"poll_button.remove_poll": "Remove poll",
"poll.closed": "Cerrada",
"poll.refresh": "Actualizar",
"poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
"poll.vote": "Votar",
"poll_button.add_poll": "Añadir una encuesta",
"poll_button.remove_poll": "Eliminar encuesta",
"privacy.change": "Ajustar privacidad",
"privacy.direct.long": "Sólo mostrar a los usuarios mencionados",
"privacy.direct.short": "Directo",
@ -289,7 +291,7 @@
"privacy.public.long": "Mostrar en la historia federada",
"privacy.public.short": "Público",
"privacy.unlisted.long": "No mostrar en la historia federada",
"privacy.unlisted.short": "Sin federar",
"privacy.unlisted.short": "No listado",
"regeneration_indicator.label": "Cargando…",
"regeneration_indicator.sublabel": "¡Tu historia de inicio se está preparando!",
"relative_time.days": "{number}d",
@ -308,19 +310,20 @@
"search_popout.search_format": "Formato de búsqueda avanzada",
"search_popout.tips.full_text": "Búsquedas de texto recuperan posts que has escrito, marcado como favoritos, retooteado o en los que has sido mencionado, así como usuarios, nombres y hashtags.",
"search_popout.tips.hashtag": "etiqueta",
"search_popout.tips.status": "status",
"search_popout.tips.status": "estado",
"search_popout.tips.text": "El texto simple devuelve correspondencias de nombre, usuario y hashtag",
"search_popout.tips.user": "usuario",
"search_results.accounts": "Gente",
"search_results.hashtags": "Etiquetas",
"search_results.statuses": "Toots",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface",
"status.block": "Block @{name}",
"status.admin_account": "Abrir interfaz de moderación para @{name}",
"status.admin_status": "Abrir este estado en la interfaz de moderación",
"status.block": "Bloquear a @{name}",
"status.cancel_reblog_private": "Des-impulsar",
"status.cannot_reblog": "Este toot no puede retootearse",
"status.copy": "Copy link to status",
"status.copy": "Copiar enlace al estado",
"status.delete": "Borrar",
"status.detailed_status": "Vista de conversación detallada",
"status.direct": "Mensaje directo a @{name}",
@ -336,7 +339,7 @@
"status.open": "Expandir estado",
"status.pin": "Fijar",
"status.pinned": "Toot fijado",
"status.read_more": "Read more",
"status.read_more": "Leer más",
"status.reblog": "Retootear",
"status.reblog_private": "Implusar a la audiencia original",
"status.reblogged_by": "Retooteado por {name}",
@ -351,27 +354,27 @@
"status.show_less_all": "Mostrar menos para todo",
"status.show_more": "Mostrar más",
"status.show_more_all": "Mostrar más para todo",
"status.show_thread": "Show thread",
"status.show_thread": "Ver hilo",
"status.unmute_conversation": "Dejar de silenciar conversación",
"status.unpin": "Dejar de fijar",
"suggestions.dismiss": "Dismiss suggestion",
"suggestions.header": "You might be interested in…",
"suggestions.dismiss": "Descartar sugerencia",
"suggestions.header": "Es posible que te interese…",
"tabs_bar.federated_timeline": "Federado",
"tabs_bar.home": "Inicio",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notificaciones",
"tabs_bar.search": "Buscar",
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
"time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
"time_remaining.days": "{number, plural, one {# día restante} other {# días restantes}}",
"time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}",
"time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}",
"time_remaining.moments": "Momentos restantes",
"time_remaining.seconds": "{number, plural, one {# segundo restante} other {# segundos restantes}}",
"trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {personas}} hablando",
"ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.",
"upload_area.title": "Arrastra y suelta para subir",
"upload_button.label": "Subir multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "File upload limit exceeded.",
"upload_error.poll": "File upload not allowed with polls.",
"upload_error.limit": "Límite de subida de archivos excedido.",
"upload_error.poll": "Subida de archivos no permitida con encuestas.",
"upload_form.description": "Describir para los usuarios con dificultad visual",
"upload_form.focus": "Recortar",
"upload_form.undo": "Borrar",

@ -156,6 +156,7 @@
"home.column_settings.basic": "Oinarrizkoa",
"home.column_settings.show_reblogs": "Erakutsi bultzadak",
"home.column_settings.show_replies": "Erakutsi erantzunak",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {egun #} other {# egun}}",
"intervals.full.hours": "{number, plural, one {ordu #} other {# ordu}}",
"intervals.full.minutes": "{number, plural, one {minutu #} other {# minutu}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Zerrenda berriaren izena",
"lists.search": "Bilatu jarraitzen dituzun pertsonen artean",
"lists.subheading": "Zure zerrendak",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Kargatzen...",
"media_gallery.toggle_visible": "Txandakatu ikusgaitasuna",
"missing_indicator.label": "Ez aurkitua",
@ -314,6 +316,7 @@
"search_results.accounts": "Jendea",
"search_results.hashtags": "Traolak",
"search_results.statuses": "Toot-ak",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {emaitza} other {emaitzak}}",
"status.admin_account": "Ireki @{name} erabiltzailearen moderazio interfazea",
"status.admin_status": "Ireki mezu hau moderazio interfazean",

@ -156,6 +156,7 @@
"home.column_settings.basic": "اصلی",
"home.column_settings.show_reblogs": "نمایش بازبوق‌ها",
"home.column_settings.show_replies": "نمایش پاسخ‌ها",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# روز} other {# روز}}",
"intervals.full.hours": "{number, plural, one {# ساعت} other {# ساعت}}",
"intervals.full.minutes": "{number, plural, one {# دقیقه} other {# دقیقه}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "نام فهرست تازه",
"lists.search": "بین کسانی که پی می‌گیرید بگردید",
"lists.subheading": "فهرست‌های شما",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "بارگیری...",
"media_gallery.toggle_visible": "تغییر پیدایی",
"missing_indicator.label": "پیدا نشد",
@ -314,6 +316,7 @@
"search_results.accounts": "افراد",
"search_results.hashtags": "هشتگ‌ها",
"search_results.statuses": "بوق‌ها",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}",
"status.admin_account": "محیط مدیریت مربوط به @{name} را باز کن",
"status.admin_status": "این نوشته را در محیط مدیریت باز کن",

@ -156,6 +156,7 @@
"home.column_settings.basic": "Perusasetukset",
"home.column_settings.show_reblogs": "Näytä buustaukset",
"home.column_settings.show_replies": "Näytä vastaukset",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "Päivä päiviä",
"intervals.full.hours": "Tunti tunteja",
"intervals.full.minutes": "Minuuti minuuteja",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Uuden listan nimi",
"lists.search": "Etsi seuraamistasi henkilöistä",
"lists.subheading": "Omat listat",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Ladataan...",
"media_gallery.toggle_visible": "Säädä näkyvyyttä",
"missing_indicator.label": "Ei löytynyt",
@ -314,6 +316,7 @@
"search_results.accounts": "Ihmiset",
"search_results.hashtags": "Hashtagit",
"search_results.statuses": "Tuuttaukset",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface",

@ -156,6 +156,7 @@
"home.column_settings.basic": "Basique",
"home.column_settings.show_reblogs": "Afficher les partages",
"home.column_settings.show_replies": "Afficher les réponses",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# jour} other {# jours}}",
"intervals.full.hours": "{number, plural, one {# heure} other {# heures}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Titre de la nouvelle liste",
"lists.search": "Rechercher parmi les gens que vous suivez",
"lists.subheading": "Vos listes",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Chargement…",
"media_gallery.toggle_visible": "Modifier la visibilité",
"missing_indicator.label": "Non trouvé",
@ -314,6 +316,7 @@
"search_results.accounts": "Comptes",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Pouets",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
"status.admin_account": "Ouvrir l'interface de modération pour @{name}",
"status.admin_status": "Ouvrir ce statut dans l'interface de modération",

@ -156,6 +156,7 @@
"home.column_settings.basic": "Básico",
"home.column_settings.show_reblogs": "Mostrar repeticións",
"home.column_settings.show_replies": "Mostrar respostas",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural,one {# día} other {# días}}",
"intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
"intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Novo título da lista",
"lists.search": "Procurar entre a xente que segues",
"lists.subheading": "As túas listas",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Cargando...",
"media_gallery.toggle_visible": "Ocultar",
"missing_indicator.label": "Non atopado",
@ -314,6 +316,7 @@
"search_results.accounts": "Xente",
"search_results.hashtags": "Etiquetas",
"search_results.statuses": "Toots",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count,plural,one {result} outros {results}}",
"status.admin_account": "Abrir interface de moderación para @{name}",
"status.admin_status": "Abrir este estado na interface de moderación",
@ -369,7 +372,7 @@
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} outras {people}} conversando",
"ui.beforeunload": "O borrador perderase se sae de Mastodon.",
"upload_area.title": "Arrastre e solte para subir",
"upload_button.label": "Engadir medios (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_button.label": "Engadir medios ({formats})",
"upload_error.limit": "Excedeu o límite de subida de ficheiros.",
"upload_error.poll": "Non se poden subir ficheiros nas sondaxes.",
"upload_form.description": "Describa para deficientes visuais",

@ -64,7 +64,7 @@
"column_header.show_settings": "הצגת העדפות",
"column_header.unpin": "שחרור קיבוע",
"column_subheading.settings": "אפשרויות",
"community.column_settings.media_only": "Media Only",
"community.column_settings.media_only": "Media only",
"compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
"compose_form.direct_message_warning_learn_more": "Learn more",
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
@ -156,6 +156,7 @@
"home.column_settings.basic": "למתחילים",
"home.column_settings.show_reblogs": "הצגת הדהודים",
"home.column_settings.show_replies": "הצגת תגובות",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "טוען...",
"media_gallery.toggle_visible": "נראה\\בלתי נראה",
"missing_indicator.label": "לא נמצא",
@ -314,6 +316,7 @@
"search_results.accounts": "People",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface",

@ -156,6 +156,7 @@
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
@ -314,6 +316,7 @@
"search_results.accounts": "People",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface",

@ -64,7 +64,7 @@
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.settings": "Postavke",
"community.column_settings.media_only": "Media Only",
"community.column_settings.media_only": "Media only",
"compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
"compose_form.direct_message_warning_learn_more": "Learn more",
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
@ -156,6 +156,7 @@
"home.column_settings.basic": "Osnovno",
"home.column_settings.show_reblogs": "Pokaži boostove",
"home.column_settings.show_replies": "Pokaži odgovore",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Učitavam...",
"media_gallery.toggle_visible": "Preklopi vidljivost",
"missing_indicator.label": "Nije nađen",
@ -314,6 +316,7 @@
"search_results.accounts": "People",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface",

@ -156,6 +156,7 @@
"home.column_settings.basic": "Alapértelmezések",
"home.column_settings.show_reblogs": "Megtolások mutatása",
"home.column_settings.show_replies": "Válaszok mutatása",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# nap} other {# nap}}",
"intervals.full.hours": "{number, plural, one {# óra} other {# óra}}",
"intervals.full.minutes": "{number, plural, one {# perc} other {# perc}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Új lista címe",
"lists.search": "Keresés a követett személyek között",
"lists.subheading": "Listáid",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Betöltés...",
"media_gallery.toggle_visible": "Láthatóság állítása",
"missing_indicator.label": "Nincs találat",
@ -314,6 +316,7 @@
"search_results.accounts": "Emberek",
"search_results.hashtags": "Hashtagek",
"search_results.statuses": "Tülkök",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {találat} other {találat}}",
"status.admin_account": "Moderáció megnyitása @{name} felhasználóhoz",
"status.admin_status": "Tülk megnyitása moderációra",

@ -64,7 +64,7 @@
"column_header.show_settings": "Ցուցադրել կարգավորումները",
"column_header.unpin": "Հանել",
"column_subheading.settings": "Կարգավորումներ",
"community.column_settings.media_only": "Media Only",
"community.column_settings.media_only": "Media only",
"compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
"compose_form.direct_message_warning_learn_more": "Learn more",
"compose_form.hashtag_warning": "Այս թութը չի հաշվառվի որեւէ պիտակի տակ, քանզի այն ծածուկ է։ Միայն հրապարակային թթերը հնարավոր է որոնել պիտակներով։",
@ -156,6 +156,7 @@
"home.column_settings.basic": "Հիմնական",
"home.column_settings.show_reblogs": "Ցուցադրել տարածածները",
"home.column_settings.show_replies": "Ցուցադրել պատասխանները",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Նոր ցանկի վերնագիր",
"lists.search": "Փնտրել քո հետեւած մարդկանց մեջ",
"lists.subheading": "Քո ցանկերը",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Բեռնվում է…",
"media_gallery.toggle_visible": "Ցուցադրել/թաքցնել",
"missing_indicator.label": "Չգտնվեց",
@ -314,6 +316,7 @@
"search_results.accounts": "People",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {արդյունք} other {արդյունք}}",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface",

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

Loading…
Cancel
Save