Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- Gemfile.lock
- app/controllers/accounts_controller.rb
- app/controllers/admin/dashboard_controller.rb
- app/controllers/follower_accounts_controller.rb
- app/controllers/following_accounts_controller.rb
- app/controllers/remote_follow_controller.rb
- app/controllers/stream_entries_controller.rb
- app/controllers/tags_controller.rb
- app/javascript/packs/public.js
- app/lib/sanitize_config.rb
- app/models/account.rb
- app/models/form/admin_settings.rb
- app/models/media_attachment.rb
- app/models/stream_entry.rb
- app/models/user.rb
- app/serializers/initial_state_serializer.rb
- app/services/batched_remove_status_service.rb
- app/services/post_status_service.rb
- app/services/process_mentions_service.rb
- app/services/reblog_service.rb
- app/services/remove_status_service.rb
- app/views/admin/settings/edit.html.haml
- config/locales/simple_form.pl.yml
- config/settings.yml
- docker-compose.yml
master
Thibaut Girka 5 years ago
commit 249991c498
  1. 1
      Dockerfile
  2. 5
      Gemfile
  3. 21
      Gemfile.lock
  4. 22
      app/controllers/about_controller.rb
  5. 31
      app/controllers/accounts_controller.rb
  6. 9
      app/controllers/activitypub/base_controller.rb
  7. 19
      app/controllers/activitypub/collections_controller.rb
  8. 33
      app/controllers/activitypub/inboxes_controller.rb
  9. 12
      app/controllers/activitypub/outboxes_controller.rb
  10. 70
      app/controllers/activitypub/replies_controller.rb
  11. 16
      app/controllers/admin/accounts_controller.rb
  12. 1
      app/controllers/admin/dashboard_controller.rb
  13. 17
      app/controllers/api/proofs_controller.rb
  14. 73
      app/controllers/api/push_controller.rb
  15. 37
      app/controllers/api/salmon_controller.rb
  16. 51
      app/controllers/api/subscriptions_controller.rb
  17. 31
      app/controllers/api/v1/follows_controller.rb
  18. 16
      app/controllers/application_controller.rb
  19. 36
      app/controllers/concerns/account_controller_concern.rb
  20. 33
      app/controllers/concerns/account_owned_concern.rb
  21. 19
      app/controllers/concerns/signature_verification.rb
  22. 87
      app/controllers/concerns/status_controller_concern.rb
  23. 1
      app/controllers/custom_css_controller.rb
  24. 5
      app/controllers/emojis_controller.rb
  25. 14
      app/controllers/follower_accounts_controller.rb
  26. 14
      app/controllers/following_accounts_controller.rb
  27. 4
      app/controllers/home_controller.rb
  28. 20
      app/controllers/instance_actors_controller.rb
  29. 1
      app/controllers/intents_controller.rb
  30. 1
      app/controllers/manifests_controller.rb
  31. 1
      app/controllers/media_controller.rb
  32. 14
      app/controllers/public_timelines_controller.rb
  33. 12
      app/controllers/remote_follow_controller.rb
  34. 39
      app/controllers/remote_unfollows_controller.rb
  35. 179
      app/controllers/statuses_controller.rb
  36. 66
      app/controllers/stream_entries_controller.rb
  37. 21
      app/controllers/tags_controller.rb
  38. 2
      app/controllers/well_known/host_meta_controller.rb
  39. 9
      app/controllers/well_known/webfinger_controller.rb
  40. 2
      app/helpers/admin/action_logs_helper.rb
  41. 17
      app/helpers/domain_control_helper.rb
  42. 2
      app/helpers/home_helper.rb
  43. 52
      app/helpers/jsonld_helper.rb
  44. 4
      app/helpers/statuses_helper.rb
  45. 2
      app/javascript/core/public.js
  46. 4
      app/javascript/mastodon/components/dropdown_menu.js
  47. 58
      app/javascript/mastodon/components/scrollable_list.js
  48. 7
      app/javascript/mastodon/containers/media_container.js
  49. 4
      app/javascript/mastodon/features/account_timeline/index.js
  50. 4
      app/javascript/mastodon/features/blocks/index.js
  51. 1
      app/javascript/mastodon/features/community_timeline/index.js
  52. 3
      app/javascript/mastodon/features/compose/components/action_bar.js
  53. 4
      app/javascript/mastodon/features/domain_blocks/index.js
  54. 1
      app/javascript/mastodon/features/favourited_statuses/index.js
  55. 4
      app/javascript/mastodon/features/favourites/index.js
  56. 4
      app/javascript/mastodon/features/follow_requests/index.js
  57. 4
      app/javascript/mastodon/features/followers/index.js
  58. 4
      app/javascript/mastodon/features/following/index.js
  59. 1
      app/javascript/mastodon/features/hashtag_timeline/index.js
  60. 1
      app/javascript/mastodon/features/home_timeline/index.js
  61. 1
      app/javascript/mastodon/features/list_timeline/index.js
  62. 4
      app/javascript/mastodon/features/lists/index.js
  63. 4
      app/javascript/mastodon/features/mutes/index.js
  64. 1
      app/javascript/mastodon/features/notifications/index.js
  65. 4
      app/javascript/mastodon/features/pinned_statuses/index.js
  66. 1
      app/javascript/mastodon/features/public_timeline/index.js
  67. 4
      app/javascript/mastodon/features/reblogs/index.js
  68. 24
      app/javascript/mastodon/features/ui/components/modal_root.js
  69. 15
      app/javascript/mastodon/features/ui/index.js
  70. 3
      app/javascript/mastodon/locales/ar.json
  71. 5
      app/javascript/mastodon/locales/ast.json
  72. 5
      app/javascript/mastodon/locales/bg.json
  73. 73
      app/javascript/mastodon/locales/bn.json
  74. 3
      app/javascript/mastodon/locales/ca.json
  75. 3
      app/javascript/mastodon/locales/co.json
  76. 3
      app/javascript/mastodon/locales/cs.json
  77. 3
      app/javascript/mastodon/locales/cy.json
  78. 3
      app/javascript/mastodon/locales/da.json
  79. 3
      app/javascript/mastodon/locales/de.json
  80. 27
      app/javascript/mastodon/locales/defaultMessages.json
  81. 3
      app/javascript/mastodon/locales/el.json
  82. 3
      app/javascript/mastodon/locales/en.json
  83. 3
      app/javascript/mastodon/locales/eo.json
  84. 109
      app/javascript/mastodon/locales/es.json
  85. 3
      app/javascript/mastodon/locales/eu.json
  86. 3
      app/javascript/mastodon/locales/fa.json
  87. 3
      app/javascript/mastodon/locales/fi.json
  88. 3
      app/javascript/mastodon/locales/fr.json
  89. 5
      app/javascript/mastodon/locales/gl.json
  90. 5
      app/javascript/mastodon/locales/he.json
  91. 3
      app/javascript/mastodon/locales/hi.json
  92. 5
      app/javascript/mastodon/locales/hr.json
  93. 3
      app/javascript/mastodon/locales/hu.json
  94. 5
      app/javascript/mastodon/locales/hy.json
  95. 93
      app/javascript/mastodon/locales/id.json
  96. 5
      app/javascript/mastodon/locales/io.json
  97. 11
      app/javascript/mastodon/locales/it.json
  98. 3
      app/javascript/mastodon/locales/ja.json
  99. 3
      app/javascript/mastodon/locales/ka.json
  100. 3
      app/javascript/mastodon/locales/kk.json
  101. Some files were not shown because too many files have changed in this diff Show More

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,5 +1,5 @@
{
"account.add_or_remove_from_list": "Add or Remove from lists",
"account.add_or_remove_from_list": "Tambah atau Hapus dari daftar",
"account.badges.bot": "Bot",
"account.block": "Blokir @{name}",
"account.block_domain": "Sembunyikan segalanya dari {domain}",
@ -7,23 +7,23 @@
"account.direct": "Direct Message @{name}",
"account.domain_blocked": "Domain disembunyikan",
"account.edit_profile": "Ubah profil",
"account.endorse": "Feature on profile",
"account.endorse": "Tampilkan di profil",
"account.follow": "Ikuti",
"account.followers": "Pengikut",
"account.followers.empty": "No one follows this user yet.",
"account.followers.empty": "Tidak ada satupun yang mengkuti pengguna ini saat ini.",
"account.follows": "Mengikuti",
"account.follows.empty": "This user doesn't follow anyone yet.",
"account.follows.empty": "Pengguna ini belum mengikuti siapapun.",
"account.follows_you": "Mengikuti anda",
"account.hide_reblogs": "Sembunyikan boosts dari @{name}",
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.link_verified_on": "Kepemilikan tautan ini telah dicek pada {date}",
"account.locked_info": "Status privasi akun ini disetel untuk dikunci. Pemilik secara manual meninjau siapa yang dapat mengikuti mereka.",
"account.media": "Media",
"account.mention": "Balasan @{name}",
"account.moved_to": "{name} telah pindah ke:",
"account.mute": "Bisukan @{name}",
"account.mute_notifications": "Sembunyikan notifikasi dari @{name}",
"account.muted": "Dibisukan",
"account.posts": "Toots",
"account.posts": "Toot",
"account.posts_with_replies": "Postingan dengan balasan",
"account.report": "Laporkan @{name}",
"account.requested": "Menunggu persetujuan. Klik untuk membatalkan permintaan",
@ -31,23 +31,23 @@
"account.show_reblogs": "Tampilkan boost dari @{name}",
"account.unblock": "Hapus blokir @{name}",
"account.unblock_domain": "Tampilkan {domain}",
"account.unendorse": "Don't feature on profile",
"account.unendorse": "Jangan tampilkan di profil",
"account.unfollow": "Berhenti mengikuti",
"account.unmute": "Berhenti membisukan @{name}",
"account.unmute_notifications": "Munculkan notifikasi dari @{name}",
"alert.unexpected.message": "An unexpected error occurred.",
"alert.unexpected.message": "Terjadi kesalahan yang tidak terduga.",
"alert.unexpected.title": "Oops!",
"boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini",
"bundle_column_error.body": "Kesalahan terjadi saat memuat komponen ini.",
"bundle_column_error.retry": "Coba lagi",
"bundle_column_error.title": "Network error",
"bundle_column_error.title": "Kesalahan jaringan",
"bundle_modal_error.close": "Tutup",
"bundle_modal_error.message": "Kesalahan terjadi saat memuat komponen ini.",
"bundle_modal_error.retry": "Coba lagi",
"column.blocks": "Pengguna diblokir",
"column.community": "Linimasa Lokal",
"column.direct": "Direct messages",
"column.domain_blocks": "Hidden domains",
"column.direct": "Pesan langsung",
"column.domain_blocks": "Topik tersembunyi",
"column.favourites": "Favorit",
"column.follow_requests": "Permintaan mengikuti",
"column.home": "Beranda",
@ -64,41 +64,41 @@
"column_header.show_settings": "Tampilkan pengaturan",
"column_header.unpin": "Lepaskan",
"column_subheading.settings": "Pengaturan",
"community.column_settings.media_only": "Media Only",
"community.column_settings.media_only": "Hanya media",
"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.direct_message_warning_learn_more": "Pelajari selengkapnya",
"compose_form.hashtag_warning": "Toot ini tidak akan ada dalam daftar tagar manapun karena telah di set sebagai tidak terdaftar. Hanya postingan publik yang bisa dicari dengan tagar.",
"compose_form.lock_disclaimer": "Akun anda tidak {locked}. Semua orang dapat mengikuti anda untuk melihat postingan khusus untuk pengikut anda.",
"compose_form.lock_disclaimer.lock": "terkunci",
"compose_form.placeholder": "Apa yang ada di pikiran anda?",
"compose_form.poll.add_option": "Add a choice",
"compose_form.poll.duration": "Poll duration",
"compose_form.poll.option_placeholder": "Choice {number}",
"compose_form.poll.remove_option": "Remove this choice",
"compose_form.poll.add_option": "Tambahkan pilihan",
"compose_form.poll.duration": "Durasi jajak pendapat",
"compose_form.poll.option_placeholder": "Pilihan {number}",
"compose_form.poll.remove_option": "Hapus opsi ini",
"compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Mark media as sensitive",
"compose_form.sensitive.hide": "Tandai sebagai media sensitif",
"compose_form.sensitive.marked": "Sumber ini telah ditandai sebagai sumber sensitif.",
"compose_form.sensitive.unmarked": "Sumber ini tidak ditandai sebagai sumber sensitif",
"compose_form.spoiler.marked": "Teks disembunyikan dibalik peringatan",
"compose_form.spoiler.unmarked": "Teks tidak tersembunyi",
"compose_form.spoiler_placeholder": "Peringatan konten",
"confirmation_modal.cancel": "Batal",
"confirmations.block.block_and_report": "Block & Report",
"confirmations.block.block_and_report": "Blokir & Laporkan",
"confirmations.block.confirm": "Blokir",
"confirmations.block.message": "Apa anda yakin ingin memblokir {name}?",
"confirmations.delete.confirm": "Hapus",
"confirmations.delete.message": "Apa anda yakin untuk menghapus status ini?",
"confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.confirm": "Hapus",
"confirmations.delete_list.message": "Apakah anda yakin untuk menghapus daftar ini secara permanen?",
"confirmations.domain_block.confirm": "Sembunyikan keseluruhan domain",
"confirmations.domain_block.message": "Apakah anda benar benar yakin untuk memblokir keseluruhan {domain}? Dalam kasus tertentu beberapa pemblokiran atau penyembunyian lebih baik.",
"confirmations.mute.confirm": "Bisukan",
"confirmations.mute.message": "Apa anda yakin ingin membisukan {name}?",
"confirmations.redraft.confirm": "Delete & redraft",
"confirmations.redraft.confirm": "Hapus dan konsep ulang",
"confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.",
"confirmations.reply.confirm": "Reply",
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.reply.confirm": "Balas",
"confirmations.reply.message": "Membalas sekarang akan menimpa pesan yang sedang Anda buat. Anda yakin ingin melanjutkan?",
"confirmations.unfollow.confirm": "Berhenti mengikuti",
"confirmations.unfollow.message": "Apakah anda ingin berhenti mengikuti {name}?",
"embed.instructions": "Sematkan status ini di website anda dengan menyalin kode di bawah ini.",
@ -117,38 +117,38 @@
"emoji_button.search_results": "Hasil pencarian",
"emoji_button.symbols": "Simbol",
"emoji_button.travel": "Tempat Wisata",
"empty_column.account_timeline": "No toots here!",
"empty_column.account_unavailable": "Profile unavailable",
"empty_column.blocks": "You haven't blocked any users yet.",
"empty_column.account_timeline": "Tidak ada toot di sini!",
"empty_column.account_unavailable": "Profil tidak tersedia",
"empty_column.blocks": "Anda belum memblokir siapapun.",
"empty_column.community": "Linimasa lokal masih kosong. Tulis sesuatu secara publik dan buat roda berputar!",
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
"empty_column.domain_blocks": "There are no hidden domains yet.",
"empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.",
"empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.",
"empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.",
"empty_column.direct": "Anda belum memiliki pesan langsung. Ketika Anda mengirim atau menerimanya, maka akan muncul di sini.",
"empty_column.domain_blocks": "Tidak ada topik tersembunyi.",
"empty_column.favourited_statuses": "Anda belum memiliki toot favorit. Ketika Anda mengirim atau menerimanya, maka akan muncul di sini.",
"empty_column.favourites": "Tidak ada seorangpun yang memfavoritkan toot ini. Ketika seseorang melakukannya, maka akan muncul disini.",
"empty_column.follow_requests": "Anda belum memiliki permintaan mengikuti. Ketika Anda menerimanya, maka akan muncul disini.",
"empty_column.hashtag": "Tidak ada apapun dalam hashtag ini.",
"empty_column.home": "Linimasa anda kosong! Kunjungi {public} atau gunakan pencarian untuk memulai dan bertemu pengguna lain.",
"empty_column.home.public_timeline": "linimasa publik",
"empty_column.list": "Tidak ada postingan di list ini. Ketika anggota dari list ini memposting status baru, status tersebut akan tampil disini.",
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
"empty_column.mutes": "You haven't muted any users yet.",
"empty_column.lists": "Anda belum memiliki daftar. Ketika Anda membuatnya, maka akan muncul disini.",
"empty_column.mutes": "Anda belum membisukan siapapun.",
"empty_column.notifications": "Anda tidak memiliki notifikasi apapun. Berinteraksi dengan orang lain untuk memulai percakapan.",
"empty_column.public": "Tidak ada apapun disini! Tulis sesuatu, atau ikuti pengguna lain dari server lain untuk mengisi ini",
"follow_request.authorize": "Izinkan",
"follow_request.reject": "Tolak",
"getting_started.developers": "Developers",
"getting_started.directory": "Profile directory",
"getting_started.documentation": "Documentation",
"getting_started.developers": "Pengembang",
"getting_started.directory": "Direktori profil",
"getting_started.documentation": "Dokumentasi",
"getting_started.heading": "Mulai",
"getting_started.invite": "Invite people",
"getting_started.invite": "Undang orang",
"getting_started.open_source_notice": "Mastodon adalah perangkat lunak yang bersifat terbuka. Anda dapat berkontribusi atau melaporkan permasalahan/bug di Github {github}.",
"getting_started.security": "Security",
"getting_started.terms": "Terms of service",
"hashtag.column_header.tag_mode.all": "and {additional}",
"hashtag.column_header.tag_mode.any": "or {additional}",
"hashtag.column_header.tag_mode.none": "without {additional}",
"hashtag.column_settings.select.no_options_message": "No suggestions found",
"hashtag.column_settings.select.placeholder": "Enter hashtags…",
"getting_started.security": "Keamanan",
"getting_started.terms": "Ketentuan layanan",
"hashtag.column_header.tag_mode.all": "dan {additional}",
"hashtag.column_header.tag_mode.any": "atau {additional}",
"hashtag.column_header.tag_mode.none": "tanpa {additional}",
"hashtag.column_settings.select.no_options_message": "Tidak ada saran yang ditemukan",
"hashtag.column_settings.select.placeholder": "Masukkan tagar…",
"hashtag.column_settings.tag_mode.all": "All of these",
"hashtag.column_settings.tag_mode.any": "Any of these",
"hashtag.column_settings.tag_mode.none": "None of these",
@ -156,6 +156,7 @@
"home.column_settings.basic": "Dasar",
"home.column_settings.show_reblogs": "Tampilkan boost",
"home.column_settings.show_replies": "Tampilkan balasan",
"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": "Tunggu sebentar...",
"media_gallery.toggle_visible": "Tampil/Sembunyikan",
"missing_indicator.label": "Tidak ditemukan",
@ -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 {hasil} other {hasil}}",
"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": "Simpla",
"home.column_settings.show_reblogs": "Montrar repeti",
"home.column_settings.show_replies": "Montrar respondi",
"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": "Kargante...",
"media_gallery.toggle_visible": "Chanjar videbleso",
"missing_indicator.label": "Ne trovita",
@ -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 {rezulto} other {rezulti}}",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface",

