Merge pull request #1158 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
f1597e1ab9
|
@ -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
Gemfile
5
Gemfile
|
@ -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'
|
||||
|
|
21
Gemfile.lock
21
Gemfile.lock
|
@ -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,14 +4,18 @@ 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
|
||||
skip_before_action :check_user_permissions, only: [:more, :terms]
|
||||
|
||||
def show; end
|
||||
|
||||
def more
|
||||
flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
|
||||
end
|
||||
|
||||
def more; end
|
||||
|
||||
def terms; end
|
||||
|
||||
private
|
||||
|
@ -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
|
||||
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
|
||||
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
|
||||
|
||||
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'
|
||||
response.headers['Referrer-Policy'] = 'origin' unless @status.distributable?
|
||||
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)
|
||||
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
|
||||
|
@ -28,11 +28,8 @@ module WellKnown
|
|||
|
||||
def username_from_resource
|
||||
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)
|
||||
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.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')
|
||||
request
|
||||
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,10 +83,15 @@ export default class ScrollableList extends PureComponent {
|
|||
scrollToTopOnMouseIdle = false;
|
||||
|
||||
setScrollTop = newScrollTop => {
|
||||
if (this.node.scrollTop !== newScrollTop) {
|
||||
if (this.getScrollTop() !== newScrollTop) {
|
||||
this.lastScrollWasSynthetic = true;
|
||||
|
||||
if (this.props.bindToDocument) {
|
||||
document.scrollingElement.scrollTop = newScrollTop;
|
||||
} else {
|
||||
this.node.scrollTop = newScrollTop;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
clearMouseIdleTimer = () => {
|
||||
|
@ -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,14 +217,24 @@ export default class ScrollableList extends PureComponent {
|
|||
}
|
||||
|
||||
attachScrollListener () {
|
||||
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 () {
|
||||
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) {
|
||||
const { children } = 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": "ছাঁকনিদিত",
|
||||
|
@ -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
Reference in New Issue