@ -4,7 +4,7 @@
"account.block": "Blocca @{name}",
"account.block_domain": "Nascondi tutto da {domain}",
"account.blocked": "Bloccato",
"account.direct": "Invia messaggio diretto a @{name}",
"account.direct": "Invia messaggio privato a @{name}",
"account.domain_blocked": "Dominio nascosto",
"account.edit_profile": "Modifica profilo",
"account.endorse": "Metti in evidenza sul profilo",
@ -121,7 +121,7 @@
"empty_column.account_unavailable": "Profilo non disponibile",
"empty_column.blocks": "Non hai ancora bloccato nessun utente.",
"empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!",
"empty_column.direct": "Non hai ancora nessun messaggio diretto. Quando ne manderai o riceverai qualcuno, apparirà qui.",
"empty_column.direct": "Non hai ancora nessun messaggio privato. Quando ne manderai o riceverai qualcuno, apparirà qui.",
"empty_column.domain_blocks": "Non vi sono domini nascosti.",
"empty_column.favourited_statuses": "Non hai ancora segnato nessun toot come apprezzato. Quando lo farai, comparirà qui.",
"empty_column.favourites": "Nessuno ha ancora segnato questo toot come apprezzato. Quando qualcuno lo farà, apparirà qui.",
@ -156,6 +156,7 @@
"home.column_settings.basic": "Semplice",
"home.column_settings.show_reblogs": "Mostra post condivisi",
"home.column_settings.show_replies": "Mostra risposte",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# giorno} other {# giorni}}",
"intervals.full.hours": "{number, plural, one {# ora} other {# ore}}",
"intervals.full.minutes": "{number, plural, one {# minuto} other {# minuti}}",
@ -221,6 +222,7 @@
"lists.new.title_placeholder": "Titolo della nuova lista",
"lists.search": "Cerca tra le persone che segui",
"lists.subheading": "Le tue liste",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Caricamento...",
"media_gallery.toggle_visible": "Imposta visibilità",
"missing_indicator.label": "Non trovato",
@ -283,7 +285,7 @@
"poll_button.remove_poll": "Rimuovi sondaggio",
"privacy.change": "Modifica privacy del post",
"privacy.direct.long": "Invia solo a utenti menzionati",
"privacy.direct.short": "Diretto",
"privacy.direct.short": "Diretto in privato",
"privacy.private.long": "Invia solo ai seguaci",
"privacy.private.short": "Privato",
"privacy.public.long": "Invia alla timeline pubblica",
@ -314,6 +316,7 @@
"search_results.accounts": "Gente",
"search_results.hashtags": "Hashtag",
"search_results.statuses": "Toot",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count} {count, plural, one {risultato} other {risultati}}",
"status.admin_account": "Apri interfaccia di moderazione per @{name}",
"status.admin_status": "Apri questo status nell'interfaccia di moderazione",
@ -323,7 +326,7 @@
"status.copy": "Copia link allo status",
"status.delete": "Elimina",
"status.detailed_status": "Vista conversazione dettagliata",
"status.direct": "Messaggio diretto @{name}",
"status.direct": "Messaggio privato @{name}",
"status.embed": "Incorpora",
"status.favourite": "Apprezzato",
"status.filtered": "Filtrato",

@ -160,6 +160,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}日",
"intervals.full.hours": "{number}時間",
"intervals.full.minutes": "{number}分",
@ -225,6 +226,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": "見つかりません",
@ -319,6 +321,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}件の結果",
"status.admin_account": "@{name} のモデレーション画面を開く",
"status.admin_status": "このトゥートをモデレーション画面で開く",

@ -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": "ხალხი",
"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} 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": "Негізгі",
"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 {# 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": "Адамдар",
"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} other {results}}",
"status.admin_account": "@{name} үшін модерация интерфейсін аш",
"status.admin_status": "Бұл жазбаны модерация интерфейсінде аш",

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

Loading…
Cancel
Save