Merge branch 'kb_development' into kb_patch

This commit is contained in:
KMY 2023-10-11 13:56:53 +09:00
commit a045c5eff1
244 changed files with 5451 additions and 1468 deletions

View file

@ -1,4 +0,0 @@
---
ignore:
# Sidekiq security issue, fixes in the latest Sidekiq 7 but we can not upgrade. Will be fixed in Sidekiq 6.5.10
- CVE-2023-26141

View file

@ -70,7 +70,7 @@ services:
hard: -1 hard: -1
libretranslate: libretranslate:
image: libretranslate/libretranslate:v1.3.11 image: libretranslate/libretranslate:v1.3.12
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- lt-data:/home/libretranslate/.local - lt-data:/home/libretranslate/.local

View file

@ -284,8 +284,8 @@ jobs:
ports: ports:
- 6379:6379 - 6379:6379
elasticsearch: search:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.13 image: ${{ matrix.search-image }}
env: env:
discovery.type: single-node discovery.type: single-node
xpack.security.enabled: false xpack.security.enabled: false
@ -315,6 +315,11 @@ jobs:
- '3.0' - '3.0'
- '3.1' - '3.1'
- '.ruby-version' - '.ruby-version'
search-image:
- docker.elastic.co/elasticsearch/elasticsearch:7.17.13
include:
- ruby-version: '.ruby-version'
search-image: docker.elastic.co/elasticsearch/elasticsearch:8.10.2
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

3
.gitignore vendored
View file

@ -31,9 +31,6 @@
# Ignore Vagrant files # Ignore Vagrant files
.vagrant/ .vagrant/
# Ignore Capistrano customizations
/config/deploy/*
# Ignore IDE files # Ignore IDE files
.vscode/ .vscode/
.idea/ .idea/

View file

@ -1,13 +1,13 @@
# This configuration was generated by # This configuration was generated by
# `haml-lint --auto-gen-config` # `haml-lint --auto-gen-config`
# on 2023-07-20 09:47:50 -0400 using Haml-Lint version 0.48.0. # on 2023-10-03 08:32:28 -0400 using Haml-Lint version 0.51.0.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the lints are removed from the code base. # one by one as the lints are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of Haml-Lint, may require this file to be generated again. # versions of Haml-Lint, may require this file to be generated again.
linters: linters:
# Offense count: 951 # Offense count: 944
LineLength: LineLength:
enabled: false enabled: false
@ -15,7 +15,7 @@ linters:
UnnecessaryStringOutput: UnnecessaryStringOutput:
enabled: false enabled: false
# Offense count: 57 # Offense count: 44
RuboCop: RuboCop:
enabled: false enabled: false
@ -27,23 +27,17 @@ linters:
- 'app/views/admin/reports/show.html.haml' - 'app/views/admin/reports/show.html.haml'
- 'app/views/disputes/strikes/show.html.haml' - 'app/views/disputes/strikes/show.html.haml'
# Offense count: 32 # Offense count: 15
InstanceVariables: InstanceVariables:
exclude: exclude:
- 'app/views/admin/reports/_actions.html.haml' - 'app/views/admin/reports/_actions.html.haml'
- 'app/views/admin/roles/_form.html.haml'
- 'app/views/admin/webhooks/_form.html.haml'
- 'app/views/auth/registrations/_status.html.haml' - 'app/views/auth/registrations/_status.html.haml'
- 'app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml' - 'app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml'
- 'app/views/authorize_interactions/_post_follow_actions.html.haml'
- 'app/views/invites/_form.html.haml'
- 'app/views/relationships/_account.html.haml' - 'app/views/relationships/_account.html.haml'
- 'app/views/shared/_og.html.haml'
- 'app/views/application/_sidebar.html.haml' - 'app/views/application/_sidebar.html.haml'
# Offense count: 3 # Offense count: 2
IdNames: IdNames:
exclude: exclude:
- 'app/views/authorize_interactions/error.html.haml'
- 'app/views/oauth/authorizations/error.html.haml' - 'app/views/oauth/authorizations/error.html.haml'
- 'app/views/shared/_error_messages.html.haml' - 'app/views/shared/_error_messages.html.haml'

2
.nvmrc
View file

@ -1 +1 @@
20.7 20.8

View file

@ -31,9 +31,6 @@
# Ignore Vagrant files # Ignore Vagrant files
.vagrant/ .vagrant/
# Ignore Capistrano customizations
/config/deploy/*
# Ignore IDE files # Ignore IDE files
.vscode/ .vscode/
.idea/ .idea/

View file

@ -28,6 +28,7 @@ AllCops:
- 'Vagrantfile' - 'Vagrantfile'
- 'vendor/**/*' - 'vendor/**/*'
- 'lib/json_ld/*' # Generated files - 'lib/json_ld/*' # Generated files
- 'lib/mastodon/migration_helpers.rb' # Vendored from GitLab
- 'lib/templates/**/*' - 'lib/templates/**/*'
# Reason: Prefer Hashes without extreme indentation # Reason: Prefer Hashes without extreme indentation
@ -76,12 +77,6 @@ Metrics/AbcSize:
- 'lib/mastodon/cli/*.rb' - 'lib/mastodon/cli/*.rb'
- db/*migrate/**/* - db/*migrate/**/*
# Reason:
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting
Metrics/BlockNesting:
Exclude:
- 'lib/mastodon/cli/*.rb'
# Reason: Currently disabled in .rubocop_todo.yml # Reason: Currently disabled in .rubocop_todo.yml
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity # https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:

View file

@ -13,32 +13,6 @@ Bundler/OrderedGems:
Exclude: Exclude:
- 'Gemfile' - 'Gemfile'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: with_first_argument, with_fixed_indentation
Layout/ArgumentAlignment:
Exclude:
- 'config/initializers/cors.rb'
- 'config/initializers/session_store.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
# SupportedHashRocketStyles: key, separator, table
# SupportedColonStyles: key, separator, table
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
Layout/HashAlignment:
Exclude:
- 'config/environments/production.rb'
- 'config/initializers/rack_attack.rb'
- 'config/routes.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment.
Layout/LeadingCommentSpace:
Exclude:
- 'config/application.rb'
- 'config/initializers/3_omniauth.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. # Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
# URISchemes: http, https # URISchemes: http, https
@ -46,14 +20,6 @@ Layout/LineLength:
Exclude: Exclude:
- 'app/models/account.rb' - 'app/models/account.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: require_no_space, require_space
Layout/SpaceInLambdaLiteral:
Exclude:
- 'config/environments/production.rb'
- 'config/initializers/content_security_policy.rb'
# Configuration parameters: AllowComments, AllowEmptyLambdas. # Configuration parameters: AllowComments, AllowEmptyLambdas.
Lint/EmptyBlock: Lint/EmptyBlock:
Exclude: Exclude:
@ -289,10 +255,6 @@ RSpec/MultipleMemoizedHelpers:
RSpec/NestedGroups: RSpec/NestedGroups:
Max: 6 Max: 6
RSpec/PendingWithoutReason:
Exclude:
- 'spec/models/account_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
Rails/ApplicationController: Rails/ApplicationController:
Exclude: Exclude:
@ -848,6 +810,5 @@ Style/TrailingCommaInHashLiteral:
Style/WordArray: Style/WordArray:
Exclude: Exclude:
- 'app/helpers/languages_helper.rb' - 'app/helpers/languages_helper.rb'
- 'config/initializers/cors.rb'
- 'spec/controllers/settings/imports_controller_spec.rb' - 'spec/controllers/settings/imports_controller_spec.rb'
- 'spec/models/form/import_spec.rb' - 'spec/models/form/import_spec.rb'

View file

@ -2,7 +2,7 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [4.2.1] - UNRELEASED ## [4.2.1] - 2023-10-10
### Added ### Added
@ -16,6 +16,11 @@ All notable changes to this project will be documented in this file.
### Fixed ### Fixed
- Fix clicking on already-opened thread post scrolling to the top of the thread ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27331), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27338), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27350))
- Fix some remote posts getting truncated ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27307))
- Fix some cases of infinite scroll code trying to fetch inaccessible posts in a loop ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27286))
- Fix `Vary` headers not being set on some redirects ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27272))
- Fix mentions being matched in some URL query strings ([mjankowski](https://github.com/mastodon/mastodon/pull/25656))
- Fix unexpected linebreak in version string in the Web UI ([vmstan](https://github.com/mastodon/mastodon/pull/26986)) - Fix unexpected linebreak in version string in the Web UI ([vmstan](https://github.com/mastodon/mastodon/pull/26986))
- Fix double scroll bars in some columns in advanced interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27187)) - Fix double scroll bars in some columns in advanced interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27187))
- Fix boosts of local users being filtered in account timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27204)) - Fix boosts of local users being filtered in account timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27204))
@ -32,7 +37,7 @@ All notable changes to this project will be documented in this file.
- Fix retention dashboard not displaying correct month ([vmstan](https://github.com/mastodon/mastodon/pull/27180)) - Fix retention dashboard not displaying correct month ([vmstan](https://github.com/mastodon/mastodon/pull/27180))
- Fix tIME chunk not being properly removed from PNG uploads ([TheEssem](https://github.com/mastodon/mastodon/pull/27111)) - Fix tIME chunk not being properly removed from PNG uploads ([TheEssem](https://github.com/mastodon/mastodon/pull/27111))
- Fix division by zero in video in bitrate computation code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27129)) - Fix division by zero in video in bitrate computation code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27129))
- Fix inefficient queries in “Follows and followers” as well as several admin pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27116)) - Fix inefficient queries in “Follows and followers” as well as several admin pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27116), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27306))
- Fix ActiveRecord using two connection pools when no replica is defined ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27061)) - Fix ActiveRecord using two connection pools when no replica is defined ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27061))
- Fix the search documentation URL in system checks ([renchap](https://github.com/mastodon/mastodon/pull/27036)) - Fix the search documentation URL in system checks ([renchap](https://github.com/mastodon/mastodon/pull/27036))

15
Capfile
View file

@ -1,15 +0,0 @@
# frozen_string_literal: true
require 'capistrano/setup'
require 'capistrano/deploy'
require 'capistrano/scm/git'
install_plugin Capistrano::SCM::Git
require 'capistrano/rbenv'
require 'capistrano/bundler'
require 'capistrano/yarn'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

View file

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.4 # syntax=docker/dockerfile:1.4
# This needs to be bookworm-slim because the Ruby image is built on bookworm-slim # This needs to be bookworm-slim because the Ruby image is built on bookworm-slim
ARG NODE_VERSION="20.7-bookworm-slim" ARG NODE_VERSION="20.8-bookworm-slim"
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby
FROM node:${NODE_VERSION} as build FROM node:${NODE_VERSION} as build

View file

@ -172,12 +172,6 @@ group :development do
# Linter CLI for HAML files # Linter CLI for HAML files
gem 'haml_lint', require: false gem 'haml_lint', require: false
# Deployment automation
gem 'capistrano', '~> 3.17'
gem 'capistrano-rails', '~> 1.6'
gem 'capistrano-rbenv', '~> 2.2'
gem 'capistrano-yarn', '~> 2.0'
# Validate missing i18n keys # Validate missing i18n keys
gem 'i18n-tasks', '~> 1.0', require: false gem 'i18n-tasks', '~> 1.0', require: false
end end

View file

@ -84,9 +84,9 @@ GEM
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_model_serializers (0.10.13) active_model_serializers (0.10.14)
actionpack (>= 4.1, < 7.1) actionpack (>= 4.1)
activemodel (>= 4.1, < 7.1) activemodel (>= 4.1)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.0.8) activejob (7.0.8)
@ -112,8 +112,6 @@ GEM
addressable (2.8.5) addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0) aes_key_wrap (1.1.0)
airbrussh (1.4.1)
sshkit (>= 1.6.1, != 1.7.0)
android_key_attestation (0.3.0) android_key_attestation (0.3.0)
annotate (3.2.0) annotate (3.2.0)
activerecord (>= 3.2, < 8.0) activerecord (>= 3.2, < 8.0)
@ -175,21 +173,6 @@ GEM
bundler-audit (0.9.1) bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 1.0) thor (~> 1.0)
capistrano (3.17.3)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
sshkit (>= 1.9.0)
capistrano-bundler (2.1.0)
capistrano (~> 3.1)
capistrano-rails (1.6.3)
capistrano (~> 3.1)
capistrano-bundler (>= 1.1, < 3)
capistrano-rbenv (2.2.0)
capistrano (~> 3.1)
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (3.39.2) capybara (3.39.2)
addressable addressable
matrix matrix
@ -324,7 +307,7 @@ GEM
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
globalid (1.1.0) globalid (1.1.0)
activesupport (>= 5.0) activesupport (>= 5.0)
haml (6.1.2) haml (6.2.0)
temple (>= 0.8.2) temple (>= 0.8.2)
thor thor
tilt tilt
@ -333,8 +316,8 @@ GEM
activesupport (>= 5.1) activesupport (>= 5.1)
haml (>= 4.0.6) haml (>= 4.0.6)
railties (>= 5.1) railties (>= 5.1)
haml_lint (0.50.0) haml_lint (0.51.0)
haml (>= 4.0, < 6.2) haml (>= 4.0)
parallel (~> 1.10) parallel (~> 1.10)
rainbow rainbow
rubocop (>= 1.0) rubocop (>= 1.0)
@ -473,11 +456,8 @@ GEM
net-protocol net-protocol
net-protocol (0.2.1) net-protocol (0.2.1)
timeout timeout
net-scp (4.0.0)
net-ssh (>= 2.6.5, < 8.0.0)
net-smtp (0.3.3) net-smtp (0.3.3)
net-protocol net-protocol
net-ssh (7.1.0)
nio4r (2.5.9) nio4r (2.5.9)
nokogiri (1.15.4) nokogiri (1.15.4)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
@ -642,7 +622,7 @@ GEM
sidekiq (>= 5, < 8) sidekiq (>= 5, < 8)
rspec-support (3.12.1) rspec-support (3.12.1)
rspec_chunked (0.6) rspec_chunked (0.6)
rubocop (1.56.3) rubocop (1.56.4)
base64 (~> 0.1.1) base64 (~> 0.1.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
@ -693,7 +673,7 @@ GEM
rubyzip (>= 1.2.2, < 3.0) rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0) websocket (~> 1.0)
semantic_range (3.0.0) semantic_range (3.0.0)
sidekiq (6.5.9) sidekiq (6.5.10)
connection_pool (>= 2.2.5, < 3) connection_pool (>= 2.2.5, < 3)
rack (~> 2.0) rack (~> 2.0)
redis (>= 4.5.0, < 5) redis (>= 4.5.0, < 5)
@ -728,9 +708,6 @@ GEM
actionpack (>= 5.2) actionpack (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sshkit (1.21.5)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.25) stackprof (0.2.25)
statsd-ruby (1.5.0) statsd-ruby (1.5.0)
stoplight (3.0.2) stoplight (3.0.2)
@ -749,7 +726,7 @@ GEM
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
test-prof (1.2.3) test-prof (1.2.3)
thor (1.2.2) thor (1.2.2)
tilt (2.2.0) tilt (2.3.0)
timeout (0.4.0) timeout (0.4.0)
tpm-key_attestation (0.12.0) tpm-key_attestation (0.12.0)
bindata (~> 2.4) bindata (~> 2.4)
@ -775,7 +752,7 @@ GEM
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.8.2) unf_ext (0.0.8.2)
unicode-display_width (2.4.2) unicode-display_width (2.5.0)
uri (0.12.2) uri (0.12.2)
validate_email (0.1.6) validate_email (0.1.6)
activemodel (>= 3.0) activemodel (>= 3.0)
@ -831,10 +808,6 @@ DEPENDENCIES
brakeman (~> 6.0) brakeman (~> 6.0)
browser browser
bundler-audit (~> 0.9) bundler-audit (~> 0.9)
capistrano (~> 3.17)
capistrano-rails (~> 1.6)
capistrano-rbenv (~> 2.2)
capistrano-yarn (~> 2.0)
capybara (~> 3.39) capybara (~> 3.39)
charlock_holmes (~> 0.7.7) charlock_holmes (~> 0.7.7)
chewy (~> 7.3) chewy (~> 7.3)

View file

@ -15,6 +15,7 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| Version | Supported | | Version | Supported |
| ------- | ---------------- | | ------- | ---------------- |
| 4.2.x | Yes |
| 4.1.x | Yes | | 4.1.x | Yes |
| 4.0.x | Until 2023-10-31 | | 4.0.x | Until 2023-10-31 |
| 3.5.x | Until 2023-12-31 | | 3.5.x | Until 2023-12-31 |

View file

@ -5,15 +5,7 @@ class AboutController < ApplicationController
skip_before_action :require_functional! skip_before_action :require_functional!
before_action :set_instance_presenter
def show def show
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
end end
private
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
end end

View file

@ -89,17 +89,17 @@ module Admin
def update_params def update_params
params.require(:domain_block).permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, params.require(:domain_block).permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag,
:reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous) :reject_straight_follow, :reject_new_follow, :reject_friend, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous)
end end
def resource_params def resource_params
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag,
:reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous) :reject_straight_follow, :reject_new_follow, :reject_friend, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous)
end end
def form_domain_block_batch_params def form_domain_block_batch_params
params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media,
:reject_send_sensitive, :reject_hashtag, :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous]) :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, :reject_new_follow, :reject_friend, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous])
end end
def action_from_button def action_from_button

View file

@ -0,0 +1,93 @@
# frozen_string_literal: true
module Admin
class FriendServersController < BaseController
before_action :set_friend, except: [:index, :new, :create]
before_action :warn_signatures_not_enabled!, only: [:new, :edit, :create, :follow, :unfollow, :accept, :reject]
def index
authorize :friend_server, :update?
@friends = FriendDomain.all
end
def new
authorize :friend_server, :update?
@friend = FriendDomain.new
end
def edit
authorize :friend_server, :update?
end
def create
authorize :friend_server, :update?
@friend = FriendDomain.new(resource_params)
if @friend.save
@friend.follow!
redirect_to admin_friend_servers_path
else
render action: :new
end
end
def update
authorize :friend_server, :update?
if @friend.update(update_resource_params)
redirect_to admin_friend_servers_path
else
render action: :edit
end
end
def destroy
authorize :friend_server, :update?
@friend.destroy
redirect_to admin_friend_servers_path
end
def follow
authorize :friend_server, :update?
@friend.follow!
render action: :edit
end
def unfollow
authorize :friend_server, :update?
@friend.unfollow!
render action: :edit
end
def accept
authorize :friend_server, :update?
@friend.accept!
render action: :edit
end
def reject
authorize :friend_server, :update?
@friend.reject!
render action: :edit
end
private
def set_friend
@friend = FriendDomain.find(params[:id])
end
def resource_params
params.require(:friend_domain).permit(:domain, :inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts)
end
def update_resource_params
params.require(:friend_domain).permit(:inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts)
end
def warn_signatures_not_enabled!
flash.now[:error] = I18n.t('admin.relays.signatures_not_enabled') if authorized_fetch_mode?
end
end
end

View file

@ -70,7 +70,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
def domain_block_params def domain_block_params
params.permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_reports, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, params.permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_reports, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow,
:reject_new_follow, :detect_invalid_subscription, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous) :reject_new_follow, :reject_friend, :detect_invalid_subscription, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous)
end end
def insert_pagination_headers def insert_pagination_headers
@ -103,6 +103,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
def resource_params def resource_params
params.permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, params.permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow,
:reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous) :reject_new_follow, :reject_friend, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous)
end end
end end

View file

@ -52,11 +52,11 @@ class Api::V1::FiltersController < Api::BaseController
end end
def resource_params def resource_params
params.permit(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :whole_word, context: []) params.permit(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :whole_word, context: [])
end end
def filter_params def filter_params
resource_params.slice(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :context) resource_params.slice(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :context)
end end
def keyword_params def keyword_params

View file

@ -31,7 +31,7 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController
end end
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new( render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(
[@status], current_account.id, emoji_reactions_map: { @status.id => false } [@status], current_account.id
) )
rescue Mastodon::NotPermittedError rescue Mastodon::NotPermittedError
not_found not_found

View file

@ -43,6 +43,6 @@ class Api::V2::FiltersController < Api::BaseController
end end
def resource_params def resource_params
params.permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) params.permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :with_quote, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
end end
end end

View file

@ -10,7 +10,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create] before_action :configure_sign_up_params, only: [:create]
before_action :set_sessions, only: [:edit, :update] before_action :set_sessions, only: [:edit, :update]
before_action :set_strikes, only: [:edit, :update] before_action :set_strikes, only: [:edit, :update]
before_action :set_instance_presenter, only: [:new, :create, :update]
before_action :set_body_classes, only: [:new, :create, :edit, :update] before_action :set_body_classes, only: [:new, :create, :edit, :update]
before_action :require_not_suspended!, only: [:update] before_action :require_not_suspended!, only: [:update]
before_action :set_cache_headers, only: [:edit, :update] before_action :set_cache_headers, only: [:edit, :update]
@ -107,10 +106,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
private private
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def set_body_classes def set_body_classes
@body_classes = %w(edit update).include?(action_name) ? 'admin' : 'lighter' @body_classes = %w(edit update).include?(action_name) ? 'admin' : 'lighter'
end end

View file

@ -11,7 +11,6 @@ class Auth::SessionsController < Devise::SessionsController
include TwoFactorAuthenticationConcern include TwoFactorAuthenticationConcern
before_action :set_instance_presenter, only: [:new]
before_action :set_body_classes before_action :set_body_classes
content_security_policy only: :new do |p| content_security_policy only: :new do |p|
@ -99,10 +98,6 @@ class Auth::SessionsController < Devise::SessionsController
private private
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def set_body_classes def set_body_classes
@body_classes = 'lighter' @body_classes = 'lighter'
end end

View file

@ -9,17 +9,11 @@ module AccountControllerConcern
FOLLOW_PER_PAGE = 12 FOLLOW_PER_PAGE = 12
included do included do
before_action :set_instance_presenter
after_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } after_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
end end
private private
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def set_link_headers def set_link_headers
response.headers['Link'] = LinkHeader.new( response.headers['Link'] = LinkHeader.new(
[ [

View file

@ -4,10 +4,10 @@ module WebAppControllerConcern
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
prepend_before_action :redirect_unauthenticated_to_permalinks!
before_action :set_app_body_class
vary_by 'Accept, Accept-Language, Cookie' vary_by 'Accept, Accept-Language, Cookie'
before_action :redirect_unauthenticated_to_permalinks!
before_action :set_app_body_class
end end
def skip_csrf_meta_tags? def skip_csrf_meta_tags?
@ -22,7 +22,9 @@ module WebAppControllerConcern
return if user_signed_in? && current_account.moved_to_account_id.nil? return if user_signed_in? && current_account.moved_to_account_id.nil?
redirect_path = PermalinkRedirector.new(request.path).redirect_path redirect_path = PermalinkRedirector.new(request.path).redirect_path
return if redirect_path.blank?
redirect_to(redirect_path) if redirect_path.present? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
redirect_to(redirect_path)
end end
end end

View file

@ -49,7 +49,7 @@ class FiltersController < ApplicationController
end end
def resource_params def resource_params
params.require(:custom_filter).permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) params.require(:custom_filter).permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :with_quote, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
end end
def set_body_classes def set_body_classes

View file

@ -3,7 +3,6 @@
class FollowerAccountsController < ApplicationController class FollowerAccountsController < ApplicationController
include AccountControllerConcern include AccountControllerConcern
include SignatureVerification include SignatureVerification
include WebAppControllerConcern
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }

View file

@ -3,7 +3,6 @@
class FollowingAccountsController < ApplicationController class FollowingAccountsController < ApplicationController
include AccountControllerConcern include AccountControllerConcern
include SignatureVerification include SignatureVerification
include WebAppControllerConcern
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }

View file

@ -3,15 +3,7 @@
class HomeController < ApplicationController class HomeController < ApplicationController
include WebAppControllerConcern include WebAppControllerConcern
before_action :set_instance_presenter
def index def index
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
end end
private
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
end end

View file

@ -5,15 +5,7 @@ class PrivacyController < ApplicationController
skip_before_action :require_functional! skip_before_action :require_functional!
before_action :set_instance_presenter
def show def show
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
end end
private
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
end end

View file

@ -10,7 +10,6 @@ class StatusesController < ApplicationController
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status before_action :set_status
before_action :set_instance_presenter
before_action :redirect_to_original, only: :show before_action :redirect_to_original, only: :show
before_action :set_body_classes, only: :embed before_action :set_body_classes, only: :embed
@ -72,10 +71,6 @@ class StatusesController < ApplicationController
not_found not_found
end end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def redirect_to_original def redirect_to_original
redirect_to(ActivityPub::TagManager.instance.url_for(@status.reblog), allow_other_host: true) if @status.reblog? redirect_to(ActivityPub::TagManager.instance.url_for(@status.reblog), allow_other_host: true) if @status.reblog?
end end

View file

@ -14,7 +14,6 @@ class TagsController < ApplicationController
before_action :set_local before_action :set_local
before_action :set_tag before_action :set_tag
before_action :set_statuses, if: -> { request.format == :rss } before_action :set_statuses, if: -> { request.format == :rss }
before_action :set_instance_presenter
skip_before_action :require_functional!, unless: :limited_federation_mode? skip_before_action :require_functional!, unless: :limited_federation_mode?
@ -49,10 +48,6 @@ class TagsController < ApplicationController
@statuses = cache_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status) @statuses = cache_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status)
end end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def limit_param def limit_param
params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
end end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Admin::AnnouncementsHelper
def datetime_pattern
'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}'
end
def datetime_placeholder
Time.zone.now.strftime('%FT%R')
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module InvitesHelper
def invites_max_uses_options
[1, 5, 10, 25, 50, 100]
end
def invites_expires_options
[30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week]
end
end

View file

@ -17,7 +17,8 @@ module KmyblueCapabilitiesHelper
:kmyblue_bookmark_category, :kmyblue_bookmark_category,
:kmyblue_quote, :kmyblue_quote,
:kmyblue_searchability_limited, :kmyblue_searchability_limited,
:kmyblue_visibility_public_unlisted, :kmyblue_searchability_public_unlisted,
:kmyblue_circle_history,
] ]
capabilities << :profile_search unless Chewy.enabled? capabilities << :profile_search unless Chewy.enabled?
@ -25,6 +26,7 @@ module KmyblueCapabilitiesHelper
capabilities << :emoji_reaction capabilities << :emoji_reaction
capabilities << :enable_wide_emoji_reaction capabilities << :enable_wide_emoji_reaction
end end
capabilities << :kmyblue_visibility_public_unlisted if Setting.enable_public_unlisted_visibility
capabilities capabilities
end end

View file

@ -230,6 +230,24 @@ module LanguagesHelper
'sr-Latn': 'Srpski (latinica)', 'sr-Latn': 'Srpski (latinica)',
}.freeze }.freeze
# Helper for self.sorted_locale_keys
private_class_method def self.locale_name_for_sorting(locale)
if locale.blank? || locale == 'und'
'000'
elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
ASCIIFolding.new.fold(supported_locale[1]).downcase
elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym])
ASCIIFolding.new.fold(regional_locale).downcase
else
locale
end
end
# Sort locales by native name for dropdown menus
def self.sorted_locale_keys(locale_keys)
locale_keys.sort_by { |key, _| locale_name_for_sorting(key) }
end
def native_locale_name(locale) def native_locale_name(locale)
if locale.blank? || locale == 'und' if locale.blank? || locale == 'und'
I18n.t('generic.none') I18n.t('generic.none')
@ -254,6 +272,7 @@ module LanguagesHelper
def valid_locale_or_nil(str) def valid_locale_or_nil(str)
return if str.blank? return if str.blank?
return str if valid_locale?(str)
code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP

View file

@ -5,8 +5,6 @@ module MascotHelper
full_asset_url(instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg')) full_asset_url(instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg'))
end end
private
def instance_presenter def instance_presenter
@instance_presenter ||= InstancePresenter.new @instance_presenter ||= InstancePresenter.new
end end

View file

@ -3,11 +3,12 @@
module RoutingHelper module RoutingHelper
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Rails.application.routes.url_helpers
include ActionView::Helpers::AssetTagHelper include ActionView::Helpers::AssetTagHelper
include Webpacker::Helper include Webpacker::Helper
included do included do
include Rails.application.routes.url_helpers
def default_url_options def default_url_options
ActionMailer::Base.default_url_options ActionMailer::Base.default_url_options
end end

View file

@ -2,7 +2,11 @@
module SettingsHelper module SettingsHelper
def filterable_languages def filterable_languages
LanguagesHelper::SUPPORTED_LOCALES.keys LanguagesHelper.sorted_locale_keys(LanguagesHelper::SUPPORTED_LOCALES.keys)
end
def ui_languages
LanguagesHelper.sorted_locale_keys(I18n.available_locales)
end end
def session_device_icon(session) def session_device_icon(session)

View file

@ -80,6 +80,10 @@ export function importFetchedStatuses(statuses) {
processStatus(status.reblog); processStatus(status.reblog);
} }
if (status.quote && status.quote.id) {
processStatus(status.quote);
}
if (status.poll && status.poll.id) { if (status.poll && status.poll.id) {
pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id]))); pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
} }

View file

@ -85,6 +85,11 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.spoiler_text = normalOldStatus.get('spoiler_text'); normalStatus.spoiler_text = normalOldStatus.get('spoiler_text');
normalStatus.hidden = normalOldStatus.get('hidden'); normalStatus.hidden = normalOldStatus.get('hidden');
// for quoted post
if (!normalStatus.filtered && normalOldStatus.get('filtered')) {
normalStatus.filtered = normalOldStatus.get('filtered');
}
if (normalOldStatus.get('translation')) { if (normalOldStatus.get('translation')) {
normalStatus.translation = normalOldStatus.get('translation'); normalStatus.translation = normalOldStatus.get('translation');
} }

View file

@ -45,6 +45,21 @@ describe('computeHashtagBarForStatus', () => {
); );
}); });
it('does not truncate the contents when the last child is a text node', () => {
const status = createStatus(
'this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text',
['test'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text"`,
);
});
it('extract tags from the last line', () => { it('extract tags from the last line', () => {
const status = createStatus( const status = createStatus(
'<p>Simple text</p><p><a href="test">#hashtag</a></p>', '<p>Simple text</p><p><a href="test">#hashtag</a></p>',

View file

@ -0,0 +1,503 @@
import PropTypes from 'prop-types';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys';
import AttachmentList from 'mastodon/components/attachment_list';
import { Icon } from 'mastodon/components/icon';
import Card from '../features/status/components/card';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { displayMedia } from '../initial_state';
import { Avatar } from './avatar';
import { DisplayName } from './display_name';
import { getHashtagBarForStatus } from './hashtag_bar';
import { RelativeTimestamp } from './relative_timestamp';
import StatusContent from './status_content';
const domParser = new DOMParser();
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']);
const spoilerText = status.getIn(['translation', 'spoiler_text']) || status.get('spoiler_text');
const contentHtml = status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
const contentText = domParser.parseFromString(contentHtml, 'text/html').documentElement.textContent;
const values = [
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
spoilerText && status.get('hidden') ? spoilerText : contentText,
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
status.getIn(['account', 'acct']),
];
if (rebloggedByText) {
values.push(rebloggedByText);
}
return values.join(', ');
};
export const defaultMediaVisibility = (status) => {
if (!status) {
return undefined;
}
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
status = status.get('reblog');
}
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
};
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' },
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle members only' },
personal_short: { id: 'privacy.personal.short', defaultMessage: 'Yourself only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
});
class CompactedStatus extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
status: ImmutablePropTypes.map,
previousId: PropTypes.string,
nextInReplyToId: PropTypes.string,
rootId: PropTypes.string,
onClick: PropTypes.func,
onOpenMedia: PropTypes.func,
onOpenVideo: PropTypes.func,
onHeightChange: PropTypes.func,
onToggleHidden: PropTypes.func,
onToggleCollapsed: PropTypes.func,
onTranslate: PropTypes.func,
onInteractionModal: PropTypes.func,
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
showThread: PropTypes.bool,
getScrollPosition: PropTypes.func,
updateScrollBottom: PropTypes.func,
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
};
// Avoid checking props that are functions (and whose equality will always
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
'muted',
'hidden',
'unread',
];
state = {
showMedia: defaultMediaVisibility(this.props.status),
statusId: undefined,
forceFilter: undefined,
};
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return {
showMedia: defaultMediaVisibility(nextProps.status),
statusId: nextProps.status.get('id'),
};
} else {
return null;
}
}
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
};
handleClick = e => {
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
return;
}
if (e) {
e.preventDefault();
}
this.handleHotkeyOpen();
};
handlePrependAccountClick = e => {
this.handleAccountClick(e, false);
};
handleAccountClick = (e, proper = true) => {
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
return;
}
if (e) {
e.preventDefault();
e.stopPropagation();
}
this._openProfile(proper);
};
handleExpandedToggle = () => {
this.props.onToggleHidden(this._properStatus());
};
handleCollapsedToggle = isCollapsed => {
this.props.onToggleCollapsed(this._properStatus(), isCollapsed);
};
handleTranslate = () => {
this.props.onTranslate(this._properStatus());
};
getAttachmentAspectRatio () {
const attachments = this._properStatus().get('media_attachments');
if (attachments.getIn([0, 'type']) === 'video') {
return `${attachments.getIn([0, 'meta', 'original', 'width'])} / ${attachments.getIn([0, 'meta', 'original', 'height'])}`;
} else if (attachments.getIn([0, 'type']) === 'audio') {
return '16 / 9';
} else {
return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2'
}
}
renderLoadingMediaGallery = () => {
return (
<div className='media-gallery' style={{ aspectRatio: this.getAttachmentAspectRatio() }} />
);
};
renderLoadingVideoPlayer = () => {
return (
<div className='video-player' style={{ aspectRatio: this.getAttachmentAspectRatio() }} />
);
};
renderLoadingAudioPlayer = () => {
return (
<div className='audio-player' style={{ aspectRatio: this.getAttachmentAspectRatio() }} />
);
};
handleOpenVideo = (options) => {
const status = this._properStatus();
const lang = status.getIn(['translation', 'language']) || status.get('language');
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, options);
};
handleOpenMedia = (media, index) => {
const status = this._properStatus();
const lang = status.getIn(['translation', 'language']) || status.get('language');
this.props.onOpenMedia(status.get('id'), media, index, lang);
};
handleHotkeyOpenMedia = e => {
const { onOpenMedia, onOpenVideo } = this.props;
const status = this._properStatus();
e.preventDefault();
if (status.get('media_attachments').size > 0) {
const lang = status.getIn(['translation', 'language']) || status.get('language');
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, { startTime: 0 });
} else {
onOpenMedia(status.get('id'), status.get('media_attachments'), 0, lang);
}
}
};
handleHotkeyOpen = () => {
if (this.props.onClick) {
this.props.onClick();
return;
}
const { router } = this.context;
const status = this._properStatus();
if (!router) {
return;
}
router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
};
handleHotkeyOpenProfile = () => {
this._openProfile();
};
_openProfile = (proper = true) => {
const { router } = this.context;
const status = proper ? this._properStatus() : this.props.status;
if (!router) {
return;
}
router.history.push(`/@${status.getIn(['account', 'acct'])}`);
};
handleHotkeyMoveUp = e => {
this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
};
handleHotkeyMoveDown = e => {
this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
};
handleHotkeyToggleHidden = () => {
this.props.onToggleHidden(this._properStatus());
};
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
};
_properStatus () {
const { status } = this.props;
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
return status.get('reblog');
} else {
return status;
}
}
handleRef = c => {
this.node = c;
};
render () {
const { intl, hidden, featured, unread, showThread, previousId, nextInReplyToId, rootId } = this.props;
let { status } = this.props;
if (status === null) {
return null;
}
const handlers = this.props.muted ? {} : {
open: this.handleHotkeyOpen,
openProfile: this.handleHotkeyOpenProfile,
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
};
let media, isCardMediaWithSensitive, prepend, rebloggedByText;
if (hidden) {
return (
<HotKeys handlers={handlers}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={0}>
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
<span>{status.get('content')}</span>
</div>
</HotKeys>
);
}
const connectUp = previousId && previousId === status.get('in_reply_to_id');
const connectToRoot = rootId && rootId === status.get('in_reply_to_id');
const connectReply = nextInReplyToId && nextInReplyToId === status.get('id');
if (showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id'])) {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='reply' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
</div>
);
}
if (status.get('quote_muted')) {
const minHandlers = {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
};
return (
<HotKeys handlers={minHandlers}>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div className='status__wrapper status__wrapper__compact status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef} onClick={this.handleClick}>
<FormattedMessage id='status.quote_filtered' defaultMessage='This quote is filtered because of muting, blocking or domain blocking' />
</div>
</HotKeys>
);
}
isCardMediaWithSensitive = false;
if (status.get('media_attachments').size > 0) {
const language = status.getIn(['translation', 'language']) || status.get('language');
if (this.props.muted) {
media = (
<AttachmentList
compact
media={status.get('media_attachments')}
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
media = (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
{Component => (
<Component
src={attachment.get('url')}
alt={description}
lang={language}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
width={this.props.cachedMediaWidth}
height={110}
cacheWidth={this.props.cacheMediaWidth}
sensitive={status.get('sensitive')}
blurhash={attachment.get('blurhash')}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => (
<Component
preview={attachment.get('preview_url')}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
alt={description}
lang={language}
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
} else {
media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
compact
media={status.get('media_attachments')}
lang={language}
sensitive={status.get('sensitive')}
height={110}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
}
} else if (status.get('card') && !this.props.muted) {
media = (
<Card
onOpenMedia={this.handleOpenMedia}
card={status.get('card')}
compact
sensitive={status.get('sensitive') && !status.get('spoiler_text')}
/>
);
isCardMediaWithSensitive = status.get('spoiler_text').length > 0;
}
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
return (
<HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', 'status__wrapper__compact', `status__wrapper-${status.get('visibility_ex')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
{prepend}
<div className={classNames('status', `status-${status.get('visibility_ex')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div onClick={this.handleClick} className='status__info'>
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
</a>
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar status__avatar__compact'>
<Avatar account={status.get('account')} size={24} inline />
</div>
<DisplayName account={status.get('account')} />
</a>
</div>
<StatusContent
status={status}
onClick={this.handleClick}
expanded={expanded}
onExpandedToggle={this.handleExpandedToggle}
onTranslate={this.handleTranslate}
collapsible
onCollapsedToggle={this.handleCollapsedToggle}
{...statusContentProps}
/>
{(!isCardMediaWithSensitive || !status.get('hidden')) && media}
{(!status.get('spoiler_text') || expanded) && hashtagBar}
</div>
</div>
</HotKeys>
);
}
}
export default injectIntl(CompactedStatus);

View file

@ -109,7 +109,7 @@ export function computeHashtagBarForStatus(status: StatusLike): {
const lastChild = template.content.lastChild; const lastChild = template.content.lastChild;
if (!lastChild) return defaultResult; if (!lastChild || lastChild.nodeType === Node.TEXT_NODE) return defaultResult;
template.content.removeChild(lastChild); template.content.removeChild(lastChild);
const contentWithoutLastLine = template; const contentWithoutLastLine = template;

View file

@ -236,6 +236,7 @@ class MediaGallery extends PureComponent {
visible: PropTypes.bool, visible: PropTypes.bool,
autoplay: PropTypes.bool, autoplay: PropTypes.bool,
onToggleVisibility: PropTypes.func, onToggleVisibility: PropTypes.func,
compact: PropTypes.bool,
}; };
state = { state = {
@ -306,7 +307,7 @@ class MediaGallery extends PureComponent {
} }
render () { render () {
const { media, lang, intl, sensitive, defaultWidth, autoplay } = this.props; const { media, lang, intl, sensitive, defaultWidth, autoplay, compact } = this.props;
const { visible } = this.state; const { visible } = this.state;
const width = this.state.width || defaultWidth; const width = this.state.width || defaultWidth;
@ -359,9 +360,10 @@ class MediaGallery extends PureComponent {
const columnClass = (size === 9) ? 'media-gallery--column3' : const columnClass = (size === 9) ? 'media-gallery--column3' :
(size === 10 || size === 11 || size === 12 || size === 13 || size === 14 || size === 15 || size === 16) ? 'media-gallery--column4' : (size === 10 || size === 11 || size === 12 || size === 13 || size === 14 || size === 15 || size === 16) ? 'media-gallery--column4' :
'media-gallery--column2'; 'media-gallery--column2';
const compactClass = compact ? 'media-gallery__compact' : null;
return ( return (
<div className={classNames('media-gallery', rowClass, columnClass)} style={style} ref={this.handleRef}> <div className={classNames('media-gallery', rowClass, columnClass, compactClass)} style={style} ref={this.handleRef}>
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}> <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}>
{spoilerButton} {spoilerButton}
</div> </div>

View file

@ -78,7 +78,7 @@ class ScrollableList extends PureComponent {
const clientHeight = this.getClientHeight(); const clientHeight = this.getClientHeight();
const offset = scrollHeight - scrollTop - clientHeight; const offset = scrollHeight - scrollTop - clientHeight;
if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) { if (scrollTop > 0 && offset < 400 && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
this.props.onLoadMore(); this.props.onLoadMore();
} }

View file

@ -13,12 +13,13 @@ import AttachmentList from 'mastodon/components/attachment_list';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import CompactedStatusContainer from '../containers/compacted_status_container'
import Card from '../features/status/components/card'; import Card from '../features/status/components/card';
// We use the component (and not the container) since we do not want // We use the component (and not the container) since we do not want
// to use the progress bar to show download progress // to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle'; import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { displayMedia, enableEmojiReaction, showEmojiReactionOnTimeline } from '../initial_state'; import { displayMedia, enableEmojiReaction, showEmojiReactionOnTimeline, showQuoteInHome, showQuoteInPublic } from '../initial_state';
import { Avatar } from './avatar'; import { Avatar } from './avatar';
import { AvatarOverlay } from './avatar_overlay'; import { AvatarOverlay } from './avatar_overlay';
@ -87,6 +88,7 @@ class Status extends ImmutablePureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
contextType: PropTypes.string,
previousId: PropTypes.string, previousId: PropTypes.string,
nextInReplyToId: PropTypes.string, nextInReplyToId: PropTypes.string,
rootId: PropTypes.string, rootId: PropTypes.string,
@ -357,15 +359,17 @@ class Status extends ImmutablePureComponent {
}; };
render () { render () {
const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId } = this.props; const { intl, hidden, featured, unread, muted, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId } = this.props;
let { status, account, ...other } = this.props; let { status, account, ...other } = this.props;
const contextType = (this.props.contextType || '').split(':')[0];
if (status === null) { if (status === null) {
return null; return null;
} }
const handlers = this.props.muted ? {} : { const handlers = muted ? {} : {
reply: this.handleHotkeyReply, reply: this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite, favourite: this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost, boost: this.handleHotkeyBoost,
@ -384,7 +388,7 @@ class Status extends ImmutablePureComponent {
if (hidden) { if (hidden) {
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={0}> <div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !muted })} tabIndex={0}>
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span> <span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
<span>{status.get('content')}</span> <span>{status.get('content')}</span>
</div> </div>
@ -412,25 +416,6 @@ class Status extends ImmutablePureComponent {
let visibilityIcon = visibilityIconInfo[status.get('limited_scope') || status.get('visibility_ex')] || visibilityIconInfo[status.get('visibility')]; let visibilityIcon = visibilityIconInfo[status.get('limited_scope') || status.get('visibility_ex')] || visibilityIconInfo[status.get('visibility')];
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
};
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
{' '}
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
</button>
</div>
</HotKeys>
);
}
if (featured) { if (featured) {
prepend = ( prepend = (
<div className='status__prepend'> <div className='status__prepend'>
@ -471,6 +456,63 @@ class Status extends ImmutablePureComponent {
); );
} }
if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={46} />;
} else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
}
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
const minHandlers = muted ? {} : {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
};
if (status.get('filter_action') === 'half_warn') {
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef}>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div onClick={this.handleClick} className='status__info'>
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
</a>
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'>
{statusAvatar}
</div>
<DisplayName account={status.get('account')} />
</a>
</div>
<div >
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
{' '}
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
</button>
</div>
</div>
</HotKeys>
);
}
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
{' '}
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
</button>
</div>
</HotKeys>
);
}
isCardMediaWithSensitive = false; isCardMediaWithSensitive = false;
if (pictureInPicture.get('inUse')) { if (pictureInPicture.get('inUse')) {
@ -478,7 +520,7 @@ class Status extends ImmutablePureComponent {
} else if (status.get('media_attachments').size > 0) { } else if (status.get('media_attachments').size > 0) {
const language = status.getIn(['translation', 'language']) || status.get('language'); const language = status.getIn(['translation', 'language']) || status.get('language');
if (this.props.muted) { if (muted) {
media = ( media = (
<AttachmentList <AttachmentList
compact compact
@ -556,7 +598,7 @@ class Status extends ImmutablePureComponent {
</Bundle> </Bundle>
); );
} }
} else if (status.get('card') && !this.props.muted) { } else if (status.get('card') && !muted) {
media = ( media = (
<Card <Card
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}
@ -568,12 +610,6 @@ class Status extends ImmutablePureComponent {
isCardMediaWithSensitive = status.get('spoiler_text').length > 0; isCardMediaWithSensitive = status.get('spoiler_text').length > 0;
} }
if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={46} />;
} else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
}
visibilityIcon = visibilityIconInfo[status.get('limited_scope') || status.get('visibility_ex')] || visibilityIconInfo[status.get('visibility')]; visibilityIcon = visibilityIconInfo[status.get('limited_scope') || status.get('visibility_ex')] || visibilityIconInfo[status.get('visibility')];
let emojiReactionsBar = null; let emojiReactionsBar = null;
@ -588,20 +624,24 @@ class Status extends ImmutablePureComponent {
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0; const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
const withLimited = status.get('visibility_ex') === 'limited' && status.get('limited_scope') ? <span className='status__visibility-icon'><Icon id='get-pocket' title='Limited' /></span> : null; const withLimited = status.get('visibility_ex') === 'limited' && status.get('limited_scope') ? <span className='status__visibility-icon'><Icon id='get-pocket' title='Limited' /></span> : null;
const withReference = status.get('status_references_count') > 0 ? <span className='status__visibility-icon'><Icon id='link' title='Reference' /></span> : null; const withQuote = status.get('quote_id') ? <span className='status__visibility-icon'><Icon id='quote-right' title='Quote' /></span> : null;
const withReference = (!withQuote && status.get('status_references_count') > 0) ? <span className='status__visibility-icon'><Icon id='link' title='Reference' /></span> : null;
const withExpiration = status.get('expires_at') ? <span className='status__visibility-icon'><Icon id='clock-o' title='Expiration' /></span> : null; const withExpiration = status.get('expires_at') ? <span className='status__visibility-icon'><Icon id='clock-o' title='Expiration' /></span> : null;
const quote = !muted && status.get('quote_id') && (['public', 'community'].includes(contextType) ? showQuoteInPublic : showQuoteInHome) && <CompactedStatusContainer id={status.get('quote_id')} />
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility_ex')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}> <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility_ex')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !muted })} tabIndex={muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
{prepend} {prepend}
<div className={classNames('status', `status-${status.get('visibility_ex')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}> <div className={classNames('status', `status-${status.get('visibility_ex')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: muted })} data-id={status.get('id')}>
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />} {(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div onClick={this.handleClick} className='status__info'> <div onClick={this.handleClick} className='status__info'>
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'> <a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
{withQuote}
{withReference} {withReference}
{withExpiration} {withExpiration}
{withLimited} {withLimited}
@ -629,6 +669,8 @@ class Status extends ImmutablePureComponent {
{...statusContentProps} {...statusContentProps}
/> />
{(!status.get('spoiler_text') || expanded) && quote}
{(!isCardMediaWithSensitive || !status.get('hidden')) && media} {(!isCardMediaWithSensitive || !status.get('hidden')) && media}
{(!status.get('spoiler_text') || expanded) && hashtagBar} {(!status.get('spoiler_text') || expanded) && hashtagBar}

View file

@ -298,6 +298,7 @@ class StatusActionBar extends ImmutablePureComponent {
const account = status.get('account'); const account = status.get('account');
const writtenByMe = status.getIn(['account', 'id']) === me; const writtenByMe = status.getIn(['account', 'id']) === me;
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']); const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
const allowQuote = status.getIn(['account', 'other_settings', 'allow_quote']);
let menu = []; let menu = [];
@ -332,7 +333,10 @@ class StatusActionBar extends ImmutablePureComponent {
if (publicStatus) { if (publicStatus) {
menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference }); menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote });
if (allowQuote) {
menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote });
}
} }
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClickOriginal }); menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClickOriginal });

View file

@ -0,0 +1,78 @@
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { openModal } from '../actions/modal';
import {
hideStatus,
revealStatus,
toggleStatusCollapse,
translateStatus,
undoStatusTranslation,
} from '../actions/statuses';
import CompactedStatus from '../components/compacted_status';
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
const mapStateToProps = (state, props) => ({
status: getStatus(state, props),
nextInReplyToId: props.nextId ? state.getIn(['statuses', props.nextId, 'in_reply_to_id']) : null,
pictureInPicture: getPictureInPicture(state, props),
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch) => ({
onTranslate (status) {
if (status.get('translation')) {
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
} else {
dispatch(translateStatus(status.get('id')));
}
},
onOpenMedia (statusId, media, index, lang) {
dispatch(openModal({
modalType: 'MEDIA',
modalProps: { statusId, media, index, lang },
}));
},
onOpenVideo (statusId, media, lang, options) {
dispatch(openModal({
modalType: 'VIDEO',
modalProps: { statusId, media, lang, options },
}));
},
onToggleHidden (status) {
if (status.get('hidden')) {
dispatch(revealStatus(status.get('id')));
} else {
dispatch(hideStatus(status.get('id')));
}
},
onToggleCollapsed (status, isCollapsed) {
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
},
onInteractionModal (type, status) {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type,
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(CompactedStatus));

View file

@ -80,6 +80,8 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
contextType,
onReply (status, router) { onReply (status, router) {
dispatch((_, getState) => { dispatch((_, getState) => {
let state = getState(); let state = getState();

View file

@ -15,6 +15,8 @@ import { IconButton } from '../../../components/icon_button';
const messages = defineMessages({ const messages = defineMessages({
public_short: { id: 'searchability.public.short', defaultMessage: 'Public' }, public_short: { id: 'searchability.public.short', defaultMessage: 'Public' },
public_long: { id: 'searchability.public.long', defaultMessage: 'Anyone can find' }, public_long: { id: 'searchability.public.long', defaultMessage: 'Anyone can find' },
public_unlisted_short: { id: 'searchability.public_unlisted.short', defaultMessage: 'Public unlisted' },
public_unlisted_long: { id: 'searchability.public_unlisted.long', defaultMessage: 'Local users and followers can find' },
private_short: { id: 'searchability.unlisted.short', defaultMessage: 'Followers' }, private_short: { id: 'searchability.unlisted.short', defaultMessage: 'Followers' },
private_long: { id: 'searchability.unlisted.long', defaultMessage: 'Your followers can find' }, private_long: { id: 'searchability.unlisted.long', defaultMessage: 'Your followers can find' },
direct_short: { id: 'searchability.private.short', defaultMessage: 'Reactionners' }, direct_short: { id: 'searchability.private.short', defaultMessage: 'Reactionners' },
@ -223,6 +225,7 @@ class SearchabilityDropdown extends PureComponent {
this.options = [ this.options = [
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
{ icon: 'cloud', value: 'public_unlisted', text: formatMessage(messages.public_unlisted_short), meta: formatMessage(messages.public_unlisted_long) },
{ icon: 'unlock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, { icon: 'unlock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
{ icon: 'lock', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, { icon: 'lock', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
{ icon: 'at', value: 'limited', text: formatMessage(messages.limited_short), meta: formatMessage(messages.limited_long) }, { icon: 'at', value: 'limited', text: formatMessage(messages.limited_short), meta: formatMessage(messages.limited_long) },

View file

@ -236,6 +236,7 @@ class ActionBar extends PureComponent {
const account = status.get('account'); const account = status.get('account');
const writtenByMe = status.getIn(['account', 'id']) === me; const writtenByMe = status.getIn(['account', 'id']) === me;
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']); const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
const allowQuote = status.getIn(['account', 'other_settings', 'allow_quote']);
let menu = []; let menu = [];
@ -259,7 +260,10 @@ class ActionBar extends PureComponent {
if (publicStatus) { if (publicStatus) {
menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference }); menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote });
if (allowQuote) {
menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote });
}
} }
menu.push({ text: intl.formatMessage(messages.bookmark_category), action: this.handleBookmarkCategoryAdderClick }); menu.push({ text: intl.formatMessage(messages.bookmark_category), action: this.handleBookmarkCategoryAdderClick });

View file

@ -38,6 +38,7 @@ const messages = defineMessages({
personal_short: { id: 'privacy.personal.short', defaultMessage: 'Yourself only' }, personal_short: { id: 'privacy.personal.short', defaultMessage: 'Yourself only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
searchability_public_short: { id: 'searchability.public.short', defaultMessage: 'Public' }, searchability_public_short: { id: 'searchability.public.short', defaultMessage: 'Public' },
searchability_public_unlisted_short: { id: 'searchability.public_unlisted.short', defaultMessage: 'Public unlisted' },
searchability_private_short: { id: 'searchability.unlisted.short', defaultMessage: 'Followers' }, searchability_private_short: { id: 'searchability.unlisted.short', defaultMessage: 'Followers' },
searchability_direct_short: { id: 'searchability.private.short', defaultMessage: 'Reactionners' }, searchability_direct_short: { id: 'searchability.private.short', defaultMessage: 'Reactionners' },
searchability_limited_short: { id: 'searchability.direct.short', defaultMessage: 'Self only' }, searchability_limited_short: { id: 'searchability.direct.short', defaultMessage: 'Self only' },
@ -270,6 +271,7 @@ class DetailedStatus extends ImmutablePureComponent {
const searchabilityIconInfo = { const searchabilityIconInfo = {
'public': { icon: 'globe', text: intl.formatMessage(messages.searchability_public_short) }, 'public': { icon: 'globe', text: intl.formatMessage(messages.searchability_public_short) },
'public_unlisted': { icon: 'cloud', text: intl.formatMessage(messages.searchability_public_unlisted_short) },
'private': { icon: 'unlock', text: intl.formatMessage(messages.searchability_private_short) }, 'private': { icon: 'unlock', text: intl.formatMessage(messages.searchability_private_short) },
'direct': { icon: 'lock', text: intl.formatMessage(messages.searchability_direct_short) }, 'direct': { icon: 'lock', text: intl.formatMessage(messages.searchability_direct_short) },
'limited': { icon: 'at', text: intl.formatMessage(messages.searchability_limited_short) }, 'limited': { icon: 'at', text: intl.formatMessage(messages.searchability_limited_short) },

View file

@ -233,6 +233,8 @@ class Status extends ImmutablePureComponent {
componentDidMount () { componentDidMount () {
attachFullscreenListener(this.onFullScreenChange); attachFullscreenListener(this.onFullScreenChange);
this._scrollStatusIntoView();
} }
UNSAFE_componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
@ -638,10 +640,10 @@ class Status extends ImmutablePureComponent {
this.node = c; this.node = c;
}; };
componentDidUpdate (prevProps) { _scrollStatusIntoView () {
const { status, ancestorsIds, multiColumn } = this.props; const { status, multiColumn } = this.props;
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) { if (status) {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true); this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true);
@ -658,6 +660,14 @@ class Status extends ImmutablePureComponent {
} }
} }
componentDidUpdate (prevProps) {
const { status, ancestorsIds } = this.props;
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
this._scrollStatusIntoView();
}
}
componentWillUnmount () { componentWillUnmount () {
detachFullscreenListener(this.onFullScreenChange); detachFullscreenListener(this.onFullScreenChange);
} }
@ -666,6 +676,22 @@ class Status extends ImmutablePureComponent {
this.setState({ fullscreen: isFullscreen() }); this.setState({ fullscreen: isFullscreen() });
}; };
shouldUpdateScroll = (prevRouterProps, { location }) => {
// Do not change scroll when opening a modal
if (location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey) {
return false;
}
// Scroll to focused post if it is loaded
const child = this.node?.querySelector('.detailed-status__wrapper');
if (child) {
return [0, child.offsetTop];
}
// Do not scroll otherwise, `componentDidUpdate` will take care of that
return false;
};
render () { render () {
let ancestors, descendants, references; let ancestors, descendants, references;
const { isLoading, status, ancestorsIds, descendantsIds, referenceIds, intl, domain, multiColumn, pictureInPicture } = this.props; const { isLoading, status, ancestorsIds, descendantsIds, referenceIds, intl, domain, multiColumn, pictureInPicture } = this.props;
@ -723,7 +749,7 @@ class Status extends ImmutablePureComponent {
)} )}
/> />
<ScrollContainer scrollKey='thread'> <ScrollContainer scrollKey='thread' shouldUpdateScroll={this.shouldUpdateScroll}>
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}> <div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
{references} {references}
{ancestors} {ancestors}

View file

@ -64,6 +64,7 @@
* @property {boolean} enable_local_privacy * @property {boolean} enable_local_privacy
* @property {boolean} enable_dtl_menu * @property {boolean} enable_dtl_menu
* @property {boolean=} expand_spoilers * @property {boolean=} expand_spoilers
* @property {boolean} hide_blocking_quote
* @property {boolean} hide_recent_emojis * @property {boolean} hide_recent_emojis
* @property {boolean} limited_federation_mode * @property {boolean} limited_federation_mode
* @property {string} locale * @property {string} locale
@ -78,6 +79,8 @@
* @property {boolean} search_enabled * @property {boolean} search_enabled
* @property {boolean} trends_enabled * @property {boolean} trends_enabled
* @property {boolean} show_emoji_reaction_on_timeline * @property {boolean} show_emoji_reaction_on_timeline
* @property {boolean} show_quote_in_home
* @property {boolean} show_quote_in_public
* @property {string} simple_timeline_menu * @property {string} simple_timeline_menu
* @property {boolean} single_user_mode * @property {boolean} single_user_mode
* @property {string} source_url * @property {string} source_url
@ -136,6 +139,7 @@ export const enableLoginPrivacy = getMeta('enable_login_privacy');
export const enableDtlMenu = getMeta('enable_dtl_menu'); export const enableDtlMenu = getMeta('enable_dtl_menu');
export const expandSpoilers = getMeta('expand_spoilers'); export const expandSpoilers = getMeta('expand_spoilers');
export const forceSingleColumn = !getMeta('advanced_layout'); export const forceSingleColumn = !getMeta('advanced_layout');
export const hideBlockingQuote = getMeta('hide_blocking_quote');
export const hideRecentEmojis = getMeta('hide_recent_emojis'); export const hideRecentEmojis = getMeta('hide_recent_emojis');
export const limitedFederationMode = getMeta('limited_federation_mode'); export const limitedFederationMode = getMeta('limited_federation_mode');
export const mascot = getMeta('mascot'); export const mascot = getMeta('mascot');
@ -149,6 +153,8 @@ export const repository = getMeta('repository');
export const searchEnabled = getMeta('search_enabled'); export const searchEnabled = getMeta('search_enabled');
export const trendsEnabled = getMeta('trends_enabled'); export const trendsEnabled = getMeta('trends_enabled');
export const showEmojiReactionOnTimeline = getMeta('show_emoji_reaction_on_timeline'); export const showEmojiReactionOnTimeline = getMeta('show_emoji_reaction_on_timeline');
export const showQuoteInHome = getMeta('show_quote_in_home');
export const showQuoteInPublic = getMeta('show_quote_in_public');
export const showTrends = getMeta('show_trends'); export const showTrends = getMeta('show_trends');
export const simpleTimelineMenu = getMeta('simple_timeline_menu'); export const simpleTimelineMenu = getMeta('simple_timeline_menu');
export const singleUserMode = getMeta('single_user_mode'); export const singleUserMode = getMeta('single_user_mode');

View file

@ -190,6 +190,7 @@
"conversation.open": "কথপোকথন দেখান", "conversation.open": "কথপোকথন দেখান",
"conversation.with": "{names} এর সঙ্গে", "conversation.with": "{names} এর সঙ্গে",
"copypaste.copied": "অনুলিপিকৃত", "copypaste.copied": "অনুলিপিকৃত",
"copypaste.copy_to_clipboard": "ক্লিপবোর্ডে কপি করুন",
"directory.federated": "পরিচিত ফেডিভারসের থেকে", "directory.federated": "পরিচিত ফেডিভারসের থেকে",
"directory.local": "শুধু {domain} থেকে", "directory.local": "শুধু {domain} থেকে",
"directory.new_arrivals": "নতুন আগত", "directory.new_arrivals": "নতুন আগত",

View file

@ -629,6 +629,8 @@
"searchability.private.short": "Reactionners", "searchability.private.short": "Reactionners",
"searchability.public.long": "Anyone can find", "searchability.public.long": "Anyone can find",
"searchability.public.short": "Everyone", "searchability.public.short": "Everyone",
"searchability.public_unlisted.long": "Local users and followers can find",
"searchability.public_unlisted.short": "Local and followers",
"searchability.unlisted.long": "Your followers and reactionners can find", "searchability.unlisted.long": "Your followers and reactionners can find",
"searchability.unlisted.short": "Followers and reactionners", "searchability.unlisted.short": "Followers and reactionners",
"search_popout.domain": "domain", "search_popout.domain": "domain",
@ -692,7 +694,7 @@
"status.open": "Expand this post", "status.open": "Expand this post",
"status.pin": "Pin on profile", "status.pin": "Pin on profile",
"status.pinned": "Pinned post", "status.pinned": "Pinned post",
"status.quote": "Ref (quote in other servers)", "status.quote": "Quote",
"status.read_more": "Read more", "status.read_more": "Read more",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility", "status.reblog_private": "Boost with original visibility",

View file

@ -686,7 +686,7 @@
"timeline_hint.resources.followers": "Seuraajat", "timeline_hint.resources.followers": "Seuraajat",
"timeline_hint.resources.follows": "seurattua", "timeline_hint.resources.follows": "seurattua",
"timeline_hint.resources.statuses": "Vanhemmat julkaisut", "timeline_hint.resources.statuses": "Vanhemmat julkaisut",
"trends.counter_by_accounts": "{count, plural, one {{counter} henkilö} other {{counter} henkilöä}} viimeisten {days, plural, one {päivän} other {{days} päivän}}", "trends.counter_by_accounts": "{count, plural, one {{counter} henkilö} other {{counter} henkilöä}} {days, plural, one {viimeisen päivän} other {viimeisten {days} päivän}} aikana",
"trends.trending_now": "Suosittua nyt", "trends.trending_now": "Suosittua nyt",
"ui.beforeunload": "Luonnos häviää, jos poistut Mastodonista.", "ui.beforeunload": "Luonnos häviää, jos poistut Mastodonista.",
"units.short.billion": "{count} mrd.", "units.short.billion": "{count} mrd.",

View file

@ -714,6 +714,8 @@
"searchability.private.short": "反応者のみ", "searchability.private.short": "反応者のみ",
"searchability.public.long": "この投稿は誰でも検索できます", "searchability.public.long": "この投稿は誰でも検索できます",
"searchability.public.short": "誰でも", "searchability.public.short": "誰でも",
"searchability.public_unlisted.long": "ローカルユーザーとフォロワーが検索できます",
"searchability.public_unlisted.short": "ローカルとフォロワー",
"searchability.unlisted.long": "この投稿はあなたのフォロワーと反応者だけが検索できます", "searchability.unlisted.long": "この投稿はあなたのフォロワーと反応者だけが検索できます",
"searchability.unlisted.short": "フォロワーと反応者", "searchability.unlisted.short": "フォロワーと反応者",
"search_popout.domain": "ドメイン", "search_popout.domain": "ドメイン",
@ -778,7 +780,7 @@
"status.open": "詳細を表示", "status.open": "詳細を表示",
"status.pin": "プロフィールに固定表示", "status.pin": "プロフィールに固定表示",
"status.pinned": "固定された投稿", "status.pinned": "固定された投稿",
"status.quote": "参照 (他サーバーで引用扱い)", "status.quote": "引用",
"status.read_more": "もっと見る", "status.read_more": "もっと見る",
"status.reblog": "ブースト", "status.reblog": "ブースト",
"status.reblog_private": "ブースト", "status.reblog_private": "ブースト",

View file

@ -102,7 +102,7 @@
"bundle_modal_error.message": "컴포넌트를 불러오는 중 문제가 발생했습니다.", "bundle_modal_error.message": "컴포넌트를 불러오는 중 문제가 발생했습니다.",
"bundle_modal_error.retry": "다시 시도", "bundle_modal_error.retry": "다시 시도",
"closed_registrations.other_server_instructions": "마스토돈은 분산화 되어 있기 때문에, 다른 서버에서 계정을 만들더라도 이 서버와 상호작용 할 수 있습니다.", "closed_registrations.other_server_instructions": "마스토돈은 분산화 되어 있기 때문에, 다른 서버에서 계정을 만들더라도 이 서버와 상호작용 할 수 있습니다.",
"closed_registrations_modal.description": "{domain}은 현재 가입이 막혀있는 상태입니다, 만약 마스토돈을 이용하기 위해 꼭 {domain}을 사용할 필요는 없다는 사실을 인지해 두세요.", "closed_registrations_modal.description": "{domain}은 현재 가입이 막혀있는 상태입니다, 마스토돈을 이용하기 위해 꼭 {domain}을 사용할 필요는 없다는 사실을 인지해 두세요.",
"closed_registrations_modal.find_another_server": "다른 서버 찾기", "closed_registrations_modal.find_another_server": "다른 서버 찾기",
"closed_registrations_modal.preamble": "마스토돈은 분산화 되어 있습니다, 그렇기 때문에 어디에서 계정을 생성하든, 이 서버에 있는 누구와도 팔로우와 상호작용을 할 수 있습니다. 심지어는 스스로 서버를 만드는 것도 가능합니다!", "closed_registrations_modal.preamble": "마스토돈은 분산화 되어 있습니다, 그렇기 때문에 어디에서 계정을 생성하든, 이 서버에 있는 누구와도 팔로우와 상호작용을 할 수 있습니다. 심지어는 스스로 서버를 만드는 것도 가능합니다!",
"closed_registrations_modal.title": "마스토돈에서 가입", "closed_registrations_modal.title": "마스토돈에서 가입",

View file

@ -161,13 +161,13 @@
"compose_form.spoiler.unmarked": "Pievienot satura brīdinājumu", "compose_form.spoiler.unmarked": "Pievienot satura brīdinājumu",
"compose_form.spoiler_placeholder": "Ieraksti savu brīdinājumu šeit", "compose_form.spoiler_placeholder": "Ieraksti savu brīdinājumu šeit",
"confirmation_modal.cancel": "Atcelt", "confirmation_modal.cancel": "Atcelt",
"confirmations.block.block_and_report": "Bloķēt un Ziņot", "confirmations.block.block_and_report": "Bloķēt un ziņot",
"confirmations.block.confirm": "Bloķēt", "confirmations.block.confirm": "Bloķēt",
"confirmations.block.message": "Vai tiešām vēlies bloķēt {name}?", "confirmations.block.message": "Vai tiešām vēlies bloķēt {name}?",
"confirmations.cancel_follow_request.confirm": "Atsaukt pieprasījumu", "confirmations.cancel_follow_request.confirm": "Atsaukt pieprasījumu",
"confirmations.cancel_follow_request.message": "Vai tiešām vēlies atsaukt pieprasījumu sekot {name}?", "confirmations.cancel_follow_request.message": "Vai tiešām vēlies atsaukt pieprasījumu sekot {name}?",
"confirmations.delete.confirm": "Dzēst", "confirmations.delete.confirm": "Dzēst",
"confirmations.delete.message": "Vai tiešām vēlies dzēst šo ziņu?", "confirmations.delete.message": "Vai tiešām vēlies dzēst šo ierakstu?",
"confirmations.delete_list.confirm": "Dzēst", "confirmations.delete_list.confirm": "Dzēst",
"confirmations.delete_list.message": "Vai tiešam vēlies neatgriezeniski dzēst šo sarakstu?", "confirmations.delete_list.message": "Vai tiešam vēlies neatgriezeniski dzēst šo sarakstu?",
"confirmations.discard_edit_media.confirm": "Atmest", "confirmations.discard_edit_media.confirm": "Atmest",
@ -244,7 +244,7 @@
"empty_column.public": "Šeit vēl nekā nav! Ieraksti ko publiski vai pieseko lietotājiem no citiem serveriem", "empty_column.public": "Šeit vēl nekā nav! Ieraksti ko publiski vai pieseko lietotājiem no citiem serveriem",
"error.unexpected_crash.explanation": "Koda kļūdas vai pārlūkprogrammas saderības problēmas dēļ šo lapu nevarēja parādīt pareizi.", "error.unexpected_crash.explanation": "Koda kļūdas vai pārlūkprogrammas saderības problēmas dēļ šo lapu nevarēja parādīt pareizi.",
"error.unexpected_crash.explanation_addons": "Šo lapu nevarēja parādīt pareizi. Šo kļūdu, iespējams, izraisīja pārlūkprogrammas papildinājums vai automātiskās tulkošanas rīki.", "error.unexpected_crash.explanation_addons": "Šo lapu nevarēja parādīt pareizi. Šo kļūdu, iespējams, izraisīja pārlūkprogrammas papildinājums vai automātiskās tulkošanas rīki.",
"error.unexpected_crash.next_steps": "Mēģini atsvaidzināt lapu. Ja tas nepalīdz, vari lietot Mastodon, izmantojot citu pārlūkprogrammu vai lietotni.", "error.unexpected_crash.next_steps": "Mēģini atsvaidzināt lapu. Ja tas nepalīdz, iespējams, varēsi lietot Mastodon, izmantojot citu pārlūkprogrammu vai lietotni.",
"error.unexpected_crash.next_steps_addons": "Mēģini tos atspējot un atsvaidzināt lapu. Ja tas nepalīdz, iespējams, varēsi lietot Mastodon, izmantojot citu pārlūkprogrammu vai lietotni.", "error.unexpected_crash.next_steps_addons": "Mēģini tos atspējot un atsvaidzināt lapu. Ja tas nepalīdz, iespējams, varēsi lietot Mastodon, izmantojot citu pārlūkprogrammu vai lietotni.",
"errors.unexpected_crash.copy_stacktrace": "Kopēt stacktrace uz starpliktuvi", "errors.unexpected_crash.copy_stacktrace": "Kopēt stacktrace uz starpliktuvi",
"errors.unexpected_crash.report_issue": "Ziņot par problēmu", "errors.unexpected_crash.report_issue": "Ziņot par problēmu",
@ -309,11 +309,11 @@
"home.column_settings.show_replies": "Rādīt atbildes", "home.column_settings.show_replies": "Rādīt atbildes",
"home.explore_prompt.body": "Tavā mājas plūsmā būs dažādu ziņu sajaukums no atsaucēm, kurām esi izvēlējies sekot, personām, kurām esi izvēlējies sekot, un ziņām, kuras tās izceļ. Ja tas šķiet pārāk kluss, iespējams, vēlēsies:", "home.explore_prompt.body": "Tavā mājas plūsmā būs dažādu ziņu sajaukums no atsaucēm, kurām esi izvēlējies sekot, personām, kurām esi izvēlējies sekot, un ziņām, kuras tās izceļ. Ja tas šķiet pārāk kluss, iespējams, vēlēsies:",
"home.explore_prompt.title": "Šī ir tava Mastodon mājvieta.", "home.explore_prompt.title": "Šī ir tava Mastodon mājvieta.",
"home.hide_announcements": "Slēpt anonsus", "home.hide_announcements": "Slēpt paziņojumus",
"home.pending_critical_update.body": "Lūdzu, pēc iespējas ātrāk atjaunini savu Mastodon serveri!", "home.pending_critical_update.body": "Lūdzu, pēc iespējas ātrāk atjaunini savu Mastodon serveri!",
"home.pending_critical_update.link": "Skatīt jauninājumus", "home.pending_critical_update.link": "Skatīt jauninājumus",
"home.pending_critical_update.title": "Pieejams kritisks drošības jauninājums!", "home.pending_critical_update.title": "Pieejams kritisks drošības jauninājums!",
"home.show_announcements": "Rādīt anonsus", "home.show_announcements": "Rādīt paziņojumus",
"interaction_modal.description.favourite": "Ar Mastodon kontu tu vari pievienot šo ziņu izlasei, lai informētu autoru, ka to novērtē, un saglabātu to vēlākai lasīšanai.", "interaction_modal.description.favourite": "Ar Mastodon kontu tu vari pievienot šo ziņu izlasei, lai informētu autoru, ka to novērtē, un saglabātu to vēlākai lasīšanai.",
"interaction_modal.description.follow": "Ar Mastodon kontu tu vari sekot {name}, lai saņemtu viņu ziņas savā mājas plūsmā.", "interaction_modal.description.follow": "Ar Mastodon kontu tu vari sekot {name}, lai saņemtu viņu ziņas savā mājas plūsmā.",
"interaction_modal.description.reblog": "Izmantojot kontu Mastodon, tu vari izcelt šo ziņu, lai kopīgotu to ar saviem sekotājiem.", "interaction_modal.description.reblog": "Izmantojot kontu Mastodon, tu vari izcelt šo ziņu, lai kopīgotu to ar saviem sekotājiem.",
@ -369,7 +369,7 @@
"lightbox.close": "Aizvērt", "lightbox.close": "Aizvērt",
"lightbox.compress": "Saspiest attēla skata lodziņu", "lightbox.compress": "Saspiest attēla skata lodziņu",
"lightbox.expand": "Izvērst attēla skata lodziņu", "lightbox.expand": "Izvērst attēla skata lodziņu",
"lightbox.next": "Nākamais", "lightbox.next": "Tālāk",
"lightbox.previous": "Iepriekšējais", "lightbox.previous": "Iepriekšējais",
"limited_account_hint.action": "Tik un tā rādīt profilu", "limited_account_hint.action": "Tik un tā rādīt profilu",
"limited_account_hint.title": "{domain} moderatori ir paslēpuši šo profilu.", "limited_account_hint.title": "{domain} moderatori ir paslēpuši šo profilu.",
@ -422,8 +422,8 @@
"navigation_bar.search": "Meklēt", "navigation_bar.search": "Meklēt",
"navigation_bar.security": "Drošība", "navigation_bar.security": "Drošība",
"not_signed_in_indicator.not_signed_in": "Lai piekļūtu šim resursam, tev ir jāpierakstās.", "not_signed_in_indicator.not_signed_in": "Lai piekļūtu šim resursam, tev ir jāpierakstās.",
"notification.admin.report": "{name} sūdzējās par {target}", "notification.admin.report": "{name} ziņoja par {target}",
"notification.admin.sign_up": "{name} pierakstījās", "notification.admin.sign_up": "{name} ir pierakstījies",
"notification.favourite": "{name} pievienoja tavu ziņu izlasei", "notification.favourite": "{name} pievienoja tavu ziņu izlasei",
"notification.follow": "{name} uzsāka tev sekot", "notification.follow": "{name} uzsāka tev sekot",
"notification.follow_request": "{name} nosūtīja tev sekošanas pieprasījumu", "notification.follow_request": "{name} nosūtīja tev sekošanas pieprasījumu",
@ -435,7 +435,7 @@
"notification.update": "{name} rediģēja ierakstu", "notification.update": "{name} rediģēja ierakstu",
"notifications.clear": "Notīrīt paziņojumus", "notifications.clear": "Notīrīt paziņojumus",
"notifications.clear_confirmation": "Vai tiešām vēlies neatgriezeniski notīrīt visus savus paziņojumus?", "notifications.clear_confirmation": "Vai tiešām vēlies neatgriezeniski notīrīt visus savus paziņojumus?",
"notifications.column_settings.admin.report": "Jaunas sūdzības:", "notifications.column_settings.admin.report": "Jauni ziņojumi:",
"notifications.column_settings.admin.sign_up": "Jaunas pierakstīšanās:", "notifications.column_settings.admin.sign_up": "Jaunas pierakstīšanās:",
"notifications.column_settings.alert": "Darbvirsmas paziņojumi", "notifications.column_settings.alert": "Darbvirsmas paziņojumi",
"notifications.column_settings.favourite": "Izlase:", "notifications.column_settings.favourite": "Izlase:",
@ -445,7 +445,7 @@
"notifications.column_settings.follow": "Jauni sekotāji:", "notifications.column_settings.follow": "Jauni sekotāji:",
"notifications.column_settings.follow_request": "Jauni sekošanas pieprasījumi:", "notifications.column_settings.follow_request": "Jauni sekošanas pieprasījumi:",
"notifications.column_settings.mention": "Pieminējumi:", "notifications.column_settings.mention": "Pieminējumi:",
"notifications.column_settings.poll": "Aptauju rezultāti:", "notifications.column_settings.poll": "Aptaujas rezultāti:",
"notifications.column_settings.push": "Uznirstošie paziņojumi", "notifications.column_settings.push": "Uznirstošie paziņojumi",
"notifications.column_settings.reblog": "Pastiprinātie ieraksti:", "notifications.column_settings.reblog": "Pastiprinātie ieraksti:",
"notifications.column_settings.show": "Rādīt kolonnā", "notifications.column_settings.show": "Rādīt kolonnā",
@ -457,13 +457,13 @@
"notifications.filter.all": "Visi", "notifications.filter.all": "Visi",
"notifications.filter.boosts": "Pastiprinātie ieraksti", "notifications.filter.boosts": "Pastiprinātie ieraksti",
"notifications.filter.favourites": "Izlases", "notifications.filter.favourites": "Izlases",
"notifications.filter.follows": "Sekošana", "notifications.filter.follows": "Seko",
"notifications.filter.mentions": "Pieminējumi", "notifications.filter.mentions": "Pieminējumi",
"notifications.filter.polls": "Aptauju rezultāti", "notifications.filter.polls": "Aptaujas rezultāti",
"notifications.filter.statuses": "Jaunumi no cilvēkiem, kuriem tu seko", "notifications.filter.statuses": "Jaunumi no cilvēkiem, kuriem tu seko",
"notifications.grant_permission": "Piešķirt atļauju.", "notifications.grant_permission": "Piešķirt atļauju.",
"notifications.group": "{count} paziņojumi", "notifications.group": "{count} paziņojumi",
"notifications.mark_as_read": "Atzīmēt visus paziņojumus kā izlasītus", "notifications.mark_as_read": "Atzīmēt katru paziņojumu kā izlasītu",
"notifications.permission_denied": "Darbvirsmas paziņojumi nav pieejami, jo iepriekš tika noraidīts pārlūka atļauju pieprasījums", "notifications.permission_denied": "Darbvirsmas paziņojumi nav pieejami, jo iepriekš tika noraidīts pārlūka atļauju pieprasījums",
"notifications.permission_denied_alert": "Darbvirsmas paziņojumus nevar iespējot, jo pārlūkprogrammai atļauja tika iepriekš atteikta", "notifications.permission_denied_alert": "Darbvirsmas paziņojumus nevar iespējot, jo pārlūkprogrammai atļauja tika iepriekš atteikta",
"notifications.permission_required": "Darbvirsmas paziņojumi nav pieejami, jo nav piešķirta nepieciešamā atļauja.", "notifications.permission_required": "Darbvirsmas paziņojumi nav pieejami, jo nav piešķirta nepieciešamā atļauja.",
@ -563,25 +563,25 @@
"report.reasons.spam": "Tas ir spams", "report.reasons.spam": "Tas ir spams",
"report.reasons.spam_description": "Ļaunprātīgas saites, viltus iesaistīšana vai atkārtotas atbildes", "report.reasons.spam_description": "Ļaunprātīgas saites, viltus iesaistīšana vai atkārtotas atbildes",
"report.reasons.violation": "Tas pārkāpj servera noteikumus", "report.reasons.violation": "Tas pārkāpj servera noteikumus",
"report.reasons.violation_description": "Tu zini, ka tas pārkāpj konkrētus noteikumus", "report.reasons.violation_description": "Tu zini, ka tas pārkāpj īpašus noteikumus",
"report.rules.subtitle": "Atlasi visus atbilstošos", "report.rules.subtitle": "Atlasi visus atbilstošos",
"report.rules.title": "Kuri noteikumi tiek pārkāpti?", "report.rules.title": "Kuri noteikumi tiek pārkāpti?",
"report.statuses.subtitle": "Atlasi visus atbilstošos", "report.statuses.subtitle": "Atlasi visus atbilstošos",
"report.statuses.title": "Vai ir kādi ieraksti, kas atbalsta šo sūdzību?", "report.statuses.title": "Vai ir kādi ieraksti, kas atbalsta šo sūdzību?",
"report.submit": "Iesniegt", "report.submit": "Iesniegt",
"report.target": "Sūdzība par {target}", "report.target": "Ziņošana par: {target}",
"report.thanks.take_action": "Vari veikt šīs darbības, lai kontrolētu Mastodon redzamo saturu:", "report.thanks.take_action": "Tālāk ir norādītas iespējas, kā kontrolēt Mastodon redzamo saturu:",
"report.thanks.take_action_actionable": "Kamēr mēs to izskatām, tu vari veikt darbības pret @{name}:", "report.thanks.take_action_actionable": "Kamēr mēs to izskatām, tu vari veikt darbības pret @{name}:",
"report.thanks.title": "Vai nevēlies to redzēt?", "report.thanks.title": "Vai nevēlies to redzēt?",
"report.thanks.title_actionable": "Paldies, ka ziņoji, mēs to izskatīsim.", "report.thanks.title_actionable": "Paldies, ka ziņoji, mēs to izskatīsim.",
"report.unfollow": "Pārtraukt sekot @{name}", "report.unfollow": "Pārtraukt sekot @{name}",
"report.unfollow_explanation": "Tu seko šim kontam. Lai vairs neredzētu viņu ziņas savā mājas plūsmā, pārtrauc viņiem sekot.", "report.unfollow_explanation": "Tu seko šim kontam. Lai vairs neredzētu viņu ziņas savā mājas plūsmā, pārtrauc viņiem sekot.",
"report_notification.attached_statuses": "{count, plural, one {Pievienots {count} ieraksts} other {Pievienoti {count} ieraksti}}", "report_notification.attached_statuses": "Pievienoti {count, plural,one {{count} sūtījums} other {{count} sūtījumi}}",
"report_notification.categories.legal": "Tiesisks", "report_notification.categories.legal": "Tiesisks",
"report_notification.categories.other": "Cita", "report_notification.categories.other": "Cita",
"report_notification.categories.spam": "Spams", "report_notification.categories.spam": "Spams",
"report_notification.categories.violation": "Noteikumu pārkāpums", "report_notification.categories.violation": "Noteikumu pārkāpums",
"report_notification.open": "Atvērt sūdzību", "report_notification.open": "Atvērt ziņojumu",
"search.no_recent_searches": "Nav nesen veiktu meklējumu", "search.no_recent_searches": "Nav nesen veiktu meklējumu",
"search.placeholder": "Meklēšana", "search.placeholder": "Meklēšana",
"search.quick_action.account_search": "Profili atbilst {x}", "search.quick_action.account_search": "Profili atbilst {x}",
@ -628,7 +628,7 @@
"status.direct_indicator": "Pieminēts privāti", "status.direct_indicator": "Pieminēts privāti",
"status.edit": "Rediģēt", "status.edit": "Rediģēt",
"status.edited": "Rediģēts {date}", "status.edited": "Rediģēts {date}",
"status.edited_x_times": "Rediģēts {count, plural, one {{count} reizi} other {{count} reizes}}", "status.edited_x_times": "Rediģēts {count, plural, one {{count} reize} other {{count} reizes}}",
"status.embed": "Iestrādāt", "status.embed": "Iestrādāt",
"status.favourite": "Iecienīts", "status.favourite": "Iecienīts",
"status.filter": "Filtrē šo ziņu", "status.filter": "Filtrē šo ziņu",
@ -656,8 +656,8 @@
"status.remove_bookmark": "Noņemt grāmatzīmi", "status.remove_bookmark": "Noņemt grāmatzīmi",
"status.replied_to": "Atbildēja {name}", "status.replied_to": "Atbildēja {name}",
"status.reply": "Atbildēt", "status.reply": "Atbildēt",
"status.replyAll": "Atbildēt uz pavedienu", "status.replyAll": "Atbildēt uz tematu",
"status.report": "Sūdzēties par @{name}", "status.report": "Ziņot par @{name}",
"status.sensitive_warning": "Sensitīvs saturs", "status.sensitive_warning": "Sensitīvs saturs",
"status.share": "Kopīgot", "status.share": "Kopīgot",
"status.show_filter_reason": "Tomēr rādīt", "status.show_filter_reason": "Tomēr rādīt",
@ -668,7 +668,7 @@
"status.show_original": "Rādīt oriģinālu", "status.show_original": "Rādīt oriģinālu",
"status.title.with_attachments": "{user} publicējis {attachmentCount, plural, one {pielikumu} other {{attachmentCount} pielikumus}}", "status.title.with_attachments": "{user} publicējis {attachmentCount, plural, one {pielikumu} other {{attachmentCount} pielikumus}}",
"status.translate": "Tulkot", "status.translate": "Tulkot",
"status.translated_from_with": "Tulkots no {lang}, izmantojot {provider}", "status.translated_from_with": "Tulkots no {lang} izmantojot {provider}",
"status.uncached_media_warning": "Priekšskatījums nav pieejams", "status.uncached_media_warning": "Priekšskatījums nav pieejams",
"status.unmute_conversation": "Noņemt sarunas apklusinājumu", "status.unmute_conversation": "Noņemt sarunas apklusinājumu",
"status.unpin": "Noņemt profila piespraudumu", "status.unpin": "Noņemt profila piespraudumu",
@ -681,16 +681,16 @@
"time_remaining.hours": "{number, plural, one {Atlikusi # stunda} other {Atlikušas # stundas}}", "time_remaining.hours": "{number, plural, one {Atlikusi # stunda} other {Atlikušas # stundas}}",
"time_remaining.minutes": "{number, plural, one {Atlikusi # minūte} other {Atlikušas # minūtes}}", "time_remaining.minutes": "{number, plural, one {Atlikusi # minūte} other {Atlikušas # minūtes}}",
"time_remaining.moments": "Atlikuši daži mirkļi", "time_remaining.moments": "Atlikuši daži mirkļi",
"time_remaining.seconds": "{number, plural, one {Atlikusi # sekunde} other {Atlikušas # sekundes}}", "time_remaining.seconds": "Atlikušas {number, plural, one {# sekunde} other {# sekundes}}",
"timeline_hint.remote_resource_not_displayed": "{resource} no citiem serveriem nav parādīti.", "timeline_hint.remote_resource_not_displayed": "{resource} no citiem serveriem nav parādīti.",
"timeline_hint.resources.followers": "Sekotāji", "timeline_hint.resources.followers": "Sekotāji",
"timeline_hint.resources.follows": "Sekojošie", "timeline_hint.resources.follows": "Seko",
"timeline_hint.resources.statuses": "Vecāki ieraksti", "timeline_hint.resources.statuses": "Vecāki ieraksti",
"trends.counter_by_accounts": "{count, plural, one {{counter} persona} other {{counter} cilvēki}} par {days, plural, one {# dienu} other {{days} dienām}}", "trends.counter_by_accounts": "{count, plural, one {{counter} persona} other {{counter} cilvēki}} par {days, plural, one {# dienu} other {{days} dienām}}",
"trends.trending_now": "Aktuālās tendences", "trends.trending_now": "Aktuālās tendences",
"ui.beforeunload": "Ja pametīsiet Mastodon, jūsu melnraksts tiks zaudēts.", "ui.beforeunload": "Ja pametīsit Mastodonu, jūsu melnraksts tiks zaudēts.",
"units.short.billion": "{count}Mjd", "units.short.billion": "{count}Mjd",
"units.short.million": "{count}Mjn", "units.short.million": "{count}M",
"units.short.thousand": "{count}Tk", "units.short.thousand": "{count}Tk",
"upload_area.title": "Velc un nomet, lai augšupielādētu", "upload_area.title": "Velc un nomet, lai augšupielādētu",
"upload_button.label": "Pievienot bildi, video vai audio datni", "upload_button.label": "Pievienot bildi, video vai audio datni",
@ -707,7 +707,7 @@
"upload_modal.apply": "Pielietot", "upload_modal.apply": "Pielietot",
"upload_modal.applying": "Pielieto…", "upload_modal.applying": "Pielieto…",
"upload_modal.choose_image": "Izvēlēties attēlu", "upload_modal.choose_image": "Izvēlēties attēlu",
"upload_modal.description_placeholder": "Raibais runcis Rīgā ratu rumbā rūc", "upload_modal.description_placeholder": "Raibais runcis rīgā ratu rumbā rūc",
"upload_modal.detect_text": "Noteikt tekstu no attēla", "upload_modal.detect_text": "Noteikt tekstu no attēla",
"upload_modal.edit_media": "Rediģēt multividi", "upload_modal.edit_media": "Rediģēt multividi",
"upload_modal.hint": "Noklikšķini vai velc apli priekšskatījumā, lai izvēlētos fokusa punktu, kas vienmēr būs redzams visos sīktēlos.", "upload_modal.hint": "Noklikšķini vai velc apli priekšskatījumā, lai izvēlētos fokusa punktu, kas vienmēr būs redzams visos sīktēlos.",

View file

@ -593,6 +593,7 @@
"search_results.all": "Semua", "search_results.all": "Semua",
"search_results.hashtags": "Tanda pagar", "search_results.hashtags": "Tanda pagar",
"search_results.nothing_found": "Tidak dapat menemui apa-apa untuk istilah carian tersebut", "search_results.nothing_found": "Tidak dapat menemui apa-apa untuk istilah carian tersebut",
"search_results.see_all": "Lihat semua",
"search_results.statuses": "Hantaran", "search_results.statuses": "Hantaran",
"search_results.title": "Mencari {q}", "search_results.title": "Mencari {q}",
"server_banner.about_active_users": "Pengguna pelayan ini sepanjang 30 hari yang lalu (Pengguna Aktif Bulanan)", "server_banner.about_active_users": "Pengguna pelayan ini sepanjang 30 hari yang lalu (Pengguna Aktif Bulanan)",

View file

@ -312,6 +312,7 @@
"home.hide_announcements": "ကြေညာချက်များကို ဖျောက်ပါ", "home.hide_announcements": "ကြေညာချက်များကို ဖျောက်ပါ",
"home.pending_critical_update.body": "သင့် Mastodon ဆာဗာ အမြန်ဆုံး အပ်ဒိတ်လုပ်ပါ။", "home.pending_critical_update.body": "သင့် Mastodon ဆာဗာ အမြန်ဆုံး အပ်ဒိတ်လုပ်ပါ။",
"home.pending_critical_update.link": "အပ်ဒိတ်များကြည့်ရန်", "home.pending_critical_update.link": "အပ်ဒိတ်များကြည့်ရန်",
"home.pending_critical_update.title": "အရေးကြီးသည့် လုံခြုံရေးအပ်ဒိတ် ရနိုင်ပါမည်။",
"home.show_announcements": "ကြေညာချက်များကို ပြပါ", "home.show_announcements": "ကြေညာချက်များကို ပြပါ",
"interaction_modal.description.favourite": "Mastodon အကောင့်ဖြင့် ဤပို့စ်ကို သင် favorite ပြုလုပ်ကြောင်း စာရေးသူအား အသိပေးပြီး နောက်ပိုင်းတွင် သိမ်းဆည်းနိုင်သည်။", "interaction_modal.description.favourite": "Mastodon အကောင့်ဖြင့် ဤပို့စ်ကို သင် favorite ပြုလုပ်ကြောင်း စာရေးသူအား အသိပေးပြီး နောက်ပိုင်းတွင် သိမ်းဆည်းနိုင်သည်။",
"interaction_modal.description.follow": "Mastodon အကောင့်ဖြင့် သင်၏ ပင်မစာမျက်နှာတွင် ၎င်းတို့၏ ပို့စ်များကို ရရှိရန်အတွက် {name} ကို စောင့်ကြည့်နိုင်ပါသည်။", "interaction_modal.description.follow": "Mastodon အကောင့်ဖြင့် သင်၏ ပင်မစာမျက်နှာတွင် ၎င်းတို့၏ ပို့စ်များကို ရရှိရန်အတွက် {name} ကို စောင့်ကြည့်နိုင်ပါသည်။",
@ -594,6 +595,7 @@
"search_popout.options": "ရွေးချယ်ထားသည်များ ရှာဖွေရန်", "search_popout.options": "ရွေးချယ်ထားသည်များ ရှာဖွေရန်",
"search_popout.quick_actions": "အမြန်လုပ်ဆောင်မှုများ", "search_popout.quick_actions": "အမြန်လုပ်ဆောင်မှုများ",
"search_popout.recent": "လတ်တလော ရှာဖွေမှုများ", "search_popout.recent": "လတ်တလော ရှာဖွေမှုများ",
"search_popout.specific_date": "သီးခြားရက်စွဲ",
"search_popout.user": "အသုံးပြုသူ", "search_popout.user": "အသုံးပြုသူ",
"search_results.accounts": "စာမျက်နှာ", "search_results.accounts": "စာမျက်နှာ",
"search_results.all": "အားလုံး", "search_results.all": "အားလုံး",

View file

@ -537,7 +537,7 @@
"relative_time.today": "сегодня", "relative_time.today": "сегодня",
"reply_indicator.cancel": "Отмена", "reply_indicator.cancel": "Отмена",
"report.block": "Заблокировать", "report.block": "Заблокировать",
"report.block_explanation": "В перестаните видеть посты этого пользователя, а он(а) больше не сможет подписаться на вас и читать ваши посты. Он(а) сможет понять что вы заблокировали его/её.", "report.block_explanation": "Вы перестанете видеть посты этого пользователя, и он(а) больше не сможет подписаться на вас и читать ваши посты. Он(а) сможет понять, что вы заблокировали его/её.",
"report.categories.legal": "Правовая информация", "report.categories.legal": "Правовая информация",
"report.categories.other": "Другое", "report.categories.other": "Другое",
"report.categories.spam": "Спам", "report.categories.spam": "Спам",

View file

@ -1,79 +1,88 @@
{ {
"about.blocks": "මැදිහත්කරණ සේවාදායක",
"about.contact": "සබඳතාව:",
"about.disclaimer": "මාස්ටඩන් යනු නිදහස් විවෘත මූලාශ්‍ර මෘදුකාංගයකි. එය මාස්ටඩන් gGmbH හි වෙළඳ නාමයකි.",
"about.domain_blocks.suspended.title": "අත්හිටුවා ඇත",
"about.rules": "සේවාදායකයේ නීති",
"account.account_note_header": "සටහන", "account.account_note_header": "සටහන",
"account.add_or_remove_from_list": "ලැයිස්තු වලින් එකතු හෝ ඉවත් කරන්න", "account.add_or_remove_from_list": "ලැයිස්තු වලින් එකතු හෝ ඉවත් කරන්න",
"account.badges.bot": "ස්වයං ක්‍රමලේඛය", "account.badges.bot": "ස්වයංක්‍රියයි",
"account.badges.group": "සමූහය", "account.badges.group": "සමූහය",
"account.block": "@{name} අවහිර කරන්න", "account.block": "@{name} අවහිර කරන්න",
"account.block_domain": "{domain} වසම අවහිර කරන්න", "account.block_domain": "{domain} වසම අවහිර කරන්න",
"account.block_short": "අවහිර",
"account.blocked": "අවහිර කර ඇත", "account.blocked": "අවහිර කර ඇත",
"account.browse_more_on_origin_server": "මුල් පැතිකඩෙහි තවත් පිරික්සන්න", "account.browse_more_on_origin_server": "මුල් පැතිකඩෙහි තවත් පිරික්සන්න",
"account.cancel_follow_request": "Withdraw follow request",
"account.disable_notifications": "@{name} පළ කරන විට මට දැනුම් නොදෙන්න", "account.disable_notifications": "@{name} පළ කරන විට මට දැනුම් නොදෙන්න",
"account.domain_blocked": "වසම අවහිර කර ඇත", "account.domain_blocked": "වසම අවහිර කර ඇත",
"account.edit_profile": "පැතිකඩ සංස්කරණය", "account.edit_profile": "පැතිකඩ සංස්කරණය",
"account.enable_notifications": "@{name} පළ කරන විට මට දැනුම් දෙන්න", "account.enable_notifications": "@{name} පළ කරන විට මට දැනුම් දෙන්න",
"account.endorse": "පැතිකඩෙහි විශේෂාංගය", "account.endorse": "පැතිකඩෙහි විශේෂාංගය",
"account.featured_tags.last_status_never": "ලිපි නැත",
"account.follow": "අනුගමනය", "account.follow": "අනුගමනය",
"account.followers": "අනුගාමිකයින්", "account.followers": "අනුගාමිකයින්",
"account.followers.empty": "කිසිවෙක් අනුගමනය කර නැත.", "account.followers.empty": "කිසිවෙක් අනුගමනය කර නැත.",
"account.followers_counter": "{count, plural, one {{counter} අනුගාමිකයෙක්} other {{counter} අනුගාමිකයින්}}", "account.followers_counter": "{count, plural, one {අනුගාමිකයින් {counter}} other {අනුගාමිකයින් {counter}}}",
"account.following": "අනුගමන", "account.following": "අනුගමන",
"account.following_counter": "{count, plural, one {අනුගාමිකයින් {counter}} other {අනුගාමිකයින් {counter}}}", "account.following_counter": "{count, plural, one {අනුගමන {counter}} other {අනුගමන {counter}}}",
"account.follows.empty": "තවමත් කිසිවෙක් අනුගමනය නොකරයි.", "account.follows.empty": "තවමත් කිසිවෙක් අනුගමනය නොකරයි.",
"account.follows_you": "ඔබව අනුගමනය කරයි", "account.follows_you": "ඔබව අනුගමනය කරයි",
"account.hide_reblogs": "@{name}සිට බූස්ට් සඟවන්න", "account.go_to_profile": "පැතිකඩට යන්න",
"account.joined_short": "එක් වූ දිනය",
"account.link_verified_on": "මෙම සබැඳියේ අයිතිය {date} දී පරීක්‍ෂා කෙරිණි", "account.link_verified_on": "මෙම සබැඳියේ අයිතිය {date} දී පරීක්‍ෂා කෙරිණි",
"account.locked_info": "මෙම ගිණුමේ රහස්‍යතා තත්ත්වය අගුලු දමා ඇත. හිමිකරු ඔවුන් අනුගමනය කළ හැක්කේ කාටදැයි හස්තීයව සමාලෝචනය කරයි.", "account.media": "මාධ්‍ය",
"account.media": "මාධ්‍යය", "account.mention": "@{name} සඳහන් කරන්ක",
"account.mention": "@{name} සැඳහුම",
"account.mute": "@{name} නිහඬ කරන්න", "account.mute": "@{name} නිහඬ කරන්න",
"account.mute_short": "නිහඬ",
"account.muted": "නිහඬ කළා", "account.muted": "නිහඬ කළා",
"account.posts": "ලිපි", "account.posts": "ලිපි",
"account.posts_with_replies": "ටූට්ස් සහ පිළිතුරු", "account.posts_with_replies": "ලිපි සහ පිළිතුරු",
"account.report": "@{name} වාර්තා කරන්න", "account.report": "@{name} වාර්තා කරන්න",
"account.requested": "අනුමැතිය බලාපොරොත්තුවෙන්",
"account.share": "@{name} ගේ පැතිකඩ බෙදාගන්න", "account.share": "@{name} ගේ පැතිකඩ බෙදාගන්න",
"account.show_reblogs": "@{name}සිට බූස්ට් පෙන්වන්න", "account.statuses_counter": "{count, plural, one {ලිපි {counter}} other {ලිපි {counter}}}",
"account.statuses_counter": "{count, plural, one {{counter} ටූට්} other {{counter} ටූට්ස්}}",
"account.unblock": "@{name} අනවහිර කරන්න", "account.unblock": "@{name} අනවහිර කරන්න",
"account.unblock_domain": "{domain} වසම අනවහිර කරන්න", "account.unblock_domain": "{domain} වසම අනවහිර කරන්න",
"account.unblock_short": "අනවහිර", "account.unblock_short": "අනවහිර",
"account.unendorse": "පැතිකඩෙහි විශේෂාංග නොකරන්න", "account.unendorse": "පැතිකඩෙහි විශේෂාංග නොකරන්න",
"account.unfollow": "අනුගමනය නොකරන්න",
"account.unmute": "@{name}නිහඬ නොකරන්න",
"account.unmute_short": "නොනිහඬ", "account.unmute_short": "නොනිහඬ",
"account_note.placeholder": "සටහන යෙදීමට ඔබන්න", "account_note.placeholder": "සටහන යෙදීමට ඔබන්න",
"admin.dashboard.daily_retention": "ලියාපදිංචි වීමෙන් පසු දිනකට පරිශීලක රඳවා ගැනීමේ අනුපාතය", "admin.dashboard.retention.cohort": "ලියාපදිංචි මාසය",
"admin.dashboard.monthly_retention": "ලියාපදිංචි වීමෙන් පසු මාසය අනුව පරිශීලක රඳවා ගැනීමේ අනුපාතය",
"admin.dashboard.retention.average": "සාමාන්යය",
"admin.dashboard.retention.cohort": "ලියාපදිංචි වීමේ මාසය",
"admin.dashboard.retention.cohort_size": "නව පරිශ්‍රීලකයින්", "admin.dashboard.retention.cohort_size": "නව පරිශ්‍රීලකයින්",
"alert.rate_limited.message": "{retry_time, time, medium} කට පසුව උත්සාහ කරන්න.", "alert.rate_limited.message": "{retry_time, time, medium} කට පසුව උත්සාහ කරන්න.",
"alert.rate_limited.title": "මිල සීමා සහිතයි", "alert.rate_limited.title": "අනුපාතනය වී ඇත",
"alert.unexpected.message": "අනපේක්ෂිත දෝෂයක් ඇතිවුනා.", "alert.unexpected.message": "අනපේක්‍ෂිත දෝෂයක් සිදු විය.",
"alert.unexpected.title": "අපොයි!", "alert.unexpected.title": "අපොයි!",
"announcement.announcement": "නිවේදනය", "announcement.announcement": "නිවේදනය",
"attachments_list.unprocessed": "(සැකසුම් නොකළ)",
"audio.hide": "හඬපටය සඟවන්න", "audio.hide": "හඬපටය සඟවන්න",
"autosuggest_hashtag.per_week": "සතියකට {count}", "autosuggest_hashtag.per_week": "සතියකට {count}",
"boost_modal.combo": "ඊළඟ වතාවේ මෙය මඟ හැරීමට ඔබට {combo} එබිය හැක", "boost_modal.combo": "ඊළඟ වතාවේ මෙය මඟ හැරීමට {combo} එබීමට හැකිය",
"bundle_column_error.copy_stacktrace": "දෝෂ වාර්තාවේ පිටපතක්",
"bundle_column_error.error.title": "අපොයි!",
"bundle_column_error.network.title": "ජාලයේ දෝෂයකි",
"bundle_column_error.retry": "නැවත උත්සාහ කරන්න", "bundle_column_error.retry": "නැවත උත්සාහ කරන්න",
"bundle_column_error.return": "ආපසු මුලට යන්න",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "වසන්න", "bundle_modal_error.close": "වසන්න",
"bundle_modal_error.message": "මෙම සංරචකය පූරණය කිරීමේදී යම් දෙයක් වැරදී ඇත.", "bundle_modal_error.message": "මෙම සංරචකය පූරණය දී යම් දෙයක් වැරදී ඇත.",
"bundle_modal_error.retry": "නැවත උත්සාහ කරන්න", "bundle_modal_error.retry": "නැවත උත්සාහ කරන්න",
"closed_registrations_modal.find_another_server": "වෙනත් සේවාදායක",
"closed_registrations_modal.title": "මාස්ටඩන් හි ලියාපදිංචි වන්න",
"column.about": "පිලිබඳව", "column.about": "පිලිබඳව",
"column.blocks": "අවහිර කළ අය", "column.blocks": "අවහිර කළ අය",
"column.bookmarks": "පොත් යොමු", "column.bookmarks": "පොත්යොමු",
"column.community": "දේශීය කාලරේඛාව", "column.community": "ස්ථානීය කාලරේඛාව",
"column.direct": "පෞද්ගලික සැඳහුම්",
"column.directory": "පැතිකඩ පිරික්සන්න", "column.directory": "පැතිකඩ පිරික්සන්න",
"column.domain_blocks": "අවහිර කළ වසම්", "column.domain_blocks": "අවහිර කළ වසම්",
"column.favourites": "ප්‍රියතමයන්",
"column.firehose": "සජීව සංග්‍රහ",
"column.follow_requests": "අනුගමන ඉල්ලීම්", "column.follow_requests": "අනුගමන ඉල්ලීම්",
"column.home": "මුල් පිටුව", "column.home": "මුල් පිටුව",
"column.lists": "ලේඛන", "column.lists": "ලැයිස්තු",
"column.mutes": "නිහඬ කළ අය", "column.mutes": "නිහඬ කළ අය",
"column.notifications": "දැනුම්දීම්", "column.notifications": "දැනුම්දීම්",
"column.pins": "ඇමිණූ ලිපි", "column.pins": "ඇමිණූ ලිපි",
"column.public": "ෆෙඩරේටඩ් කාලරේඛාව", "column.public": "ඒකාබද්ධ කාලරේඛාව",
"column_back_button.label": "ආපසු", "column_back_button.label": "ආපසු",
"column_header.hide_settings": "සැකසුම් සඟවන්න", "column_header.hide_settings": "සැකසුම් සඟවන්න",
"column_header.moveLeft_settings": "තීරුව වමට ගෙනයන්න", "column_header.moveLeft_settings": "තීරුව වමට ගෙනයන්න",
@ -87,70 +96,62 @@
"community.column_settings.remote_only": "දුරස්ථව පමණයි", "community.column_settings.remote_only": "දුරස්ථව පමණයි",
"compose.language.change": "භාෂාව සංශෝධනය", "compose.language.change": "භාෂාව සංශෝධනය",
"compose.language.search": "භාෂා සොයන්න...", "compose.language.search": "භාෂා සොයන්න...",
"compose.published.body": "ලිපිය පළ විය.",
"compose.published.open": "අරින්න",
"compose.saved.body": "ලිපිය සුරැකිණි.",
"compose_form.direct_message_warning_learn_more": "තව දැනගන්න", "compose_form.direct_message_warning_learn_more": "තව දැනගන්න",
"compose_form.encryption_warning": "Mastodon හි පළ කිරීම් අන්තයේ සිට අවසානය දක්වා සංකේතනය කර නොමැත. Mastodon හරහා කිසිදු සංවේදී තොරතුරක් බෙදා නොගන්න.", "compose_form.encryption_warning": "මාස්ටඩන් වෙත පළ කරන දෑ අන්ත සංකේතනයෙන් ආරක්‍ෂා නොවේ. මාස්ටඩන් හරහා කිසිදු සංවේදී තොරතුරක් බෙදා නොගන්න.",
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
"compose_form.lock_disclaimer": "ඔබගේ ගිණුම {locked}නොවේ. ඔබගේ අනුගාමිකයින්ට පමණක් පළ කිරීම් බැලීමට ඕනෑම කෙනෙකුට ඔබව අනුගමනය කළ හැක.",
"compose_form.lock_disclaimer.lock": "අගුළු දමා ඇත", "compose_form.lock_disclaimer.lock": "අගුළු දමා ඇත",
"compose_form.placeholder": "ඔබගේ සිතුවිලි මොනවාද?", "compose_form.placeholder": "ඔබගේ සිතුවිලි මොනවාද?",
"compose_form.poll.add_option": "තේරීමක් යොදන්න", "compose_form.poll.add_option": "තේරීමක් යොදන්න",
"compose_form.poll.duration": "මත විමසීමේ කාලය", "compose_form.poll.duration": "මත විමසීමේ කාලය",
"compose_form.poll.option_placeholder": "තේරීම {number}", "compose_form.poll.option_placeholder": "තේරීම {number}",
"compose_form.poll.remove_option": "මෙම ඉවත් කරන්න", "compose_form.poll.remove_option": "මෙම ඉවත් කරන්න",
"compose_form.poll.switch_to_multiple": "තේරීම් කිහිපයක් ඉඩ දීම සඳහා මත විමසුම වෙනස් කරන්න", "compose_form.poll.switch_to_multiple": "තේරීම් කිහිපයක මත විමසුම වෙනස් කරන්න",
"compose_form.poll.switch_to_single": "තනි තේරීමකට ඉඩ දීම සඳහා මත විමසුම වෙනස් කරන්න", "compose_form.poll.switch_to_single": "තනි තේරීමකට මත විමසුම වෙනස් කරන්න",
"compose_form.publish": "ප්‍රකාශනය", "compose_form.publish": "ප්‍රකාශනය",
"compose_form.publish_form": "නව ලිපිය", "compose_form.publish_form": "නව ලිපිය",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "වෙනස්කම් සුරකින්න", "compose_form.save_changes": "වෙනස්කම් සුරකින්න",
"compose_form.sensitive.hide": "{count, plural, one {මාධ්ය සංවේදී ලෙස සලකුණු කරන්න} other {මාධ්ය සංවේදී ලෙස සලකුණු කරන්න}}", "compose_form.spoiler.marked": "අන්තර්ගත අවවාදය ඉවත් කරන්න",
"compose_form.sensitive.marked": "{count, plural, one {මාධ්‍ය සංවේදී ලෙස සලකුණු කර ඇත} other {මාධ්‍ය සංවේදී ලෙස සලකුණු කර ඇත}}", "compose_form.spoiler.unmarked": "අන්තර්ගත අවවාදයක් එක් කරන්න",
"compose_form.sensitive.unmarked": "{count, plural, one {මාධ්‍ය සංවේදී ලෙස සලකුණු කර නැත} other {මාධ්‍ය සංවේදී ලෙස සලකුණු කර නැත}}",
"compose_form.spoiler.marked": "අනතුරු ඇඟවීම පිටුපස පෙළ සඟවා ඇත",
"compose_form.spoiler.unmarked": "ප්‍රයෝජනය සඟවා නැත",
"compose_form.spoiler_placeholder": "අවවාදය මෙහි ලියන්න", "compose_form.spoiler_placeholder": "අවවාදය මෙහි ලියන්න",
"confirmation_modal.cancel": "අවලංගු", "confirmation_modal.cancel": "අවලංගු",
"confirmations.block.block_and_report": "අවහිර කර වාර්තා කරන්න", "confirmations.block.block_and_report": "අවහිර කර වාර්තා කරන්න",
"confirmations.block.confirm": "අවහිර", "confirmations.block.confirm": "අවහිර",
"confirmations.block.message": "ඔබට {name} අවහිර කිරීමට වුවමනා ද?", "confirmations.block.message": "ඔබට {name} අවහිර කිරීමට වුවමනා ද?",
"confirmations.delete.confirm": "මකන්න", "confirmations.delete.confirm": "මකන්න",
"confirmations.delete.message": "ඔබට මෙම තත්ත්වය මැකීමට අවශ්‍ය බව විශ්වාසද?", "confirmations.delete.message": "ඔබට මෙම ලිපිය මැකීමට වුවමනා ද?",
"confirmations.delete_list.confirm": "මකන්න", "confirmations.delete_list.confirm": "මකන්න",
"confirmations.delete_list.message": "ඔබට මෙම ලැයිස්තුව ස්ථිරවම මැකීමට අවශ්‍ය බව විශ්වාසද?", "confirmations.delete_list.message": "ඔබට මෙම ලැයිස්තුව සදහටම මැකීමට වුවමනා ද?",
"confirmations.discard_edit_media.confirm": "ඉවත ලන්න", "confirmations.discard_edit_media.confirm": "ඉවත ලන්න",
"confirmations.discard_edit_media.message": "ඔබට මාධ්‍ය විස්තරයට හෝ පෙරදසුනට නොසුරකින ලද වෙනස්කම් තිබේ, කෙසේ වෙතත් ඒවා ඉවත දමන්නද?", "confirmations.discard_edit_media.message": "ඔබට මාධ්‍ය විස්තරයට හෝ පෙරදසුනට නොසුරකින ලද වෙනස්කම් තිබේ, කෙසේ වෙතත් ඒවා ඉවත දමන්නද?",
"confirmations.domain_block.confirm": "සම්පූර්ණ වසම අවහිර කරන්න", "confirmations.domain_block.confirm": "සම්පූර්ණ වසම අවහිර කරන්න",
"confirmations.domain_block.message": "ඔබට සම්පූර්ණ {domain}අවහිර කිරීමට අවශ්‍ය බව ඔබට සැබවින්ම විශ්වාසද? බොහෝ අවස්ථාවලදී ඉලක්කගත බ්ලොක් හෝ නිශ්ශබ්ද කිරීම් කිහිපයක් ප්රමාණවත් වන අතර වඩාත් යෝග්ය වේ. ඔබ කිසිදු පොදු කාලරාමුවක හෝ ඔබගේ දැනුම්දීම් වල එම වසමේ අන්තර්ගතය නොදකිනු ඇත. එම වසමෙන් ඔබගේ අනුගාමිකයින් ඉවත් කරනු ලැබේ.", "confirmations.edit.confirm": "සංස්කරණය",
"confirmations.logout.confirm": "නික්මෙන්න", "confirmations.logout.confirm": "නික්මෙන්න",
"confirmations.logout.message": "ඔබට නික්මෙන්න අවශ්‍ය බව විශ්වාසද?", "confirmations.logout.message": "ඔබට නික්මෙන්න අවශ්‍ය බව විශ්වාසද?",
"confirmations.mute.confirm": "නිශ්ශබ්ද", "confirmations.mute.confirm": "නිශ්ශබ්ද",
"confirmations.mute.explanation": "මෙය ඔවුන්ගෙන් පළ කිරීම් සහ ඒවා සඳහන් කරන පළ කිරීම් සඟවයි, නමුත් එය ඔවුන්ට ඔබේ පළ කිරීම් බැලීමට සහ ඔබව අනුගමනය කිරීමට තවමත් ඉඩ ලබා දේ.", "confirmations.mute.message": "{name} නිහඬ කිරීමට වුවමනා ද?",
"confirmations.mute.message": "ඔබට {name} නිශ්ශබ්ද කිරීමට අවශ්‍ය බව විශ්වාසද?",
"confirmations.redraft.confirm": "මකන්න සහ නැවත කෙටුම්පත් කරන්න",
"confirmations.reply.confirm": "පිළිතුර", "confirmations.reply.confirm": "පිළිතුර",
"confirmations.reply.message": "දැන් පිළිතුරු දීම ඔබ දැනට රචනා කරන පණිවිඩය උඩින් ලියයි. ඔබට ඉදිරියට යාමට අවශ්‍ය බව විශ්වාසද?",
"confirmations.unfollow.confirm": "අනුගමනය නොකරන්න",
"confirmations.unfollow.message": "ඔබට {name}අනුගමනය නොකිරීමට අවශ්‍ය බව විශ්වාසද?",
"conversation.delete": "සංවාදය මකන්න", "conversation.delete": "සංවාදය මකන්න",
"conversation.mark_as_read": "කියවූ බව යොදන්න", "conversation.mark_as_read": "කියවූ බව යොදන්න",
"conversation.open": "සංවාදය බලන්න", "conversation.open": "සංවාදය බලන්න",
"conversation.with": "{names} සමඟ", "conversation.with": "{names} සමඟ",
"copypaste.copied": "පිටපත් විය", "copypaste.copied": "පිටපත් විය",
"directory.federated": "දන්නා fediverse වලින්", "copypaste.copy_to_clipboard": "පසුරුපුවරුවට පිටපතක්",
"directory.federated": "දන්නා ෆෙඩිවර්ස් වෙතින්",
"directory.local": "{domain} වෙතින් පමණි", "directory.local": "{domain} වෙතින් පමණි",
"directory.new_arrivals": "නව පැමිණීම්", "directory.new_arrivals": "නව පැමිණීම්",
"directory.recently_active": "මෑත දී සක්‍රියයි", "directory.recently_active": "මෑත දී සක්‍රියයි",
"dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.", "disabled_account_banner.account_settings": "ගිණුමේ සැකසුම්",
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.", "embed.instructions": "පහත කේතය පිටපත් කිරීමෙන් මෙම ලිපිය ඔබගේ අඩවියට කාවද්දන්න.",
"embed.instructions": "පහත කේතය පිටපත් කිරීමෙන් මෙම තත්ත්වය ඔබේ වෙබ් අඩවියට ඇතුළත් කරන්න.", "embed.preview": "මෙන්න එය පෙනෙන අන්දම:",
"embed.preview": "එය පෙනෙන්නේ කෙසේද යන්න මෙන්න:",
"emoji_button.activity": "ක්‍රියාකාරකම", "emoji_button.activity": "ක්‍රියාකාරකම",
"emoji_button.clear": "මකන්න", "emoji_button.clear": "මකන්න",
"emoji_button.custom": "අභිරුචි", "emoji_button.custom": "අභිරුචි",
"emoji_button.flags": "කොඩි",
"emoji_button.food": "ආහාර සහ පාන", "emoji_button.food": "ආහාර සහ පාන",
"emoji_button.label": "ඉමොජි යොදන්න", "emoji_button.label": "ඉමොජි යොදන්න",
"emoji_button.nature": "ස්වභාවික", "emoji_button.nature": "සොබාදහම",
"emoji_button.not_found": "ගැළපෙන ඉමෝජි හමු නොවිණි", "emoji_button.not_found": "ගැළපෙන ඉමෝජි හමු නොවිණි",
"emoji_button.objects": "වස්තූන්", "emoji_button.objects": "වස්තූන්",
"emoji_button.people": "මිනිසුන්", "emoji_button.people": "මිනිසුන්",
@ -163,155 +164,146 @@
"empty_column.account_timeline": "මෙහි ලිපි නැත!", "empty_column.account_timeline": "මෙහි ලිපි නැත!",
"empty_column.account_unavailable": "පැතිකඩ නොතිබේ", "empty_column.account_unavailable": "පැතිකඩ නොතිබේ",
"empty_column.blocks": "කිසිදු පරිශීලකයෙකු අවහිර කර නැත.", "empty_column.blocks": "කිසිදු පරිශීලකයෙකු අවහිර කර නැත.",
"empty_column.bookmarked_statuses": "ඔබට තවමත් පිටු සලකුණු කළ මෙවලම් කිසිවක් නොමැත. ඔබ එකක් පිටු සලකුණු කළ විට, එය මෙහි පෙන්වනු ඇත.", "empty_column.bookmarked_statuses": "ඔබ සතුව පොත්යොමු තබන ලද ලිපි කිසිවක් නැත. ඔබ පොත්යොමුවක් තබන විට, එය මෙහි දිස්වනු ඇත.",
"empty_column.community": "දේශීය කාලරේඛාව හිස් ය. පන්දුව පෙරළීමට ප්‍රසිද්ධියේ යමක් ලියන්න!",
"empty_column.domain_blocks": "අවහිර කරන ලද වසම් නැත.", "empty_column.domain_blocks": "අවහිර කරන ලද වසම් නැත.",
"empty_column.explore_statuses": "දැන් කිසිවක් නැඹුරු නොවේ. පසුව නැවත පරීක්ෂා කරන්න!", "empty_column.explore_statuses": "දැන් කිසිවක් නැඹුරු නොවේ. පසුව නැවත පරීක්ෂා කරන්න!",
"empty_column.follow_requests": "ඔබට තවමත් අනුගමනය කිරීමේ ඉල්ලීම් කිසිවක් නොමැත. ඔබට එකක් ලැබුණු විට, එය මෙහි පෙන්වනු ඇත.", "empty_column.follow_requests": "ඔබට තවමත් අනුගමන ඉල්ලීම් ලැබී නැත. ඉල්ලීමක් ලැබුණු විට, එය මෙහි පෙන්වනු ඇත.",
"empty_column.hashtag": "මෙම හැෂ් ටැග් එකේ තවම කිසිවක් නොමැත.", "empty_column.home": "මුල් පිටුව හිස් ය! මෙය පිරවීමට බොහෝ පුද්ගලයින් අනුගමනය කරන්න.",
"empty_column.home": "ඔබගේ නිවසේ කාලරේඛාව හිස්ය! එය පිරවීම සඳහා තවත් පුද්ගලයින් අනුගමනය කරන්න. {suggestions}",
"empty_column.list": "මෙම ලැයිස්තුවේ තවමත් කිසිවක් නොමැත. මෙම ලැයිස්තුවේ සාමාජිකයන් නව තත්ව පළ කරන විට, ඔවුන් මෙහි දිස් වනු ඇත.",
"empty_column.lists": "ඔබට තවමත් ලැයිස්තු කිසිවක් නැත. ඔබ එකක් සාදන විට, එය මෙහි පෙන්වනු ඇත.", "empty_column.lists": "ඔබට තවමත් ලැයිස්තු කිසිවක් නැත. ඔබ එකක් සාදන විට, එය මෙහි පෙන්වනු ඇත.",
"empty_column.mutes": "ඔබ තවමත් කිසිදු පරිශීලකයෙකු නිහඬ කර නැත.", "empty_column.mutes": "ඔබ තවමත් කිසිදු පරිශීලකයෙකු නිහඬ කර නැත.",
"empty_column.notifications": "ඔබට තවම දැනුම්දීම් කිසිවක් නැත. වෙනත් පුද්ගලයින් ඔබ සමඟ අන්තර් ක්‍රියා කරන විට, ඔබ එය මෙහි දකිනු ඇත.", "empty_column.notifications": "ඔබට දැනුම්දීම් ලැබී නැත. අන් අය සහ ඔබ අතර අන්‍යෝන්‍ය බලපවත්වන දෑ මෙහි දිස්වනු ඇත.",
"empty_column.public": "මෙහි කිසිවක් නැත! යමක් ප්‍රසිද්ධියේ ලියන්න, නැතහොත් එය පිරවීම සඳහා වෙනත් සේවාදායකයන්ගෙන් පරිශීලකයන් හස්තීයව අනුගමනය කරන්න",
"error.unexpected_crash.explanation": "අපගේ කේතයේ දෝෂයක් හෝ බ්‍රවුසර ගැළපුම් ගැටලුවක් හේතුවෙන්, මෙම පිටුව නිවැරදිව ප්‍රදර්ශනය කළ නොහැක.", "error.unexpected_crash.explanation": "අපගේ කේතයේ දෝෂයක් හෝ බ්‍රවුසර ගැළපුම් ගැටලුවක් හේතුවෙන්, මෙම පිටුව නිවැරදිව ප්‍රදර්ශනය කළ නොහැක.",
"error.unexpected_crash.explanation_addons": "මෙම පිටුව නිවැරදිව ප්‍රදර්ශනය කළ නොහැක. මෙම දෝෂය බ්‍රවුසර ඇඩෝනයක් හෝ ස්වයංක්‍රීය පරිවර්තන මෙවලම් නිසා ඇති විය හැක.", "error.unexpected_crash.explanation_addons": "මෙම පිටුව නිවැරදිව ප්‍රදර්ශනය කළ නොහැක. මෙම දෝෂය බ්‍රවුසර ඇඩෝනයක් හෝ ස්වයංක්‍රීය පරිවර්තන මෙවලම් නිසා ඇති විය හැක.",
"error.unexpected_crash.next_steps": "පිටුව නැවුම් කිරීමට උත්සාහ කරන්න. එය උදව් නොකළහොත්, ඔබට තවමත් වෙනත් බ්‍රවුසරයක් හෝ ස්වදේශීය යෙදුමක් හරහා Mastodon භාවිත කිරීමට හැකි වේ.", "error.unexpected_crash.next_steps": "පිටුව නැවුම් කර බලන්න. එයින් ඵලක් නොවේ නම්, වෙනත් අතිරික්සුවක් හෝ නිසග යෙදුමක් හරහා මාස්ටඩන් භාවිතා කරන්න.",
"error.unexpected_crash.next_steps_addons": "ඒවා අක්‍රිය කර පිටුව නැවුම් කිරීමට උත්සාහ කරන්න. එය උදව් නොකළහොත්, ඔබට තවමත් වෙනත් බ්‍රවුසරයක් හෝ ස්වදේශීය යෙදුමක් හරහා Mastodon භාවිත කිරීමට හැකි වේ.", "error.unexpected_crash.next_steps_addons": "ඒවා අබල කර පිටුව නැවුම් කරන්න. එයින් ඵලක් නොවේ නම්, වෙනත් අතිරික්සුවක් හෝ නිසග යෙදුමක් හරහා මාස්ටඩන් භාවිතා කරන්න.",
"errors.unexpected_crash.copy_stacktrace": "ස්ටැක්ට්රේස් පසුරු පුවරුවට පිටපත් කරන්න",
"errors.unexpected_crash.report_issue": "ගැටළුව වාර්තාව", "errors.unexpected_crash.report_issue": "ගැටළුව වාර්තාව",
"explore.search_results": "සෙවුම් ප්‍රතිඵල", "explore.search_results": "සෙවුම් ප්‍රතිඵල",
"explore.title": "ගවේශණය", "explore.suggested_follows": "පුද්ගලයින්",
"explore.title": "ගවේශනය",
"explore.trending_links": "පුවත්",
"explore.trending_statuses": "ලිපි",
"filter_modal.added.expired_title": "පෙරහන ඉකුත්ය!", "filter_modal.added.expired_title": "පෙරහන ඉකුත්ය!",
"filter_modal.added.review_and_configure_title": "පෙරහන් සැකසුම්", "filter_modal.added.review_and_configure_title": "පෙරහන් සැකසුම්",
"filter_modal.added.settings_link": "සැකසුම් පිටුව", "filter_modal.added.settings_link": "සැකසුම් පිටුව",
"filter_modal.added.title": "පෙරහන එක් කළා!",
"filter_modal.select_filter.expired": "ඉකුත්ය", "filter_modal.select_filter.expired": "ඉකුත්ය",
"filter_modal.select_filter.prompt_new": "නව ප්‍රවර්ගය: {name}", "filter_modal.select_filter.prompt_new": "නව ප්‍රවර්ගය: {name}",
"filter_modal.select_filter.search": "සොයන්න හෝ සාදන්න", "filter_modal.select_filter.search": "සොයන්න හෝ සාදන්න",
"follow_request.authorize": "අවසරලත්", "filter_modal.select_filter.title": "මෙම ලිපිය පෙරන්න",
"filter_modal.title.status": "ලිපියක් පෙරන්න",
"firehose.local": "මෙම සේවාදායකය",
"firehose.remote": "වෙනත් සේවාදායක",
"follow_request.reject": "ප්‍රතික්‍ෂේප", "follow_request.reject": "ප්‍රතික්‍ෂේප",
"follow_requests.unlocked_explanation": "ඔබගේ ගිණුම අගුලු දමා නොතිබුණද, {domain} කාර්ය මණ්ඩලය සිතුවේ ඔබට මෙම ගිණුම් වලින් ලැබෙන ඉල්ලීම් හස්තීයව සමාලෝචනය කිරීමට අවශ්‍ය විය හැකි බවයි.", "footer.about": "පිළිබඳව",
"footer.directory": "පැතිකඩ නාමාවලිය",
"footer.get_app": "යෙදුම ගන්න",
"footer.invite": "ආරාධනා කරන්න",
"footer.keyboard_shortcuts": "යතුරුපුවරුවේ කෙටිමං",
"footer.privacy_policy": "රහස්‍යතා ප්‍රතිපත්තිය",
"footer.source_code": "මූලාශ්‍ර කේතය බලන්න",
"footer.status": "තත්‍වය",
"generic.saved": "සුරැකිණි", "generic.saved": "සුරැකිණි",
"getting_started.heading": "පටන් ගන්න", "getting_started.heading": "පටන් ගන්න",
"hashtag.column_header.tag_mode.all": "සහ {additional}", "hashtag.column_header.tag_mode.all": "සහ {additional}",
"hashtag.column_header.tag_mode.any": "හෝ {additional}", "hashtag.column_header.tag_mode.any": "හෝ {additional}",
"hashtag.column_header.tag_mode.none": "{additional}නොමැතිව",
"hashtag.column_settings.select.no_options_message": "යෝජනා හමු නොවිණි", "hashtag.column_settings.select.no_options_message": "යෝජනා හමු නොවිණි",
"hashtag.column_settings.select.placeholder": "හැෂ් ටැග්…ඇතුලත් කරන්න",
"hashtag.column_settings.tag_mode.all": "මේ සියල්ලම", "hashtag.column_settings.tag_mode.all": "මේ සියල්ලම",
"hashtag.column_settings.tag_mode.any": "ඇතුළත් එකක්",
"hashtag.column_settings.tag_mode.none": "මේ කිසිවක් නැත", "hashtag.column_settings.tag_mode.none": "මේ කිසිවක් නැත",
"hashtag.column_settings.tag_toggle": "මෙම තීරුවේ අමතර ටැග් ඇතුළත් කරන්න", "hashtag.column_settings.tag_toggle": "මෙම තීරුවේ අමතර ටැග් ඇතුළත් කරන්න",
"home.actions.go_to_explore": "නැගී එන දෑ බලන්න",
"home.actions.go_to_suggestions": "පුද්ගලයින් සොයන්න",
"home.column_settings.basic": "මූලික", "home.column_settings.basic": "මූලික",
"home.column_settings.show_reblogs": "බූස්ට් පෙන්වන්න",
"home.column_settings.show_replies": "පිළිතුරු පෙන්වන්න", "home.column_settings.show_replies": "පිළිතුරු පෙන්වන්න",
"home.explore_prompt.title": "මෙය ඔබගේ මාස්ටඩන් මුල් පිටුවයි.",
"home.hide_announcements": "නිවේදන සඟවන්න", "home.hide_announcements": "නිවේදන සඟවන්න",
"home.pending_critical_update.link": "යාවත්කාල බලන්න",
"home.show_announcements": "නිවේදන පෙන්වන්න", "home.show_announcements": "නිවේදන පෙන්වන්න",
"intervals.full.days": "{number, plural, one {# දින} other {# දින}}", "interaction_modal.on_this_server": "මෙම සේවාදායකයෙහි",
"intervals.full.hours": "{number, plural, one {# පැය} other {# පැය}}", "intervals.full.days": "{number, plural, one {දවස් #} other {දවස් #}}",
"intervals.full.minutes": "{number, plural, one {විනාඩි #} other {# මිනිත්තු}}", "intervals.full.hours": "{number, plural, one {පැය #} other {පැය #}}",
"intervals.full.minutes": "{number, plural, one {විනාඩි #} other {විනාඩි #}}",
"keyboard_shortcuts.back": "ආපසු යාත්‍රණය", "keyboard_shortcuts.back": "ආපසු යාත්‍රණය",
"keyboard_shortcuts.blocked": "අවහිර කළ පරිශීලක ලැයිස්තුව විවෘත කිරීමට",
"keyboard_shortcuts.boost": "වැඩි කිරීමට",
"keyboard_shortcuts.column": "එක් තීරුවක තත්ත්වය නාභිගත කිරීමට",
"keyboard_shortcuts.compose": "රචනා පාඨ ප්‍රදේශය නාභිගත කිරීමට",
"keyboard_shortcuts.description": "සවිස්තරය", "keyboard_shortcuts.description": "සවිස්තරය",
"keyboard_shortcuts.direct": "to open direct messages column", "keyboard_shortcuts.down": "ලැයිස්තුවේ පහළට ගෙනයන්න",
"keyboard_shortcuts.down": "ලැයිස්තුවේ පහළට ගමන් කිරීමට",
"keyboard_shortcuts.enter": "ලිපිය අරින්න", "keyboard_shortcuts.enter": "ලිපිය අරින්න",
"keyboard_shortcuts.favourites": "ප්‍රියතමයන් ලැයිස්තුව අරින්න",
"keyboard_shortcuts.federated": "ෆෙඩරේටඩ් කාලරාමුව විවෘත කිරීමට", "keyboard_shortcuts.federated": "ෆෙඩරේටඩ් කාලරාමුව විවෘත කිරීමට",
"keyboard_shortcuts.heading": "යතුරුපුවරු කෙටිමං", "keyboard_shortcuts.heading": "යතුරුපුවරු කෙටිමං",
"keyboard_shortcuts.home": "නිවසේ කාලරේඛාව විවෘත කිරීමට",
"keyboard_shortcuts.hotkey": "උණු යතුර", "keyboard_shortcuts.hotkey": "උණු යතුර",
"keyboard_shortcuts.legend": "මෙම පුරාවෘත්තය ප්රදර්ශනය කිරීමට",
"keyboard_shortcuts.local": "දේශීය කාලරේඛාව විවෘත කිරීමට", "keyboard_shortcuts.local": "දේශීය කාලරේඛාව විවෘත කිරීමට",
"keyboard_shortcuts.mention": "කතුවරයා සඳහන් කිරීමට", "keyboard_shortcuts.mention": "කතුවරයා සඳහන් කිරීමට",
"keyboard_shortcuts.muted": "නිශ්ශබ්ද පරිශීලක ලැයිස්තුව විවෘත කිරීමට", "keyboard_shortcuts.muted": "නිහඬ කළ අය පෙන්වන්න",
"keyboard_shortcuts.my_profile": "ඔබගේ පැතිකඩ අරින්න", "keyboard_shortcuts.my_profile": "ඔබගේ පැතිකඩ අරින්න",
"keyboard_shortcuts.notifications": "දැනුම්දීම් තීරුව විවෘත කිරීමට", "keyboard_shortcuts.notifications": "දැනුම්දීම් තීරුව විවෘත කිරීමට",
"keyboard_shortcuts.open_media": "මාධ්‍ය අරින්න", "keyboard_shortcuts.open_media": "මාධ්‍ය අරින්න",
"keyboard_shortcuts.pinned": "ඇමිණූ ලිපි ලේඛනය අරින්න", "keyboard_shortcuts.pinned": "ඇමිණූ ලිපි ලැයිස්තුව අරින්න",
"keyboard_shortcuts.profile": "කතෘගේ පැතිකඩ අරින්න", "keyboard_shortcuts.profile": "කතෘගේ පැතිකඩ අරින්න",
"keyboard_shortcuts.reply": "පිළිතුරු දීමට", "keyboard_shortcuts.reply": "පිළිතුරු දීමට",
"keyboard_shortcuts.requests": "පහත ඉල්ලීම් ලැයිස්තුව විවෘත කිරීමට", "keyboard_shortcuts.spoilers": "CW ක්‍ෂේත්‍රය පෙන්වන්න/සඟවන්න",
"keyboard_shortcuts.search": "සෙවුම් අවධානය යොමු කිරීමට",
"keyboard_shortcuts.spoilers": "CW ක්ෂේත්‍රය පෙන්වීමට/සැඟවීමට",
"keyboard_shortcuts.start": "\"පටන් ගන්න\" තීරුව අරින්න", "keyboard_shortcuts.start": "\"පටන් ගන්න\" තීරුව අරින්න",
"keyboard_shortcuts.toggle_hidden": "CW පිටුපස පෙළ පෙන්වීමට/සැඟවීමට",
"keyboard_shortcuts.toggle_sensitivity": "මාධ්‍ය පෙන්වන්න/සඟවන්න", "keyboard_shortcuts.toggle_sensitivity": "මාධ්‍ය පෙන්වන්න/සඟවන්න",
"keyboard_shortcuts.toot": "නව ලිපියක් අරඹන්න", "keyboard_shortcuts.toot": "නව ලිපියක් අරඹන්න",
"keyboard_shortcuts.unfocus": "අවධානය යොමු නොකිරීමට textarea/search රචනා කරන්න", "keyboard_shortcuts.up": "ලැයිස්තුවේ ඉහළට ගෙනයන්න",
"keyboard_shortcuts.up": "ලැයිස්තුවේ ඉහළට යාමට",
"lightbox.close": "වසන්න", "lightbox.close": "වසන්න",
"lightbox.compress": "රූප බැලීමේ කොටුව සම්පීඩනය කරන්න",
"lightbox.expand": "රූප දර්ශන පෙට්ටිය දිග හරින්න",
"lightbox.next": "ඊළඟ", "lightbox.next": "ඊළඟ",
"lightbox.previous": "පෙර", "lightbox.previous": "පෙර",
"limited_account_hint.action": "කෙසේ හෝ පැතිකඩ පෙන්වන්න", "limited_account_hint.action": "කෙසේ හෝ පැතිකඩ පෙන්වන්න",
"lists.account.add": "ලේඛනයට දමන්න", "lists.account.add": "ලැයිස්තුවට දමන්න",
"lists.account.remove": "ලේඛනයෙන් ඉවතලන්න", "lists.account.remove": "ලැයිස්තුවෙන් ඉවතලන්න",
"lists.delete": "ලේඛනය මකන්න", "lists.delete": "ලැයිස්තුව මකන්න",
"lists.edit": "ලේඛනය සංස්කරණය", "lists.edit": "ලැයිස්තුව සංස්කරණය",
"lists.edit.submit": "සිරැසිය සංශෝධනය", "lists.edit.submit": "සිරැසිය සංශෝධනය",
"lists.new.create": "ලැයිස්තුව එකතු කරන්න", "lists.new.title_placeholder": "නව ලැයිස්තුවේ සිරැසිය",
"lists.new.title_placeholder": "නව ලැයිස්තු මාතෘකාව", "lists.replies_policy.list": "ලැයිස්තුවේ සාමාජිකයින්",
"lists.replies_policy.followed": "අනුගමනය කරන ඕනෑම පරිශීලකයෙක්",
"lists.replies_policy.list": "ලැයිස්තුවේ සාමාජිකයන්",
"lists.replies_policy.none": "කිසිවෙක් නැත", "lists.replies_policy.none": "කිසිවෙක් නැත",
"lists.replies_policy.title": "පිළිතුරු පෙන්වන්න:", "lists.replies_policy.title": "පිළිතුරු පෙන්වන්න:",
"lists.search": "ඔබ අනුගමනය කරන පුද්ගලයින් අතර සොයන්න", "lists.subheading": "ඔබගේ ලැයිස්තු",
"lists.subheading": "ඔබගේ ලේඛන",
"load_pending": "{count, plural, one {# නව අයිතමයක්} other {නව අයිතම #ක්}}",
"loading_indicator.label": "පූරණය වෙමින්...", "loading_indicator.label": "පූරණය වෙමින්...",
"media_gallery.toggle_visible": "{number, plural, one {රූපය සඟවන්න} other {පින්තූර සඟවන්න}}",
"mute_modal.duration": "පරාසය", "mute_modal.duration": "පරාසය",
"mute_modal.hide_notifications": "මෙම පරිශීලකයාගෙන් දැනුම්දීම් සඟවන්නද?", "mute_modal.hide_notifications": "මෙම පුද්ගලයාගේ දැනුම්දීම් සඟවන්නද?",
"mute_modal.indefinite": "අවිනිශ්චිත", "navigation_bar.about": "පිළිබඳව",
"navigation_bar.blocks": "අවහිර කළ අය", "navigation_bar.blocks": "අවහිර කළ අය",
"navigation_bar.bookmarks": "පොත්යොමු", "navigation_bar.bookmarks": "පොත්යොමු",
"navigation_bar.community_timeline": "දේශීය කාලරේඛාව", "navigation_bar.community_timeline": "ස්ථානීය කාලරේඛාව",
"navigation_bar.compose": "නව ටූට් සාදන්න", "navigation_bar.compose": "නව ලිපියක් ලියන්න",
"navigation_bar.discover": "සොයා ගන්න", "navigation_bar.direct": "පෞද්ගලික සැඳහුම්",
"navigation_bar.domain_blocks": "අවහිර කළ වසම්", "navigation_bar.domain_blocks": "අවහිර කළ වසම්",
"navigation_bar.edit_profile": "පැතිකඩ සංස්කරණය", "navigation_bar.edit_profile": "පැතිකඩ සංස්කරණය",
"navigation_bar.explore": "ගවේෂණය කරන්න", "navigation_bar.explore": "ගවේශනය",
"navigation_bar.favourites": "ප්‍රියතමයන්",
"navigation_bar.filters": "නිහඬ කළ වචන", "navigation_bar.filters": "නිහඬ කළ වචන",
"navigation_bar.follow_requests": "අනුගමන ඉල්ලීම්", "navigation_bar.follow_requests": "අනුගමන ඉල්ලීම්",
"navigation_bar.follows_and_followers": "අනුගමන හා අනුගාමිකයින්", "navigation_bar.follows_and_followers": "අනුගමන හා අනුගාමික",
"navigation_bar.lists": "ලේඛන", "navigation_bar.lists": "ලැයිස්තු",
"navigation_bar.logout": "නික්මෙන්න", "navigation_bar.logout": "නික්මෙන්න",
"navigation_bar.mutes": "නිහඬ කළ අය", "navigation_bar.mutes": "නිහඬ කළ අය",
"navigation_bar.personal": "පුද්ගලික", "navigation_bar.personal": "පුද්ගලික",
"navigation_bar.pins": "ඇමිණූ ලිපි", "navigation_bar.pins": "ඇමිණූ ලිපි",
"navigation_bar.preferences": "අභිප්‍රේත", "navigation_bar.preferences": "අභිප්‍රේත",
"navigation_bar.public_timeline": "ෆෙඩරේටඩ් කාලරේඛාව", "navigation_bar.public_timeline": "ඒකාබද්ධ කාලරේඛාව",
"navigation_bar.search": "සොයන්න",
"navigation_bar.security": "ආරක්ෂාව", "navigation_bar.security": "ආරක්ෂාව",
"not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.", "not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.",
"notification.admin.report": "{name} වාර්තා {target}",
"notification.admin.sign_up": "{name} අත්සන් කර ඇත",
"notification.follow": "{name} ඔබව අනුගමනය කළා", "notification.follow": "{name} ඔබව අනුගමනය කළා",
"notification.follow_request": "{name} ඔබව අනුගමනය කිරීමට ඉල්ලා ඇත",
"notification.mention": "{name} ඔබව සඳහන් කර ඇත", "notification.mention": "{name} ඔබව සඳහන් කර ඇත",
"notification.own_poll": "ඔබගේ මත විමසුම නිමයි", "notification.own_poll": "ඔබගේ මත විමසුම නිමයි",
"notification.poll": "ඔබ ඡන්දය දුන් මත විමසුමක් නිමයි", "notification.poll": "ඔබ ඡන්දය දුන් මත විමසුමක් නිමයි",
"notification.reblog": "{name} ඔබේ තත්ත්වය ඉහළ නැංවීය",
"notification.status": "{name} දැන් පළ කළා", "notification.status": "{name} දැන් පළ කළා",
"notification.update": "{name} පළ කිරීමක් සංස්කරණය කළා", "notification.update": "{name} ලිපියක් සංස්කරණය කළා",
"notifications.clear": "දැනුම්දීම් මකන්න", "notifications.clear": "දැනුම්දීම් මකන්න",
"notifications.clear_confirmation": "ඔබට ඔබගේ සියලු දැනුම්දීම් ස්ථිරවම හිස් කිරීමට අවශ්‍ය බව විශ්වාසද?", "notifications.clear_confirmation": "දැනුම්දීම් සියල්ල හිස් කිරීමට වුවමනා ද?",
"notifications.column_settings.admin.report": "නව වාර්තා:", "notifications.column_settings.admin.report": "නව වාර්තා:",
"notifications.column_settings.admin.sign_up": "නව ලියාපදිංචි:", "notifications.column_settings.admin.sign_up": "නව ලියාපදිංචි:",
"notifications.column_settings.alert": "වැඩතල දැනුම්දීම්", "notifications.column_settings.alert": "වැඩතල දැනුම්දීම්",
"notifications.column_settings.favourite": "ප්‍රියතමයන්:",
"notifications.column_settings.filter_bar.advanced": "සියළු ප්‍රවර්ග පෙන්වන්න", "notifications.column_settings.filter_bar.advanced": "සියළු ප්‍රවර්ග පෙන්වන්න",
"notifications.column_settings.filter_bar.category": "ඉක්මන් පෙරහන් තීරුව", "notifications.column_settings.filter_bar.category": "ඉක්මන් පෙරහන් තීරුව",
"notifications.column_settings.filter_bar.show_bar": "පෙරහන් තීරුව පෙන්වන්න", "notifications.column_settings.filter_bar.show_bar": "පෙරහන් තීරුව පෙන්වන්න",
"notifications.column_settings.follow": "නව අනුගාමිකයින්:", "notifications.column_settings.follow": "නව අනුගාමිකයින්:",
"notifications.column_settings.follow_request": "නව අනුගමන ඉල්ලීම්:", "notifications.column_settings.follow_request": "නව අනුගමන ඉල්ලීම්:",
"notifications.column_settings.mention": "සැඳහුම්:", "notifications.column_settings.mention": "සැඳහුම්:",
"notifications.column_settings.poll": "ඡන්ද ප්‍රතිඵල:", "notifications.column_settings.poll": "මත විමසුමේ ප්‍රතිඵල:",
"notifications.column_settings.push": "තල්ලු දැනුම්දීම්", "notifications.column_settings.push": "තල්ලු දැනුම්දීම්",
"notifications.column_settings.reblog": "තල්ලු කිරීම්:",
"notifications.column_settings.show": "තීරුවෙහි පෙන්වන්න", "notifications.column_settings.show": "තීරුවෙහි පෙන්වන්න",
"notifications.column_settings.sound": "ශබ්දය වාදනය", "notifications.column_settings.sound": "ශබ්දය වාදනය",
"notifications.column_settings.status": "නව ලිපි:", "notifications.column_settings.status": "නව ලිපි:",
@ -319,38 +311,26 @@
"notifications.column_settings.unread_notifications.highlight": "නොකියවූ දැනුම්දීම් ඉස්මතු කරන්න", "notifications.column_settings.unread_notifications.highlight": "නොකියවූ දැනුම්දීම් ඉස්මතු කරන්න",
"notifications.column_settings.update": "සංශෝධන:", "notifications.column_settings.update": "සංශෝධන:",
"notifications.filter.all": "සියල්ල", "notifications.filter.all": "සියල්ල",
"notifications.filter.boosts": "බූස්ට් කරයි", "notifications.filter.favourites": "ප්‍රියතමයන්",
"notifications.filter.follows": "අනුගමනය", "notifications.filter.follows": "අනුගමනය",
"notifications.filter.mentions": "සැඳහුම්", "notifications.filter.mentions": "සැඳහුම්",
"notifications.filter.polls": "ඡන්ද ප්‍රතිඵල", "notifications.filter.polls": "මත විමසුමේ ප්‍රතිඵල",
"notifications.filter.statuses": "ඔබ අනුගමනය කරන පුද්ගලයින්ගෙන් යාවත්කාලීන",
"notifications.grant_permission": "අවසර දෙන්න.",
"notifications.group": "දැනුම්දීම් {count}", "notifications.group": "දැනුම්දීම් {count}",
"notifications.mark_as_read": "සියළු දැනුම්දීම් කියවූ බව යොදන්න", "notifications.mark_as_read": "සියළු දැනුම්දීම් කියවූ බව යොදන්න",
"notifications.permission_denied": "කලින් ප්‍රතික්ෂේප කළ බ්‍රවුසර අවසර ඉල්ලීම හේතුවෙන් ඩෙස්ක්ටොප් දැනුම්දීම් නොමැත",
"notifications.permission_denied_alert": "බ්‍රවුසර අවසරය පෙර ප්‍රතික්ෂේප කර ඇති බැවින්, ඩෙස්ක්ටොප් දැනුම්දීම් සබල කළ නොහැක",
"notifications.permission_required": "අවශ්‍ය අවසරය ලබා දී නොමැති නිසා ඩෙස්ක්ටොප් දැනුම්දීම් නොමැත.",
"notifications_permission_banner.enable": "වැඩතල දැනුම්දීම් සබල කරන්න", "notifications_permission_banner.enable": "වැඩතල දැනුම්දීම් සබල කරන්න",
"notifications_permission_banner.how_to_control": "Mastodon විවෘතව නොමැති විට දැනුම්දීම් ලබා ගැනීමට, ඩෙස්ක්ටොප් දැනුම්දීම් සබල කරන්න. ඔබට ඒවා සක්‍රිය කළ පසු ඉහත {icon} බොත්තම හරහා ඩෙස්ක්ටොප් දැනුම්දීම් ජනනය කරන්නේ කුමන ආකාරයේ අන්තර්ක්‍රියාද යන්න නිවැරදිව පාලනය කළ හැක.", "notifications_permission_banner.title": "කිසිවක් අතපසු නොකරන්න",
"notifications_permission_banner.title": "කිසිම දෙයක් අතපසු කරන්න එපා", "onboarding.compose.template": "ආයුබෝ #මාස්ටඩන්!",
"onboarding.actions.go_to_explore": "See what's trending", "onboarding.share.title": "ඔබගේ පැතිකඩ බෙදාගන්න",
"onboarding.actions.go_to_home": "Go to your home feed", "onboarding.steps.publish_status.title": "පළමු ලිපිය පළ කරන්න",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!", "onboarding.steps.setup_profile.title": "ඔබගේ පැතිකඩ අභිරුචිකරණය",
"onboarding.follows.title": "Popular on Mastodon", "onboarding.steps.share_profile.body": "මාස්ටඩන් හි ඔබව සොයා ගන්නේ කෙසේදැයි යහළුවන්ට දන්වන්න",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:", "onboarding.steps.share_profile.title": "ඔබගේ පැතිකඩ බෙදාගන්න",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Say hello to the world.",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile",
"poll.closed": "වසා ඇත", "poll.closed": "වසා ඇත",
"poll.refresh": "නැවුම් කරන්න", "poll.refresh": "නැවුම් කරන්න",
"poll.reveal": "ප්‍රතිඵල බලන්න",
"poll.vote": "ඡන්දය", "poll.vote": "ඡන්දය",
"poll.voted": "ඔබ මෙම උත්තරයට ඡන්දය දී ඇත", "poll.voted": "ඔබ මෙම උත්තරයට ඡන්දය දී ඇත",
"poll_button.add_poll": "මත විමසුමක් යොදන්න", "poll_button.add_poll": "මත විමසුමක් අරඹන්න",
"poll_button.remove_poll": "මත විමසුම ඉවතලන්න", "poll_button.remove_poll": "මත විමසුම ඉවතලන්න",
"privacy.change": "ලිපියේ රහස්‍යතාව සංශෝධනය", "privacy.change": "ලිපියේ රහස්‍යතාව සංශෝධනය",
"privacy.direct.long": "සඳහන් කළ අයට දිස්වෙයි", "privacy.direct.long": "සඳහන් කළ අයට දිස්වෙයි",
@ -363,6 +343,7 @@
"refresh": "නැවුම් කරන්න", "refresh": "නැවුම් කරන්න",
"regeneration_indicator.label": "පූරණය වෙමින්…", "regeneration_indicator.label": "පූරණය වෙමින්…",
"relative_time.days": "ද. {number}", "relative_time.days": "ද. {number}",
"relative_time.full.days": "{number, plural, one {දවස් #} other {දවස් #}} කට පෙර",
"relative_time.full.hours": "{number, plural, one {පැය #} other {පැය #}} කට පෙර", "relative_time.full.hours": "{number, plural, one {පැය #} other {පැය #}} කට පෙර",
"relative_time.full.just_now": "මේ දැන්", "relative_time.full.just_now": "මේ දැන්",
"relative_time.full.minutes": "{number, plural, one {විනාඩි #} other {විනාඩි #}} කට පෙර", "relative_time.full.minutes": "{number, plural, one {විනාඩි #} other {විනාඩි #}} කට පෙර",
@ -377,21 +358,20 @@
"report.categories.other": "වෙනත්", "report.categories.other": "වෙනත්",
"report.categories.spam": "ආයාචිත", "report.categories.spam": "ආයාචිත",
"report.categories.violation": "අන්තර්ගතය නිසා සේවාදායකයේ නීතියක් හෝ කිහිපයක් කඩ වේ", "report.categories.violation": "අන්තර්ගතය නිසා සේවාදායකයේ නීතියක් හෝ කිහිපයක් කඩ වේ",
"report.category.subtitle": "හොඳම ගැලපීම තෝරන්න",
"report.category.title": "මෙම {type}සමඟ සිදුවන්නේ කුමක්දැයි අපට කියන්න", "report.category.title": "මෙම {type}සමඟ සිදුවන්නේ කුමක්දැයි අපට කියන්න",
"report.category.title_account": "පැතිකඩ", "report.category.title_account": "පැතිකඩ",
"report.category.title_status": "තැපැල්", "report.category.title_status": "ලිපිය",
"report.close": "අහවරයි", "report.close": "අහවරයි",
"report.comment.title": "අප දැනගත යුතු යැයි ඔබ සිතන තවත් යමක් තිබේද?", "report.comment.title": "අප දැනගත යුතු යැයි ඔබ සිතන තවත් යමක් තිබේද?",
"report.forward": "{target} වෙත හරවන්න", "report.forward": "{target} වෙත හරවන්න",
"report.forward_hint": "ගිණුම වෙනත් සේවාදායකයකින්. වාර්තාවේ නිර්නාමික පිටපතක් එතනටත් එවන්න?", "report.forward_hint": "ගිණුම වෙනත් සේවාදායකයකින්. වාර්තාවේ නිර්නාමික පිටපතක් එතනටත් එවන්න?",
"report.mute": "නිහඬ", "report.mute": "නිහඬ",
"report.mute_explanation": "ඔබට ඔවුන්ගේ පෝස්ට් නොපෙනේ. ඔවුන්ට තවමත් ඔබව අනුගමනය කිරීමට සහ ඔබේ පළ කිරීම් දැකීමට හැකි අතර ඒවා නිශ්ශබ්ද කර ඇති බව නොදැනේ.", "report.mute_explanation": "ඔබ ඔවුන්ගේ ලිපි නොදකිනු ඇත. ඔවුන්ට තවමත් ඔබව අනුගමනයට සහ ඔබගේ ලිපි දැකීමට හැකි අතර ඔවුන්ව නිහඬ කර ඇති බව දැන ගැනීමට නොහැකිය.",
"report.next": "ඊළඟ", "report.next": "ඊළඟ",
"report.placeholder": "අමතර අදහස්", "report.placeholder": "අමතර අදහස්",
"report.reasons.dislike": "මම එයට අකැමතියි", "report.reasons.dislike": "මම එයට අකැමතියි",
"report.reasons.dislike_description": "ඒක බලන්න ඕන දෙයක් නෙවෙයි", "report.reasons.dislike_description": "ඒක බලන්න ඕන දෙයක් නෙවෙයි",
"report.reasons.other": "ඒක වෙන දෙයක්", "report.reasons.other": "එය වෙනත් දෙයක්",
"report.reasons.other_description": "ගැටළුව වෙනත් වර්ග වලට නොගැලපේ", "report.reasons.other_description": "ගැටළුව වෙනත් වර්ග වලට නොගැලපේ",
"report.reasons.spam": "එය අයාචිතයි", "report.reasons.spam": "එය අයාචිතයි",
"report.reasons.spam_description": "අනිෂ්ට සබැඳි, ව්‍යාජ නියැලීම, හෝ පුනරාවර්තන පිළිතුරු", "report.reasons.spam_description": "අනිෂ්ට සබැඳි, ව්‍යාජ නියැලීම, හෝ පුනරාවර්තන පිළිතුරු",
@ -400,23 +380,24 @@
"report.rules.subtitle": "අදාළ සියල්ල තෝරන්න", "report.rules.subtitle": "අදාළ සියල්ල තෝරන්න",
"report.rules.title": "කුමන නීති උල්ලංඝනය කරන්නේද?", "report.rules.title": "කුමන නීති උල්ලංඝනය කරන්නේද?",
"report.statuses.subtitle": "අදාළ සියල්ල තෝරන්න", "report.statuses.subtitle": "අදාළ සියල්ල තෝරන්න",
"report.statuses.title": "මෙම වාර්තාව උපස්ථ කරන පෝස්ට් තිබේද?", "report.statuses.title": "මෙම වාර්තාව උපස්ථ කළ ලිපි තිබේ ද?",
"report.submit": "යොමන්න", "report.submit": "යොමන්න",
"report.target": "වාර්තාව {target}", "report.target": "{target} වාර්තා කිරීම",
"report.thanks.take_action": "Mastodon හි ඔබ දකින දේ පාලනය කිරීම සඳහා ඔබේ විකල්ප මෙන්න:", "report.thanks.take_action": "මාස්ටඩන් හි ඔබ දකින දෑ පාලනයට තිබෙන විකල්ප:",
"report.thanks.take_action_actionable": "අපි මෙය සමාලෝචනය කරන අතරතුර, ඔබට @{name}ට එරෙහිව පියවර ගත හැක:", "report.thanks.take_action_actionable": "අපි මෙය සමාලෝචනය කරන අතරතුර, ඔබට @{name}ට එරෙහිව පියවර ගත හැක:",
"report.thanks.title": "මෙය නොපෙන්විය යුතුද?", "report.thanks.title": "මෙය නොපෙන්විය යුතුද?",
"report.thanks.title_actionable": "වාර්තා කිරීමට ස්තූතියි, අපි මේ ගැන සොයා බලමු.", "report.thanks.title_actionable": "වාර්තා කිරීමට ස්තූතියි, අපි මේ ගැන සොයා බලමු.",
"report.unfollow": "@{name}අනුගමනය නොකරන්න", "report.unfollow": "@{name}අනුගමනය නොකරන්න",
"report.unfollow_explanation": "ඔබ මෙම ගිණුම අනුගමනය කරයි. ඔබේ නිවසේ සංග්‍රහයේ ඔවුන්ගේ පළ කිරීම් තවදුරටත් නොදැකීමට, ඒවා අනුගමනය නොකරන්න.", "report.unfollow_explanation": "ඔබ මෙම ගිණුම අනුගමනය කරයි. ඔබගේ මුල් පිටුවේ ඔවුන්ගේ ලිපි නොදැකීමට, ඔවුන්ව තවදුරටත් අනුගමනය නොකරන්න.",
"report_notification.attached_statuses": "{count, plural, one {ලිපි {count}} other {ලිපි {count} ක්}} අමුණා ඇත", "report_notification.attached_statuses": "{count, plural, one {ලිපි {count}} other {ලිපි {count} ක්}} අමුණා ඇත",
"report_notification.categories.other": "වෙනත්", "report_notification.categories.other": "වෙනත්",
"report_notification.categories.spam": "ආයාචිත", "report_notification.categories.spam": "ආයාචිත",
"report_notification.categories.violation": "නීතිය කඩ කිරීම", "report_notification.categories.violation": "නීතිය කඩ කිරීම",
"report_notification.open": "විවෘත වාර්තාව", "report_notification.open": "විවෘත වාර්තාව",
"search.placeholder": "සොයන්න", "search.placeholder": "සොයන්න",
"search.quick_action.open_url": "ලිපිනය මාස්ටඩන්හි අරින්න",
"search.search_or_paste": "සොයන්න හෝ ඒ.ස.නි. අලවන්න",
"search_results.all": "සියල්ල", "search_results.all": "සියල්ල",
"search_results.hashtags": "හැෂ් ටැග්",
"search_results.nothing_found": "මෙම සෙවුම් පද සඳහා කිසිවක් සොයාගත නොහැකි විය", "search_results.nothing_found": "මෙම සෙවුම් පද සඳහා කිසිවක් සොයාගත නොහැකි විය",
"search_results.see_all": "සියල්ල බලන්න", "search_results.see_all": "සියල්ල බලන්න",
"search_results.statuses": "ලිපි", "search_results.statuses": "ලිපි",
@ -424,24 +405,23 @@
"server_banner.learn_more": "තව දැනගන්න", "server_banner.learn_more": "තව දැනගන්න",
"sign_in_banner.create_account": "ගිණුමක් සාදන්න", "sign_in_banner.create_account": "ගිණුමක් සාදන්න",
"sign_in_banner.sign_in": "පිවිසෙන්න", "sign_in_banner.sign_in": "පිවිසෙන්න",
"status.admin_account": "@{name}සඳහා මධ්‍යස්ථ අතුරුමුහුණත විවෘත කරන්න", "status.admin_status": "මෙම ලිපිය මැදිහත්කරණ අතුරුමුහුණතෙහි අරින්න",
"status.admin_status": "මධ්‍යස්ථ අතුරුමුහුණතෙහි මෙම තත්ත්වය විවෘත කරන්න",
"status.block": "@{name} අවහිර", "status.block": "@{name} අවහිර",
"status.bookmark": "පොත්යොමුවක්", "status.bookmark": "පොත්යොමුවක්",
"status.cannot_reblog": "මෙම තනතුර වැඩි කළ නොහැක",
"status.copy": "තත්වයට සබැඳිය පිටපත් කරන්න",
"status.delete": "මකන්න", "status.delete": "මකන්න",
"status.detailed_status": "විස්තරාත්මක සංවාද දැක්ම", "status.detailed_status": "විස්තරාත්මක සංවාද දැක්ම",
"status.edit": "සංස්කරණය", "status.edit": "සංස්කරණය",
"status.edited": "සංශෝධිතයි {date}", "status.edited": "සංශෝධිතයි {date}",
"status.edited_x_times": "සංශෝධිතයි {count, plural, one {වාර {count}} other {වාර {count}}}", "status.edited_x_times": "සංශෝධිතයි {count, plural, one {වාර {count}} other {වාර {count}}}",
"status.embed": "කාවැද්දූ", "status.embed": "කාවැද්දූ",
"status.filter": "මෙම ලිපිය පෙරන්න",
"status.filtered": "පෙරන ලද", "status.filtered": "පෙරන ලද",
"status.hide": "ලිපිය සඟවන්න",
"status.history.created": "{name} නිර්මාණය {date}", "status.history.created": "{name} නිර්මාණය {date}",
"status.history.edited": "{name} සංස්කරණය {date}", "status.history.edited": "{name} සංස්කරණය {date}",
"status.load_more": "තව පූරණය", "status.load_more": "තව පූරණය",
"status.media_hidden": "මාධ්‍ය සඟවා ඇත", "status.media_hidden": "මාධ්‍ය සඟවා ඇත",
"status.mention": "@{name} සැඳහුම", "status.mention": "@{name} සඳහන් කරන්ක",
"status.more": "තව", "status.more": "තව",
"status.mute": "@{name} නිහඬව", "status.mute": "@{name} නිහඬව",
"status.mute_conversation": "සංවාදය නිහඬව", "status.mute_conversation": "සංවාදය නිහඬව",
@ -449,14 +429,10 @@
"status.pin": "පැතිකඩට අමුණන්න", "status.pin": "පැතිකඩට අමුණන්න",
"status.pinned": "ඇමිණූ ලිපියකි", "status.pinned": "ඇමිණූ ලිපියකි",
"status.read_more": "තව කියවන්න", "status.read_more": "තව කියවන්න",
"status.reblog": "බූස්ට් කරන්න",
"status.reblog_private": "මුල් දෘශ්‍යතාව සමඟ වැඩි කරන්න",
"status.reblogs.empty": "තාම කවුරුත් මේ toot එක boost කරලා නැහැ. යමෙකු එසේ කළ විට, ඔවුන් මෙහි පෙන්වනු ඇත.",
"status.redraft": "මකන්න සහ නැවත කෙටුම්පත",
"status.remove_bookmark": "පොත්යොමුව ඉවතලන්න", "status.remove_bookmark": "පොත්යොමුව ඉවතලන්න",
"status.reply": "පිළිතුරු", "status.reply": "පිළිතුරු",
"status.replyAll": "නූලට පිළිතුරු දෙන්න", "status.replyAll": "නූලට පිළිතුරු දෙන්න",
"status.report": "@{name} වාර්තා", "status.report": "@{name} වාර්තා කරන්න",
"status.sensitive_warning": "සංවේදී අන්තර්ගතයකි", "status.sensitive_warning": "සංවේදී අන්තර්ගතයකි",
"status.share": "බෙදාගන්න", "status.share": "බෙදාගන්න",
"status.show_filter_reason": "කෙසේ වුවද පෙන්වන්න", "status.show_filter_reason": "කෙසේ වුවද පෙන්වන්න",
@ -464,29 +440,29 @@
"status.show_less_all": "සියල්ල අඩුවෙන් පෙන්වන්න", "status.show_less_all": "සියල්ල අඩුවෙන් පෙන්වන්න",
"status.show_more": "තවත් පෙන්වන්න", "status.show_more": "තවත් පෙන්වන්න",
"status.show_more_all": "සියල්ල වැඩියෙන් පෙන්වන්න", "status.show_more_all": "සියල්ල වැඩියෙන් පෙන්වන්න",
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}", "status.translate": "පරිවර්තනය",
"status.translated_from_with": "{provider} මගින් {lang} භාෂාවෙන් පරිවර්තනය කර ඇත",
"status.uncached_media_warning": "පෙරදසුන නැත",
"status.unmute_conversation": "සංවාදය නොනිහඬ", "status.unmute_conversation": "සංවාදය නොනිහඬ",
"status.unpin": "පැතිකඩෙන් ගළවන්න", "status.unpin": "පැතිකඩෙන් ගළවන්න",
"subscribed_languages.save": "වෙනස්කම් සුරකින්න", "subscribed_languages.save": "වෙනස්කම් සුරකින්න",
"tabs_bar.home": "මුල් පිටුව", "tabs_bar.home": "මුල් පිටුව",
"tabs_bar.notifications": "දැනුම්දීම්", "tabs_bar.notifications": "දැනුම්දීම්",
"time_remaining.days": "{number, plural, one {# දින} other {# දින}} අත්හැරියා", "time_remaining.days": "{number, plural, one {දවස් #} other {දවස් #}} ක් ඉතිරිය",
"time_remaining.hours": "{number, plural, one {# පැය} other {# පැය}} අත්හැරියා", "time_remaining.hours": "{number, plural, one {පැය #} other {පැය #}} ක් ඉතිරිය",
"time_remaining.minutes": "{number, plural, one {විනාඩි #} other {# මිනිත්තු}} අත්හැරියා", "time_remaining.minutes": "{number, plural, one {විනාඩි #} other {විනාඩි #}} ක් ඉතිරිය",
"time_remaining.moments": "ඉතිරිව ඇති මොහොත", "time_remaining.seconds": "{number, plural, one {තත්පර #} other {තත්පර #}} ක් ඉතිරිය",
"time_remaining.seconds": "{number, plural, one {# දෙවැනි} other {# තත්පර}} අත්හැරියා",
"timeline_hint.remote_resource_not_displayed": "වෙනත් සේවාදායකයන්ගෙන් {resource} දර්ශනය නොවේ.", "timeline_hint.remote_resource_not_displayed": "වෙනත් සේවාදායකයන්ගෙන් {resource} දර්ශනය නොවේ.",
"timeline_hint.resources.followers": "අනුගාමිකයින්", "timeline_hint.resources.followers": "අනුගාමිකයින්",
"timeline_hint.resources.follows": "අනුගමන", "timeline_hint.resources.follows": "අනුගමන",
"timeline_hint.resources.statuses": "පරණ ලිපි", "timeline_hint.resources.statuses": "පරණ ලිපි",
"trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}", "trends.trending_now": "දැන් නැගී එන",
"trends.trending_now": "දැන් ප්‍රවණතාවය",
"ui.beforeunload": "ඔබ මාස්ටඩන් හැර ගියහොත් කටුපිටපත අහිමි වේ.", "ui.beforeunload": "ඔබ මාස්ටඩන් හැර ගියහොත් කටුපිටපත අහිමි වේ.",
"units.short.billion": "{count}බී", "units.short.billion": "{count}බී",
"units.short.million": "ද.ල. {count}", "units.short.million": "ද.ල. {count}",
"units.short.thousand": "{count}කි", "units.short.thousand": "{count}කි",
"upload_area.title": "උඩුගතයට ඇද දමන්න", "upload_area.title": "උඩුගතයට ඇද දමන්න",
"upload_button.label": "රූප, දෘශ්‍යක හෝ හඬපට යොදන්න", "upload_button.label": "රූප, දෘශ්‍යක හෝ හඬපට අමුණන්න",
"upload_error.limit": "සීමාව ඉක්මවා ඇත.", "upload_error.limit": "සීමාව ඉක්මවා ඇත.",
"upload_error.poll": "මත විමසුම් සමඟ ගොනු යෙදීමට ඉඩ නොදේ.", "upload_error.poll": "මත විමසුම් සමඟ ගොනු යෙදීමට ඉඩ නොදේ.",
"upload_form.audio_description": "නොඇසෙන අය සඳහා විස්තර කරන්න", "upload_form.audio_description": "නොඇසෙන අය සඳහා විස්තර කරන්න",
@ -507,6 +483,7 @@
"upload_modal.preview_label": "පෙරදසුන ({ratio})", "upload_modal.preview_label": "පෙරදසුන ({ratio})",
"upload_progress.label": "උඩුගත වෙමින්...", "upload_progress.label": "උඩුගත වෙමින්...",
"upload_progress.processing": "සැකසෙමින්…", "upload_progress.processing": "සැකසෙමින්…",
"username.taken": "නම දැනටමත් අරගෙන ඇත",
"video.close": "දෘශ්‍යකය වසන්න", "video.close": "දෘශ්‍යකය වසන්න",
"video.download": "ගොනුව බාගන්න", "video.download": "ගොනුව බාගන්න",
"video.exit_fullscreen": "පූර්ණ තිරයෙන් පිටවන්න", "video.exit_fullscreen": "පූර්ණ තිරයෙන් පිටවන්න",

View file

@ -48,11 +48,11 @@
"account.media": "Médiá", "account.media": "Médiá",
"account.mention": "Spomeň @{name}", "account.mention": "Spomeň @{name}",
"account.moved_to": "{name} uvádza, že jeho/jej nový účet je teraz:", "account.moved_to": "{name} uvádza, že jeho/jej nový účet je teraz:",
"account.mute": "Nevšímaj si @{name}", "account.mute": "Stíš @{name}",
"account.mute_notifications_short": "Stíš oboznámenia", "account.mute_notifications_short": "Stíš oznámenia",
"account.mute_short": "Nevšímaj si", "account.mute_short": "Stíš",
"account.muted": "Nevšímaný/á", "account.muted": "Stíšený",
"account.no_bio": "Nieje uvedený žiadny popis.", "account.no_bio": "Nie je uvedený žiadny popis.",
"account.open_original_page": "Otvor pôvodnú stránku", "account.open_original_page": "Otvor pôvodnú stránku",
"account.posts": "Príspevky", "account.posts": "Príspevky",
"account.posts_with_replies": "Príspevky a odpovede", "account.posts_with_replies": "Príspevky a odpovede",
@ -307,8 +307,9 @@
"home.column_settings.basic": "Základné", "home.column_settings.basic": "Základné",
"home.column_settings.show_reblogs": "Ukáž vyzdvihnuté", "home.column_settings.show_reblogs": "Ukáž vyzdvihnuté",
"home.column_settings.show_replies": "Ukáž odpovede", "home.column_settings.show_replies": "Ukáž odpovede",
"home.explore_prompt.body": "Váš domovský informačný kanál bude obsahovať mix príspevkov z mriežok, ktoré ste sa rozhodli sledovať, ľudí, ktorých ste sa rozhodli sledovať, a príspevkov, ktoré preferujú. Ak sa vám to zdá príliš málo, možno budete chcieť:",
"home.explore_prompt.title": "Toto je tvoja domovina v rámci Mastodonu.", "home.explore_prompt.title": "Toto je tvoja domovina v rámci Mastodonu.",
"home.hide_announcements": "Skry oboznámenia", "home.hide_announcements": "Skry oznámenia",
"home.pending_critical_update.body": "Prosím aktualizuj si svoj Mastodon server, ako náhle to bude možné!", "home.pending_critical_update.body": "Prosím aktualizuj si svoj Mastodon server, ako náhle to bude možné!",
"home.pending_critical_update.link": "Pozri aktualizácie", "home.pending_critical_update.link": "Pozri aktualizácie",
"home.pending_critical_update.title": "Je dostupná kritická bezpečnostná aktualizácia!", "home.pending_critical_update.title": "Je dostupná kritická bezpečnostná aktualizácia!",

View file

@ -79,16 +79,16 @@
"admin.impact_report.instance_accounts": "Профілі облікових записів буде видалено", "admin.impact_report.instance_accounts": "Профілі облікових записів буде видалено",
"admin.impact_report.instance_followers": "Підписники, яких можуть втратити наші користувачі", "admin.impact_report.instance_followers": "Підписники, яких можуть втратити наші користувачі",
"admin.impact_report.instance_follows": "Підписники, яких можуть втратити їхні користувачі", "admin.impact_report.instance_follows": "Підписники, яких можуть втратити їхні користувачі",
"admin.impact_report.title": "Наслідки", "admin.impact_report.title": "Підсумки впливу",
"alert.rate_limited.message": "Спробуйте ще раз через {retry_time, time, medium}.", "alert.rate_limited.message": "Спробуйте ще раз за {retry_time, time, medium}.",
"alert.rate_limited.title": "Швидкість обмежена", "alert.rate_limited.title": "Швидкість обмежена",
"alert.unexpected.message": "Сталася неочікувана помилка.", "alert.unexpected.message": "Сталася неочікувана помилка.",
"alert.unexpected.title": "Ой!", "alert.unexpected.title": "Ой!",
"announcement.announcement": "Оголошення", "announcement.announcement": "Оголошення",
"attachments_list.unprocessed": "(не оброблено)", "attachments_list.unprocessed": "(не оброблено)",
"audio.hide": "Сховати аудіо", "audio.hide": "Сховати аудіо",
"autosuggest_hashtag.per_week": "{count} в тиждень", "autosuggest_hashtag.per_week": "{count} на тиждень",
"boost_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу", "boost_modal.combo": "Ви можете натиснути {combo}, щоби пропустити це наступного разу",
"bundle_column_error.copy_stacktrace": "Копіювати звіт про помилку", "bundle_column_error.copy_stacktrace": "Копіювати звіт про помилку",
"bundle_column_error.error.body": "Неможливо показати запитану сторінку. Це може бути спричинено помилкою у нашому коді, або через проблему сумісності з браузером.", "bundle_column_error.error.body": "Неможливо показати запитану сторінку. Це може бути спричинено помилкою у нашому коді, або через проблему сумісності з браузером.",
"bundle_column_error.error.title": "О, ні!", "bundle_column_error.error.title": "О, ні!",
@ -140,7 +140,7 @@
"compose.saved.body": "Допис збережено.", "compose.saved.body": "Допис збережено.",
"compose_form.direct_message_warning_learn_more": "Дізнатися більше", "compose_form.direct_message_warning_learn_more": "Дізнатися більше",
"compose_form.encryption_warning": "Дописи на Mastodon не захищені шифруванням. Не поширюйте жодну делікатну інформацію.", "compose_form.encryption_warning": "Дописи на Mastodon не захищені шифруванням. Не поширюйте жодну делікатну інформацію.",
"compose_form.hashtag_warning": ей допис не буде зображений у жодній стрічці гештеґу, оскільки він прихований. Тільки публічні дописи можуть бути знайдені за гештеґом.", "compose_form.hashtag_warning": ього допису не буде під жодним гештеґом, оскільки він не є загальнодоступним. За гештеґом можна шукати лише публічні дописи.",
"compose_form.lock_disclaimer": "Ваш обліковий запис не {locked}. Будь-який користувач може підписатися на вас та переглядати ваші дописи для підписників.", "compose_form.lock_disclaimer": "Ваш обліковий запис не {locked}. Будь-який користувач може підписатися на вас та переглядати ваші дописи для підписників.",
"compose_form.lock_disclaimer.lock": "приватний", "compose_form.lock_disclaimer.lock": "приватний",
"compose_form.placeholder": "Що у вас на думці?", "compose_form.placeholder": "Що у вас на думці?",
@ -151,7 +151,7 @@
"compose_form.poll.switch_to_multiple": "Дозволити вибір декількох відповідей", "compose_form.poll.switch_to_multiple": "Дозволити вибір декількох відповідей",
"compose_form.poll.switch_to_single": "Перемкнути у режим вибору однієї відповіді", "compose_form.poll.switch_to_single": "Перемкнути у режим вибору однієї відповіді",
"compose_form.publish": "Опублікувати", "compose_form.publish": "Опублікувати",
"compose_form.publish_form": "Опублікувати", "compose_form.publish_form": "Новий допис",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Зберегти зміни", "compose_form.save_changes": "Зберегти зміни",
"compose_form.sensitive.hide": "{count, plural, one {Позначити медіа делікатним} other {Позначити медіа делікатними}}", "compose_form.sensitive.hide": "{count, plural, one {Позначити медіа делікатним} other {Позначити медіа делікатними}}",
@ -206,7 +206,7 @@
"dismissable_banner.explore_tags": "Ці хештеги зараз набирають популярності серед людей на цьому та інших серверах децентралізованої мережі. Хештеги, які використовуються більшою кількістю людей, мають вищий рейтинг.", "dismissable_banner.explore_tags": "Ці хештеги зараз набирають популярності серед людей на цьому та інших серверах децентралізованої мережі. Хештеги, які використовуються більшою кількістю людей, мають вищий рейтинг.",
"dismissable_banner.public_timeline": "Це найновіші загальнодоступні дописи від людей в соціальній мережі, на які підписані люди в {domain}.", "dismissable_banner.public_timeline": "Це найновіші загальнодоступні дописи від людей в соціальній мережі, на які підписані люди в {domain}.",
"embed.instructions": "Вбудуйте цей допис до вашого вебсайту, скопіювавши код нижче.", "embed.instructions": "Вбудуйте цей допис до вашого вебсайту, скопіювавши код нижче.",
"embed.preview": "Ось як він виглядатиме:", "embed.preview": "Ось який вигляд це матиме:",
"emoji_button.activity": "Діяльність", "emoji_button.activity": "Діяльність",
"emoji_button.clear": "Очистити", "emoji_button.clear": "Очистити",
"emoji_button.custom": "Власні", "emoji_button.custom": "Власні",
@ -227,7 +227,7 @@
"empty_column.account_unavailable": "Профіль недоступний", "empty_column.account_unavailable": "Профіль недоступний",
"empty_column.blocks": "Ви ще не заблокували жодного користувача.", "empty_column.blocks": "Ви ще не заблокували жодного користувача.",
"empty_column.bookmarked_statuses": "У вас ще немає дописів у закладках. Коли ви щось додасте до закладок, воно з'явиться тут.", "empty_column.bookmarked_statuses": "У вас ще немає дописів у закладках. Коли ви щось додасте до закладок, воно з'явиться тут.",
"empty_column.community": "Локальна стрічка пуста. Напишіть щось, щоб розігріти народ!", "empty_column.community": "Локальна стрічка порожня. Напишіть щось, щоб розігріти народ!",
"empty_column.direct": "У вас ще немає жодних особистих згадок. Коли ви надсилаєте чи отримуєте повідомлення, воно з'явиться тут.", "empty_column.direct": "У вас ще немає жодних особистих згадок. Коли ви надсилаєте чи отримуєте повідомлення, воно з'явиться тут.",
"empty_column.domain_blocks": "Тут поки немає прихованих доменів.", "empty_column.domain_blocks": "Тут поки немає прихованих доменів.",
"empty_column.explore_statuses": "Нема нічого популярного. Подивіться пізніше!", "empty_column.explore_statuses": "Нема нічого популярного. Подивіться пізніше!",
@ -240,10 +240,10 @@
"empty_column.list": "Цей список порожній. Коли його учасники додадуть нові дописи, вони з'являться тут.", "empty_column.list": "Цей список порожній. Коли його учасники додадуть нові дописи, вони з'являться тут.",
"empty_column.lists": "У вас ще немає списків. Коли ви їх створите, вони з'являться тут.", "empty_column.lists": "У вас ще немає списків. Коли ви їх створите, вони з'являться тут.",
"empty_column.mutes": "Ви ще не приховали жодного користувача.", "empty_column.mutes": "Ви ще не приховали жодного користувача.",
"empty_column.notifications": "У вас ще немає сповіщень. Переписуйтесь з іншими користувачами, щоб почати розмову.", "empty_column.notifications": "У вас ще немає сповіщень. Коли інші люди почнуть взаємодіяти з вами, ви побачите їх тут.",
"empty_column.public": "Тут поки нічого немає! Опублікуйте щось, або вручну підпишіться на користувачів інших серверів, щоб заповнити стрічку", "empty_column.public": "Тут поки нічого немає! Опублікуйте щось, або вручну підпишіться на користувачів інших серверів, щоб заповнити стрічку",
"error.unexpected_crash.explanation": "Через помилку у нашому коді або несумісність браузера, ця сторінка не може бути зображена коректно.", "error.unexpected_crash.explanation": "Через помилку у нашому коді або несумісність браузера, ця сторінка не може бути зображена коректно.",
"error.unexpected_crash.explanation_addons": "Неможливо правильно показати цю сторінку. Ймовірно, цю помилку викликано додатком браузера або автоматичним засобом перекладу.", "error.unexpected_crash.explanation_addons": "Неможливо правильно показати цю сторінку. Ймовірно, цю помилку спричинило розширення браузера або автоматичний засіб перекладу.",
"error.unexpected_crash.next_steps": "Спробуйте перезавантажити сторінку. Якщо це не допоможе, ви все ще зможете використовувати Mastodon через інший браузер або рідний застосунок.", "error.unexpected_crash.next_steps": "Спробуйте перезавантажити сторінку. Якщо це не допоможе, ви все ще зможете використовувати Mastodon через інший браузер або рідний застосунок.",
"error.unexpected_crash.next_steps_addons": "Спробуйте їх вимкнути та оновити сторінку. Якщо це не допомагає, ви можете використовувати Mastodon через інший браузер або окремий застосунок.", "error.unexpected_crash.next_steps_addons": "Спробуйте їх вимкнути та оновити сторінку. Якщо це не допомагає, ви можете використовувати Mastodon через інший браузер або окремий застосунок.",
"errors.unexpected_crash.copy_stacktrace": "Скопіювати трасування стека у буфер обміну", "errors.unexpected_crash.copy_stacktrace": "Скопіювати трасування стека у буфер обміну",
@ -394,7 +394,7 @@
"moved_to_account_banner.text": "Ваш обліковий запис {disabledAccount} наразі вимкнений, оскільки вас перенесено до {movedToAccount}.", "moved_to_account_banner.text": "Ваш обліковий запис {disabledAccount} наразі вимкнений, оскільки вас перенесено до {movedToAccount}.",
"mute_modal.duration": "Тривалість", "mute_modal.duration": "Тривалість",
"mute_modal.hide_notifications": "Сховати сповіщення цього користувача?", "mute_modal.hide_notifications": "Сховати сповіщення цього користувача?",
"mute_modal.indefinite": "Назавжди", "mute_modal.indefinite": "Невизначений строк",
"navigation_bar.about": "Про застосунок", "navigation_bar.about": "Про застосунок",
"navigation_bar.advanced_interface": "Відкрити в розширеному вебінтерфейсі", "navigation_bar.advanced_interface": "Відкрити в розширеному вебінтерфейсі",
"navigation_bar.blocks": "Заблоковані користувачі", "navigation_bar.blocks": "Заблоковані користувачі",
@ -428,7 +428,7 @@
"notification.follow": "{name} підписалися на вас", "notification.follow": "{name} підписалися на вас",
"notification.follow_request": "{name} відправили запит на підписку", "notification.follow_request": "{name} відправили запит на підписку",
"notification.mention": "{name} згадали вас", "notification.mention": "{name} згадали вас",
"notification.own_poll": "Ваше опитування завершено", "notification.own_poll": "Ваше опитування завершилося",
"notification.poll": "Опитування, у якому ви голосували, скінчилося", "notification.poll": "Опитування, у якому ви голосували, скінчилося",
"notification.reblog": "{name} поширює ваш допис", "notification.reblog": "{name} поширює ваш допис",
"notification.status": "{name} щойно дописує", "notification.status": "{name} щойно дописує",
@ -480,7 +480,7 @@
"onboarding.follows.title": "Персоналізуйте домашню стрічку", "onboarding.follows.title": "Персоналізуйте домашню стрічку",
"onboarding.share.lead": "Розкажіть людям про те, як вони можуть знайти вас на Mastodon!", "onboarding.share.lead": "Розкажіть людям про те, як вони можуть знайти вас на Mastodon!",
"onboarding.share.message": "Я {username} на #Mastodon! Стежте за мною на {url}", "onboarding.share.message": "Я {username} на #Mastodon! Стежте за мною на {url}",
"onboarding.share.next_steps": "Можливі наступні кроки:", "onboarding.share.next_steps": "Можливі такі кроки:",
"onboarding.share.title": "Поділитися своїм профілем", "onboarding.share.title": "Поділитися своїм профілем",
"onboarding.start.lead": "Тепер ви — частина Mastodon, унікальної децентралізованої платформи соціальних медіа, де ви, а не алгоритми керують вашими вподобаннями. Розпочнімо роботу:", "onboarding.start.lead": "Тепер ви — частина Mastodon, унікальної децентралізованої платформи соціальних медіа, де ви, а не алгоритми керують вашими вподобаннями. Розпочнімо роботу:",
"onboarding.start.skip": "Хочете пропустити?", "onboarding.start.skip": "Хочете пропустити?",
@ -694,7 +694,7 @@
"units.short.thousand": "{count} тис", "units.short.thousand": "{count} тис",
"upload_area.title": "Перетягніть сюди, щоб завантажити", "upload_area.title": "Перетягніть сюди, щоб завантажити",
"upload_button.label": "Додати зображення, відео або аудіо", "upload_button.label": "Додати зображення, відео або аудіо",
"upload_error.limit": "Ліміт завантаження файлів перевищено.", "upload_error.limit": "Ви перевищили ліміт завантаження файлів.",
"upload_error.poll": "Не можна завантажувати файли до опитувань.", "upload_error.poll": "Не можна завантажувати файли до опитувань.",
"upload_form.audio_description": "Опишіть для людей із вадами слуху", "upload_form.audio_description": "Опишіть для людей із вадами слуху",
"upload_form.description": "Опишіть для людей з вадами зору", "upload_form.description": "Опишіть для людей з вадами зору",
@ -713,7 +713,7 @@
"upload_modal.hint": "Клацніть або перетягніть коло на превʼю, щоб обрати точку, яку буде завжди видно на мініатюрах.", "upload_modal.hint": "Клацніть або перетягніть коло на превʼю, щоб обрати точку, яку буде завжди видно на мініатюрах.",
"upload_modal.preparing_ocr": "Підготовка OCR…", "upload_modal.preparing_ocr": "Підготовка OCR…",
"upload_modal.preview_label": "Переглянути ({ratio})", "upload_modal.preview_label": "Переглянути ({ratio})",
"upload_progress.label": "Завантаження...", "upload_progress.label": "Вивантаження...",
"upload_progress.processing": "Обробка…", "upload_progress.processing": "Обробка…",
"username.taken": "Це ім'я користувача вже зайнято. Спробуйте інше", "username.taken": "Це ім'я користувача вже зайнято. Спробуйте інше",
"video.close": "Закрити відео", "video.close": "Закрити відео",

View file

@ -11,6 +11,7 @@ const normalizeFilter = (state, filter) => {
filter_action: filter.filter_action, filter_action: filter.filter_action,
keywords: filter.keywords, keywords: filter.keywords,
expires_at: filter.expires_at ? Date.parse(filter.expires_at) : null, expires_at: filter.expires_at ? Date.parse(filter.expires_at) : null,
with_quote: filter.with_quote,
}); });
if (is(state.get(filter.id), normalizedFilter)) { if (is(state.get(filter.id), normalizedFilter)) {

View file

@ -3,7 +3,7 @@ import { createSelector } from 'reselect';
import { toServerSideType } from 'mastodon/utils/filters'; import { toServerSideType } from 'mastodon/utils/filters';
import { me } from '../initial_state'; import { me, hideBlockingQuote } from '../initial_state';
const getAccountBase = (state, id) => state.getIn(['accounts', id], null); const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null); const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
@ -37,38 +37,57 @@ export const makeGetStatus = () => {
[ [
(state, { id }) => state.getIn(['statuses', id]), (state, { id }) => state.getIn(['statuses', id]),
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]), (state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id'])]),
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'quote_id'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
getFilters, getFilters,
], ],
(statusBase, statusReblog, accountBase, accountReblog, filters) => { (statusBase, statusReblog, statusQuote, statusReblogQuote, accountBase, accountReblog, filters) => {
if (!statusBase || statusBase.get('isLoading')) { if (!statusBase || statusBase.get('isLoading')) {
return null; return null;
} }
if (statusReblog) { if (statusReblog) {
statusReblog = statusReblog.set('account', accountReblog); statusReblog = statusReblog.set('account', accountReblog);
statusQuote = statusReblogQuote;
} else { } else {
statusReblog = null; statusReblog = null;
} }
if (hideBlockingQuote && (statusReblog || statusBase).getIn(['quote', 'quote_muted'])) {
return null;
}
let filtered = false; let filtered = false;
let filterAction = 'warn';
if ((accountReblog || accountBase).get('id') !== me && filters) { if ((accountReblog || accountBase).get('id') !== me && filters) {
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList(); let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
const quoteFilterResults = statusQuote?.get('filtered');
if (quoteFilterResults) {
const filterWithQuote = quoteFilterResults.some((result) => filters.getIn([result.get('filter'), 'with_quote']));
if (filterWithQuote) {
filterResults = filterResults.concat(quoteFilterResults);
}
}
if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) { if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
return null; return null;
} }
filterResults = filterResults.filter(result => filters.has(result.get('filter'))); filterResults = filterResults.filter(result => filters.has(result.get('filter')));
if (!filterResults.isEmpty()) { if (!filterResults.isEmpty()) {
filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title'])); filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
filterAction = filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'warn') ? 'warn' : 'half_warn';
} }
} }
return statusBase.withMutations(map => { return statusBase.withMutations(map => {
map.set('reblog', statusReblog); map.set('reblog', statusReblog);
map.set('quote', statusQuote);
map.set('account', accountBase); map.set('account', accountBase);
map.set('matched_filters', filtered); map.set('matched_filters', filtered);
map.set('filter_action', filterAction);
}); });
}, },
); );

View file

@ -1733,6 +1733,11 @@ a.account__display-name {
.status__avatar { .status__avatar {
width: 46px; width: 46px;
height: 46px; height: 46px;
&.status__avatar__compact {
width: 24px;
height: 24px;
}
} }
.muted { .muted {
@ -8483,6 +8488,13 @@ noscript {
.status__wrapper { .status__wrapper {
position: relative; position: relative;
&.status__wrapper__compact {
border-radius: 4px;
border: 1px solid $ui-primary-color;
margin-block-start: 16px;
cursor: pointer;
}
&.unread { &.unread {
&::before { &::before {
content: ''; content: '';

View file

@ -26,7 +26,7 @@ class AccountStatusesFilter
scope.merge!(no_reblogs_scope) if exclude_reblogs? scope.merge!(no_reblogs_scope) if exclude_reblogs?
scope.merge!(hashtag_scope) if tagged? scope.merge!(hashtag_scope) if tagged?
available_searchabilities = [:public, :unlisted, :private, :direct, :limited, nil] available_searchabilities = [:public, :public_unlisted, :unlisted, :private, :direct, :limited, nil]
available_visibilities = [:public, :public_unlisted, :login, :unlisted, :private, :direct, :limited] available_visibilities = [:public, :public_unlisted, :login, :unlisted, :private, :direct, :limited]
available_searchabilities = [:public] if domain_block&.reject_send_not_public_searchability available_searchabilities = [:public] if domain_block&.reject_send_not_public_searchability

View file

@ -3,6 +3,7 @@
class ActivityPub::Activity::Accept < ActivityPub::Activity class ActivityPub::Activity::Accept < ActivityPub::Activity
def perform def perform
return accept_follow_for_relay if relay_follow? return accept_follow_for_relay if relay_follow?
return accept_follow_for_friend if friend_follow?
return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil? return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil?
case @object['type'] case @object['type']
@ -43,6 +44,18 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
relay.present? relay.present?
end end
def accept_follow_for_friend
friend.update!(active_state: :accepted, passive_state: :idle)
end
def friend
@friend ||= FriendDomain.find_by(domain: @account.domain, active_follow_activity_id: object_uri, active_state: [:pending, :accepted]) if @account.domain.present?
end
def friend_follow?
friend.present?
end
def target_uri def target_uri
@target_uri ||= value_or_id(@object['actor']) @target_uri ||= value_or_id(@object['actor'])
end end

View file

@ -93,9 +93,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
resolve_thread(@status) resolve_thread(@status)
fetch_replies(@status) fetch_replies(@status)
process_references!
distribute distribute
forward_for_reply forward_for_reply
process_references!
join_group! join_group!
end end
@ -114,7 +114,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end end
def process_status_params def process_status_params
@status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url, object: @object, account: @account) @status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url, object: @object, account: @account, friend_domain: friend_domain?)
@params = { @params = {
uri: @status_parser.uri, uri: @status_parser.uri,
@ -256,17 +256,20 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
emoji = CustomEmoji.find_by(shortcode: custom_emoji_parser.shortcode, domain: @account.domain) emoji = CustomEmoji.find_by(shortcode: custom_emoji_parser.shortcode, domain: @account.domain)
return unless emoji.nil? || custom_emoji_parser.image_remote_url != emoji.image_remote_url || (custom_emoji_parser.updated_at && custom_emoji_parser.updated_at >= emoji.updated_at) return unless emoji.nil? ||
custom_emoji_parser.image_remote_url != emoji.image_remote_url ||
(custom_emoji_parser.updated_at && custom_emoji_parser.updated_at >= emoji.updated_at) ||
custom_emoji_parser.license != emoji.license
begin begin
emoji ||= CustomEmoji.new( emoji ||= CustomEmoji.new(
domain: @account.domain, domain: @account.domain,
shortcode: custom_emoji_parser.shortcode, shortcode: custom_emoji_parser.shortcode,
uri: custom_emoji_parser.uri, uri: custom_emoji_parser.uri
is_sensitive: custom_emoji_parser.is_sensitive,
license: custom_emoji_parser.license
) )
emoji.image_remote_url = custom_emoji_parser.image_remote_url emoji.image_remote_url = custom_emoji_parser.image_remote_url
emoji.license = custom_emoji_parser.license
emoji.is_sensitive = custom_emoji_parser.is_sensitive
emoji.save emoji.save
rescue Seahorse::Client::NetworkingError => e rescue Seahorse::Client::NetworkingError => e
Rails.logger.warn "Error storing emoji: #{e}" Rails.logger.warn "Error storing emoji: #{e}"
@ -444,7 +447,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def related_to_local_activity? def related_to_local_activity?
fetch? || followed_by_local_accounts? || requested_through_relay? || fetch? || followed_by_local_accounts? || requested_through_relay? ||
responds_to_followed_account? || addresses_local_accounts? responds_to_followed_account? || addresses_local_accounts? || quote_local? || free_friend_domain?
end end
def responds_to_followed_account? def responds_to_followed_account?
@ -485,10 +488,30 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def process_references! def process_references!
references = @object['references'].nil? ? [] : ActivityPub::FetchReferencesService.new.call(@status, @object['references']) references = @object['references'].nil? ? [] : ActivityPub::FetchReferencesService.new.call(@status, @object['references'])
quote = @object['quote'] || @object['quoteUrl'] || @object['quoteURL'] || @object['_misskey_quote']
references << quote if quote
ProcessReferencesService.perform_worker_async(@status, [], references) ProcessReferencesService.call_service_without_error(@status, [], references, [quote].compact)
end
def quote_local?
url = quote
if url.present?
ResolveURLService.new.call(url, on_behalf_of: @account, local_only: true).present?
else
false
end
end
def free_friend_domain?
FriendDomain.free_receivings.exists?(domain: @account.domain)
end
def friend_domain?
FriendDomain.enabled.find_by(domain: @account.domain)&.accepted?
end
def quote
@quote ||= @object['quote'] || @object['quoteUrl'] || @object['quoteURL'] || @object['_misskey_quote']
end end
def join_group! def join_group!

View file

@ -4,6 +4,8 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
def perform def perform
if @account.uri == object_uri if @account.uri == object_uri
delete_person delete_person
elsif object_uri == ActivityPub::TagManager::COLLECTIONS[:public]
delete_friend
else else
delete_note delete_note
end end
@ -42,6 +44,11 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
end end
end end
def delete_friend
friend = FriendDomain.find_by(domain: @account.domain)
friend&.destroy
end
def forwarder def forwarder
@forwarder ||= ActivityPub::Forwarder.new(@account, @json, @status) @forwarder ||= ActivityPub::Forwarder.new(@account, @json, @status)
end end

View file

@ -4,6 +4,8 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
include Payloadable include Payloadable
def perform def perform
return request_follow_for_friend if friend_follow?
target_account = account_from_uri(object_uri) target_account = account_from_uri(object_uri)
return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id'])
@ -30,7 +32,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id']) follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id'])
if target_account.locked? || @account.silenced? || block_straight_follow? || (@account.bot? && target_account.user&.setting_lock_follow_from_bot) if target_account.locked? || @account.silenced? || block_straight_follow? || ((@account.bot? || proxy_account?) && target_account.user&.setting_lock_follow_from_bot)
LocalNotificationWorker.perform_async(target_account.id, follow_request.id, 'FollowRequest', 'follow_request') LocalNotificationWorker.perform_async(target_account.id, follow_request.id, 'FollowRequest', 'follow_request')
else else
AuthorizeFollowService.new.call(@account, target_account) AuthorizeFollowService.new.call(@account, target_account)
@ -43,6 +45,40 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
ActivityPub::DeliveryWorker.perform_async(json, target_account.id, @account.inbox_url) ActivityPub::DeliveryWorker.perform_async(json, target_account.id, @account.inbox_url)
end end
def request_follow_for_friend
already_accepted = false
if friend.present?
already_accepted = friend.accepted?
friend.update!(passive_state: :pending, active_state: :idle, passive_follow_activity_id: @json['id'])
else
@friend = FriendDomain.new(domain: @account.domain, passive_state: :pending, passive_follow_activity_id: @json['id'])
@friend.initialize_inbox_url!
@friend.save!
end
if already_accepted || Setting.unlocked_friend
friend.accept!
# Notify for admin even if unlocked
notify_staff_about_pending_friend_server! unless already_accepted
else
notify_staff_about_pending_friend_server!
end
end
def friend
@friend ||= FriendDomain.find_by(domain: @account.domain) if @account.domain.present?
end
def friend_follow?
@json['object'] == ActivityPub::TagManager::COLLECTIONS[:public] && !block_friend?
end
def block_friend?
@block_friend ||= DomainBlock.reject_friend?(@account.domain) || DomainBlock.blocked?(@account.domain)
end
def block_straight_follow? def block_straight_follow?
@block_straight_follow ||= DomainBlock.reject_straight_follow?(@account.domain) @block_straight_follow ||= DomainBlock.reject_straight_follow?(@account.domain)
end end
@ -50,4 +86,35 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
def block_new_follow? def block_new_follow?
@block_new_follow ||= DomainBlock.reject_new_follow?(@account.domain) @block_new_follow ||= DomainBlock.reject_new_follow?(@account.domain)
end end
def proxy_account?
(@account.username.downcase.include?('_proxy') ||
@account.username.downcase.end_with?('proxy') ||
@account.username.downcase.include?('_bot_') ||
@account.username.downcase.end_with?('bot') ||
@account.display_name&.downcase&.include?('proxy') ||
@account.display_name&.include?('プロキシ') ||
@account.note&.include?('プロキシ')) &&
(@account.following_count.zero? || @account.following_count > @account.followers_count) &&
proxyable_software?
end
def proxyable_software?
info = instance_info
return false if info.nil?
%w(misskey calckey firefish meisskey cherrypick).include?(info.software)
end
def instance_info
@instance_info ||= InstanceInfo.find_by(domain: @account.domain)
end
def notify_staff_about_pending_friend_server!
User.those_who_can(:manage_federation).includes(:account).find_each do |u|
next unless u.allows_pending_friend_server_emails?
AdminMailer.with(recipient: u.account).new_pending_friend_server(friend).deliver_later
end
end
end end

View file

@ -7,7 +7,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
def perform def perform
@original_status = status_from_uri(object_uri) @original_status = status_from_uri(object_uri)
return if @original_status.nil? || delete_arrived_first?(@json['id']) || reject_favourite? return if @original_status.nil? || delete_arrived_first?(@json['id']) || block_domain? || reject_favourite?
if shortcode.nil? || !Setting.enable_emoji_reaction if shortcode.nil? || !Setting.enable_emoji_reaction
process_favourite process_favourite
@ -34,19 +34,11 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
def process_emoji_reaction def process_emoji_reaction
return if !@original_status.account.local? && !Setting.receive_other_servers_emoji_reaction return if !@original_status.account.local? && !Setting.receive_other_servers_emoji_reaction
# custom emoji
emoji = nil
if emoji_tag.present? if emoji_tag.present?
return if emoji_tag['id'].blank? || emoji_tag['name'].blank? || emoji_tag['icon'].blank? || emoji_tag['icon']['url'].blank? emoji = process_emoji(emoji_tag)
return if emoji.nil?
image_url = emoji_tag['icon']['url']
uri = emoji_tag['id']
domain = URI.split(uri)[2]
emoji = CustomEmoji.find_or_create_by!(shortcode: shortcode, domain: domain) do |emoji_data|
emoji_data.uri = uri
emoji_data.image_remote_url = image_url
end
Trends.statuses.register(@original_status)
end end
reaction = nil reaction = nil
@ -58,12 +50,14 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
reaction = @original_status.emoji_reactions.create!(account: @account, name: shortcode, custom_emoji: emoji, uri: @json['id']) reaction = @original_status.emoji_reactions.create!(account: @account, name: shortcode, custom_emoji: emoji, uri: @json['id'])
end end
Trends.statuses.register(@original_status)
write_stream(reaction) write_stream(reaction)
if @original_status.account.local? if @original_status.account.local?
NotifyService.new.call(@original_status.account, :emoji_reaction, reaction) NotifyService.new.call(@original_status.account, :emoji_reaction, reaction)
forward_for_emoji_reaction forward_for_emoji_reaction
relay_for_emoji_reaction relay_for_emoji_reaction
relay_friend_for_emoji_reaction
end end
rescue Seahorse::Client::NetworkingError rescue Seahorse::Client::NetworkingError
nil nil
@ -83,6 +77,14 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
end end
end end
def relay_friend_for_emoji_reaction
return unless @json['signature'].present? && @original_status.distributable_friend?
ActivityPub::DeliveryWorker.push_bulk(FriendDomain.distributables.pluck(:inbox_url)) do |inbox_url|
[Oj.dump(@json), @original_status.account.id, inbox_url]
end
end
def shortcode def shortcode
return @shortcode if defined?(@shortcode) return @shortcode if defined?(@shortcode)
@ -95,6 +97,47 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
end end
end end
def process_emoji(tag)
custom_emoji_parser = ActivityPub::Parser::CustomEmojiParser.new(tag)
return if custom_emoji_parser.shortcode.blank? || custom_emoji_parser.image_remote_url.blank?
domain = tag['domain'] || URI.split(custom_emoji_parser.uri)[2] || @account.domain
domain = nil if domain == Rails.configuration.x.local_domain || domain == Rails.configuration.x.web_domain
return if domain.present? && skip_download?(domain)
emoji = CustomEmoji.find_by(shortcode: custom_emoji_parser.shortcode, domain: domain)
return emoji unless emoji.nil? ||
custom_emoji_parser.image_remote_url != emoji.image_remote_url ||
(custom_emoji_parser.updated_at && custom_emoji_parser.updated_at >= emoji.updated_at) ||
custom_emoji_parser.license != emoji.license
begin
emoji ||= CustomEmoji.new(
domain: domain,
shortcode: custom_emoji_parser.shortcode,
uri: custom_emoji_parser.uri
)
emoji.image_remote_url = custom_emoji_parser.image_remote_url
emoji.license = custom_emoji_parser.license
emoji.is_sensitive = custom_emoji_parser.is_sensitive
emoji.save
rescue Seahorse::Client::NetworkingError => e
Rails.logger.warn "Error storing emoji: #{e}"
end
emoji
end
def skip_download?(domain)
DomainBlock.reject_media?(domain)
end
def block_domain?
DomainBlock.blocked?(@account.domain)
end
def misskey_favourite? def misskey_favourite?
misskey_shortcode = @json['_misskey_reaction']&.delete(':') misskey_shortcode = @json['_misskey_reaction']&.delete(':')

View file

@ -3,6 +3,7 @@
class ActivityPub::Activity::Reject < ActivityPub::Activity class ActivityPub::Activity::Reject < ActivityPub::Activity
def perform def perform
return reject_follow_for_relay if relay_follow? return reject_follow_for_relay if relay_follow?
return reject_follow_for_friend if friend_follow?
return follow_request_from_object.reject! unless follow_request_from_object.nil? return follow_request_from_object.reject! unless follow_request_from_object.nil?
return UnfollowService.new.call(follow_from_object.account, @account) unless follow_from_object.nil? return UnfollowService.new.call(follow_from_object.account, @account) unless follow_from_object.nil?
@ -37,6 +38,18 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity
relay.present? relay.present?
end end
def reject_follow_for_friend
friend.update!(active_state: :rejected, passive_state: :idle)
end
def friend
@friend ||= FriendDomain.find_by(domain: @account.domain, active_follow_activity_id: object_uri, active_state: [:pending, :accepted]) if @account.domain.present?
end
def friend_follow?
friend.present?
end
def target_uri def target_uri
@target_uri ||= value_or_id(@object['actor']) @target_uri ||= value_or_id(@object['actor'])
end end

View file

@ -87,6 +87,8 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
end end
def undo_follow def undo_follow
return remove_follow_from_friend if friend_follow?
target_account = account_from_uri(target_uri) target_account = account_from_uri(target_uri)
return if target_account.nil? || !target_account.local? return if target_account.nil? || !target_account.local?
@ -100,6 +102,18 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
end end
end end
def remove_follow_from_friend
friend.destroy_without_signal!
end
def friend
@friend ||= FriendDomain.find_by(domain: @account.domain) if @account.domain.present? && @object['object'] == ActivityPub::TagManager::COLLECTIONS[:public]
end
def friend_follow?
friend.present?
end
def undo_like_original def undo_like_original
status = status_from_uri(target_uri) status = status_from_uri(target_uri)
@ -132,6 +146,7 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
if @original_status.account.local? if @original_status.account.local?
forward_for_undo_emoji_reaction forward_for_undo_emoji_reaction
relay_for_undo_emoji_reaction relay_for_undo_emoji_reaction
relay_friend_for_undo_emoji_reaction
end end
end end
else else
@ -170,6 +185,14 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
end end
end end
def relay_friend_for_undo_emoji_reaction
return unless @json['signature'].present? && @original_status.distributable_friend?
ActivityPub::DeliveryWorker.push_bulk(FriendDomain.distributables.pluck(:inbox_url)) do |inbox_url|
[Oj.dump(@json), @original_status.account.id, inbox_url]
end
end
def shortcode def shortcode
return @shortcode if defined?(@shortcode) return @shortcode if defined?(@shortcode)

View file

@ -21,6 +21,8 @@ module ActivityPub::CaseTransform
value value
elsif value.start_with?('_:') elsif value.start_with?('_:')
"_:#{value.delete_prefix('_:').underscore.camelize(:lower)}" "_:#{value.delete_prefix('_:').underscore.camelize(:lower)}"
elsif LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym) # rubocop:disable Lint/DuplicateBranch
value
else else
value.underscore.camelize(:lower) value.underscore.camelize(:lower)
end end

View file

@ -30,6 +30,6 @@ class ActivityPub::Parser::CustomEmojiParser
end end
def license def license
@json['license'] @json['license'] || @json['licence']
end end
end end

View file

@ -11,6 +11,7 @@ class ActivityPub::Parser::StatusParser
@object = magic_values[:object] || json['object'] || json @object = magic_values[:object] || json['object'] || json
@magic_values = magic_values @magic_values = magic_values
@account = magic_values[:account] @account = magic_values[:account]
@friend = magic_values[:friend_domain]
end end
def uri def uri
@ -76,9 +77,11 @@ class ActivityPub::Parser::StatusParser
def visibility def visibility
if audience_to.any? { |to| ActivityPub::TagManager.instance.public_collection?(to) } if audience_to.any? { |to| ActivityPub::TagManager.instance.public_collection?(to) }
:public :public
elsif audience_to.include?('kmyblue:LocalPublic') && @friend
:public_unlisted
elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) } elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) }
:unlisted :unlisted
elsif audience_to.include?('as:LoginOnly') || audience_to.include?('LoginUser') elsif audience_to.include?('kmyblue:LoginOnly') || audience_to.include?('as:LoginOnly') || audience_to.include?('LoginUser')
:login :login
elsif audience_to.include?(@magic_values[:followers_collection]) elsif audience_to.include?(@magic_values[:followers_collection])
:private :private
@ -196,8 +199,10 @@ class ActivityPub::Parser::StatusParser
nil nil
elsif audience_searchable_by.any? { |uri| ActivityPub::TagManager.instance.public_collection?(uri) } elsif audience_searchable_by.any? { |uri| ActivityPub::TagManager.instance.public_collection?(uri) }
:public :public
elsif audience_searchable_by.include?('as:Limited') elsif audience_searchable_by.include?('kmyblue:Limited') || audience_searchable_by.include?('as:Limited')
:limited :limited
elsif audience_searchable_by.include?('kmyblue:LocalPublic') && @friend
:public_unlisted
elsif audience_searchable_by.include?(@account.followers_url) elsif audience_searchable_by.include?(@account.followers_url)
:private :private
else else

View file

@ -99,7 +99,7 @@ class ActivityPub::TagManager
when 'unlisted', 'public_unlisted', 'private' when 'unlisted', 'public_unlisted', 'private'
[account_followers_url(status.account)] [account_followers_url(status.account)]
when 'login' when 'login'
[account_followers_url(status.account), 'as:LoginOnly', 'LoginUser'] [account_followers_url(status.account), 'as:LoginOnly', 'kmyblue:LoginOnly', 'LoginUser']
when 'direct' when 'direct'
if status.account.silenced? if status.account.silenced?
# Only notify followers if the account is locally silenced # Only notify followers if the account is locally silenced
@ -126,6 +126,12 @@ class ActivityPub::TagManager
end end
end end
def to_for_friend(status)
to = to(status)
to << 'kmyblue:LocalPublic' if status.public_unlisted_visibility?
to
end
# Secondary audience of a status # Secondary audience of a status
# Public statuses go out to followers as well # Public statuses go out to followers as well
# Unlisted statuses go to the public as well # Unlisted statuses go to the public as well
@ -147,7 +153,7 @@ class ActivityPub::TagManager
end end
def cc_for_misskey(status) def cc_for_misskey(status)
if (status.account.user&.setting_reject_unlisted_subscription && status.visibility == 'unlisted') || (status.account.user&.setting_reject_public_unlisted_subscription && status.visibility == 'public_unlisted') if (status.account.user&.setting_reject_unlisted_subscription && status.unlisted_visibility?) || (status.account.user&.setting_reject_public_unlisted_subscription && status.public_unlisted_visibility?)
cc = cc_private_visibility(status) cc = cc_private_visibility(status)
cc << uri_for(status.reblog.account) if status.reblog? cc << uri_for(status.reblog.account) if status.reblog?
return cc return cc
@ -243,7 +249,7 @@ class ActivityPub::TagManager
when 'direct' when 'direct'
status.conversation_id.present? ? [uri_for(status.conversation)] : [] status.conversation_id.present? ? [uri_for(status.conversation)] : []
when 'limited' when 'limited'
['as:Limited'] ['as:Limited', 'kmyblue:Limited']
else else
[] []
end end
@ -251,6 +257,12 @@ class ActivityPub::TagManager
searchable_by.concat(mentions_uris(status)).compact searchable_by.concat(mentions_uris(status)).compact
end end
def searchable_by_for_friend(status)
searchable = searchable_by(status)
searchable << 'kmyblue:LocalPublic' if status.compute_searchability_local == 'public_unlisted'
searchable
end
def account_searchable_by(account) def account_searchable_by(account)
case account.compute_searchability_activitypub case account.compute_searchability_activitypub
when 'public' when 'public'
@ -258,7 +270,7 @@ class ActivityPub::TagManager
when 'private', 'direct' when 'private', 'direct'
[account_followers_url(account)] [account_followers_url(account)]
when 'limited' when 'limited'
['as:Limited'] ['as:Limited', 'kmyblue:Limited']
else else
[] []
end end

View file

@ -25,7 +25,8 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim
end end
def ruby_version def ruby_version
value = "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}" yjit = defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
value = "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}#{yjit ? ' +YJIT' : ''}"
{ {
key: 'ruby', key: 'ruby',

View file

@ -11,7 +11,7 @@ class Admin::Metrics::Dimension::SpaceUsageDimension < Admin::Metrics::Dimension
protected protected
def perform_query def perform_query
[postgresql_size, redis_size, media_size] [postgresql_size, redis_size, media_size, search_size].compact
end end
def postgresql_size def postgresql_size
@ -65,4 +65,22 @@ class Admin::Metrics::Dimension::SpaceUsageDimension < Admin::Metrics::Dimension
redis.info redis.info
end end
end end
def search_size
return unless Chewy.enabled?
client_info = Chewy.client.info
value = Chewy.client.indices.stats['indices'].values.sum { |index_data| index_data['primaries']['store']['size_in_bytes'] }
{
key: 'search',
human_key: client_info.dig('version', 'distribution') == 'opensearch' ? 'OpenSearch' : 'Elasticsearch',
value: value.to_s,
unit: 'bytes',
human_value: number_to_human_size(value),
}
rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error
nil
end
end end

View file

@ -76,14 +76,35 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
end end
def compatible_version? def compatible_version?
return false if running_version.nil? running_version_ok? || compatible_wire_version_ok?
Gem::Version.new(running_version) >= Gem::Version.new(required_version) ||
Gem::Version.new(compatible_wire_version) >= Gem::Version.new(required_version)
rescue ArgumentError rescue ArgumentError
false false
end end
def running_version_ok?
return false if running_version.blank?
gem_version_running >= gem_version_required
end
def compatible_wire_version_ok?
return false if compatible_wire_version.blank?
gem_version_compatible_wire >= gem_version_required
end
def gem_version_running
Gem::Version.new(running_version)
end
def gem_version_required
Gem::Version.new(required_version)
end
def gem_version_compatible_wire
Gem::Version.new(compatible_wire_version)
end
def mismatched_indexes def mismatched_indexes
@mismatched_indexes ||= INDEXES.filter_map do |klass| @mismatched_indexes ||= INDEXES.filter_map do |klass|
klass.base_name if Chewy.client.indices.get_mapping[klass.index_name]&.deep_symbolize_keys != klass.mappings_hash klass.base_name if Chewy.client.indices.get_mapping[klass.index_name]&.deep_symbolize_keys != klass.mappings_hash

View file

@ -58,7 +58,7 @@ class FeedManager
# @param [Boolean] update # @param [Boolean] update
# @return [Boolean] # @return [Boolean]
def push_to_home(account, status, update: false) def push_to_home(account, status, update: false)
return false unless add_to_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?) return false unless add_to_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?, update: update)
trim(:home, account.id) trim(:home, account.id)
PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}", { 'update' => update }) if push_update_required?("timeline:#{account.id}") PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}", { 'update' => update }) if push_update_required?("timeline:#{account.id}")
@ -83,7 +83,7 @@ class FeedManager
# @param [Boolean] update # @param [Boolean] update
# @return [Boolean] # @return [Boolean]
def push_to_list(list, status, update: false) def push_to_list(list, status, update: false)
return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?) return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?, update: update)
trim(:list, list.id) trim(:list, list.id)
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}", { 'update' => update }) if push_update_required?("timeline:list:#{list.id}") PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}", { 'update' => update }) if push_update_required?("timeline:list:#{list.id}")
@ -91,7 +91,7 @@ class FeedManager
end end
def push_to_antenna(antenna, status, update: false) def push_to_antenna(antenna, status, update: false)
return false unless add_to_feed(:antenna, antenna.id, status, aggregate_reblogs: antenna.account.user&.aggregates_reblogs?) return false unless add_to_feed(:antenna, antenna.id, status, aggregate_reblogs: antenna.account.user&.aggregates_reblogs?, update: update)
trim(:antenna, antenna.id) trim(:antenna, antenna.id)
PushUpdateWorker.perform_async(antenna.account_id, status.id, "timeline:antenna:#{antenna.id}", { 'update' => update }) if push_update_required?("timeline:antenna:#{antenna.id}") PushUpdateWorker.perform_async(antenna.account_id, status.id, "timeline:antenna:#{antenna.id}", { 'update' => update }) if push_update_required?("timeline:antenna:#{antenna.id}")
@ -497,7 +497,9 @@ class FeedManager
# @param [Status] status # @param [Status] status
# @param [Boolean] aggregate_reblogs # @param [Boolean] aggregate_reblogs
# @return [Boolean] # @return [Boolean]
def add_to_feed(timeline_type, account_id, status, aggregate_reblogs: true) def add_to_feed(timeline_type, account_id, status, aggregate_reblogs: true, update: false)
return true if update
timeline_key = key(timeline_type, account_id) timeline_key = key(timeline_type, account_id)
reblog_key = key(timeline_type, account_id, 'reblogs') reblog_key = key(timeline_type, account_id, 'reblogs')

View file

@ -23,7 +23,7 @@ class Importer::StatusesIndexImporter < Importer::BaseImporter
to_index.map do |object| to_index.map do |object|
# This is unlikely to happen, but the post may have been # This is unlikely to happen, but the post may have been
# un-interacted with since it was queued for indexing # un-interacted with since it was queued for indexing
if object.searchable_by.empty? && %w(public private).exclude?(object.searchability) if object.searchable_by.empty? && %w(public public_unlisted private).exclude?(object.searchability)
deleted += 1 deleted += 1
{ delete: { _id: object.id } } { delete: { _id: object.id } }
else else

View file

@ -89,10 +89,10 @@ class SearchQueryTransformer < Parslet::Transform
public_index, public_index,
searchability_limited, searchability_limited,
] ]
definition_should << searchability_public if %i(public).include?(@searchability) definition_should << searchability_public if %i(public public_unlisted).include?(@searchability)
definition_should << searchability_private if %i(public unlisted private).include?(@searchability) definition_should << searchability_private if %i(public public_unlisted unlisted private).include?(@searchability)
definition_should << searchable_by_me if %i(public unlisted private direct).include?(@searchability) definition_should << searchable_by_me if %i(public public_unlisted unlisted private direct).include?(@searchability)
definition_should << self_posts if %i(public unlisted private direct).exclude?(@searchability) definition_should << self_posts if %i(public public_unlisted unlisted private direct).exclude?(@searchability)
{ {
bool: { bool: {
@ -199,8 +199,8 @@ class SearchQueryTransformer < Parslet::Transform
def following_account_ids def following_account_ids
return @following_account_ids if defined?(@following_account_ids) return @following_account_ids if defined?(@following_account_ids)
account_exists_sql = Account.where('accounts.id = follows.target_account_id').where(searchability: %w(public private)).reorder(nil).select(1).to_sql account_exists_sql = Account.where('accounts.id = follows.target_account_id').where(searchability: %w(public public_unlisted private)).reorder(nil).select(1).to_sql
status_exists_sql = Status.where('statuses.account_id = follows.target_account_id').where(reblog_of_id: nil).where(searchability: %w(public private)).reorder(nil).select(1).to_sql status_exists_sql = Status.where('statuses.account_id = follows.target_account_id').where(reblog_of_id: nil).where(searchability: %w(public public_unlisted private)).reorder(nil).select(1).to_sql
following_accounts = Follow.where(account_id: @options[:current_account].id).merge(Account.where("EXISTS (#{account_exists_sql})").or(Account.where("EXISTS (#{status_exists_sql})"))) following_accounts = Follow.where(account_id: @options[:current_account].id).merge(Account.where("EXISTS (#{account_exists_sql})").or(Account.where("EXISTS (#{status_exists_sql})")))
@following_account_ids = following_accounts.pluck(:target_account_id) @following_account_ids = following_accounts.pluck(:target_account_id)
end end

View file

@ -10,7 +10,7 @@ class StatusReachFinder
end end
def inboxes def inboxes
(reached_account_inboxes + followers_inboxes + relay_inboxes).uniq (reached_account_inboxes + followers_inboxes + relay_inboxes + nolocal_friend_inboxes).uniq
end end
def inboxes_for_misskey def inboxes_for_misskey
@ -21,6 +21,10 @@ class StatusReachFinder
end end
end end
def inboxes_for_friend
(reached_account_inboxes_for_friend + followers_inboxes_for_friend + friend_inboxes).uniq
end
private private
def reached_account_inboxes def reached_account_inboxes
@ -32,7 +36,7 @@ class StatusReachFinder
elsif @status.limited_visibility? elsif @status.limited_visibility?
Account.where(id: mentioned_account_ids).where.not(domain: banned_domains).inboxes Account.where(id: mentioned_account_ids).where.not(domain: banned_domains).inboxes
else else
Account.where(id: reached_account_ids).where.not(domain: banned_domains).inboxes Account.where(id: reached_account_ids).where.not(domain: banned_domains + friend_domains).inboxes
end end
end end
@ -42,7 +46,17 @@ class StatusReachFinder
elsif @status.limited_visibility? elsif @status.limited_visibility?
Account.where(id: mentioned_account_ids).where(domain: banned_domains_for_misskey).inboxes Account.where(id: mentioned_account_ids).where(domain: banned_domains_for_misskey).inboxes
else else
Account.where(id: reached_account_ids).where(domain: banned_domains_for_misskey).inboxes Account.where(id: reached_account_ids).where(domain: banned_domains_for_misskey - friend_domains).inboxes
end
end
def reached_account_inboxes_for_friend
if @status.reblog?
[]
elsif @status.limited_visibility?
Account.where(id: mentioned_account_ids).where.not(domain: banned_domains).inboxes
else
Account.where(id: reached_account_ids, domain: friend_domains).where.not(domain: banned_domains - friend_domains).inboxes
end end
end end
@ -54,6 +68,7 @@ class StatusReachFinder
reblogs_account_ids, reblogs_account_ids,
favourites_account_ids, favourites_account_ids,
replies_account_ids, replies_account_ids,
quoted_account_id,
].tap do |arr| ].tap do |arr|
arr.flatten! arr.flatten!
arr.compact! arr.compact!
@ -88,23 +103,37 @@ class StatusReachFinder
@status.replies.pluck(:account_id) if distributable? || unsafe? @status.replies.pluck(:account_id) if distributable? || unsafe?
end end
def quoted_account_id
@status.quote.account_id if @status.quote?
end
def followers_inboxes def followers_inboxes
if @status.in_reply_to_local_account? && distributable? if @status.in_reply_to_local_account? && distributable?
@status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where.not(domain: banned_domains).inboxes @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where.not(domain: banned_domains + friend_domains).inboxes
elsif @status.direct_visibility? || @status.limited_visibility? elsif @status.direct_visibility? || @status.limited_visibility?
[] []
else else
@status.account.followers.where.not(domain: banned_domains).inboxes @status.account.followers.where.not(domain: banned_domains + friend_domains).inboxes
end end
end end
def followers_inboxes_for_misskey def followers_inboxes_for_misskey
if @status.in_reply_to_local_account? && distributable? if @status.in_reply_to_local_account? && distributable?
@status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where(domain: banned_domains_for_misskey).inboxes @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where(domain: banned_domains_for_misskey - friend_domains).inboxes
elsif @status.direct_visibility? || @status.limited_visibility? elsif @status.direct_visibility? || @status.limited_visibility?
[] []
else else
@status.account.followers.where(domain: banned_domains_for_misskey).inboxes @status.account.followers.where(domain: banned_domains_for_misskey - friend_domains).inboxes
end
end
def followers_inboxes_for_friend
if @status.in_reply_to_local_account? && distributable?
@status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where(domain: friend_domains).inboxes
elsif @status.direct_visibility? || @status.limited_visibility?
[]
else
@status.account.followers.where(domain: friend_domains).inboxes
end end
end end
@ -116,6 +145,22 @@ class StatusReachFinder
end end
end end
def friend_inboxes
if @status.public_visibility? || @status.public_unlisted_visibility? || (@status.unlisted_visibility? && (@status.public_searchability? || @status.public_unlisted_searchability?))
DeliveryFailureTracker.without_unavailable(FriendDomain.distributables.where(delivery_local: true).pluck(:inbox_url))
else
[]
end
end
def nolocal_friend_inboxes
if @status.public_visibility?
DeliveryFailureTracker.without_unavailable(FriendDomain.distributables.where(delivery_local: false).pluck(:inbox_url))
else
[]
end
end
def distributable? def distributable?
@status.public_visibility? || @status.unlisted_visibility? || @status.public_unlisted_visibility? @status.public_visibility? || @status.unlisted_visibility? || @status.public_unlisted_visibility?
end end
@ -124,6 +169,13 @@ class StatusReachFinder
@options[:unsafe] @options[:unsafe]
end end
def friend_domains
return @friend_domains if defined?(@friend_domains)
@friend_domains = FriendDomain.deliver_locals.pluck(:domain)
@friend_domains -= UnavailableDomain.where(domain: @friend_domains).pluck(:domain)
end
def banned_domains def banned_domains
return @banned_domains if @banned_domains return @banned_domains if @banned_domains

View file

@ -35,6 +35,14 @@ class AdminMailer < ApplicationMailer
end end
end end
def new_pending_friend_server(friend_server)
@friend = friend_server
locale_for_account(@me) do
mail subject: default_i18n_subject(instance: @instance, domain: @friend.domain)
end
end
def new_trends(links, tags, statuses) def new_trends(links, tags, statuses)
@links = links @links = links
@tags = tags @tags = tags

View file

@ -70,7 +70,7 @@ class Account < ApplicationRecord
BACKGROUND_REFRESH_INTERVAL = 1.week.freeze BACKGROUND_REFRESH_INTERVAL = 1.week.freeze
USERNAME_RE = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i USERNAME_RE = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i
MENTION_RE = %r{(?<=^|[^/[:word:]])@((#{USERNAME_RE})(?:@[[:word:].-]+[[:word:]]+)?)}i MENTION_RE = %r{(?<![=/[:word:]])@((#{USERNAME_RE})(?:@[[:word:].-]+[[:word:]]+)?)}i
URL_PREFIX_RE = %r{\Ahttp(s?)://[^/]+} URL_PREFIX_RE = %r{\Ahttp(s?)://[^/]+}
USERNAME_ONLY_RE = /\A#{USERNAME_RE}\z/i USERNAME_ONLY_RE = /\A#{USERNAME_RE}\z/i
@ -132,7 +132,7 @@ class Account < ApplicationRecord
scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) } scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) }
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat) } scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat) }
scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) } scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
scope :by_recent_status, -> { order(Arel.sql('account_stats.last_status_at DESC NULLS LAST')) } scope :by_recent_status, -> { includes(:account_stat).merge(AccountStat.order('last_status_at DESC NULLS LAST')).references(:account_stat) }
scope :by_recent_sign_in, -> { order(Arel.sql('users.current_sign_in_at DESC NULLS LAST')) } scope :by_recent_sign_in, -> { order(Arel.sql('users.current_sign_in_at DESC NULLS LAST')) }
scope :popular, -> { order('account_stats.followers_count desc') } scope :popular, -> { order('account_stats.followers_count desc') }
scope :by_domain_and_subdomains, ->(domain) { where(domain: Instance.by_domain_and_subdomains(domain).select(:domain)) } scope :by_domain_and_subdomains, ->(domain) { where(domain: Instance.by_domain_and_subdomains(domain).select(:domain)) }
@ -330,6 +330,13 @@ class Account < ApplicationRecord
true true
end end
def allow_quote?
return user.setting_allow_quote if local? && user.present?
return settings['allow_quote'] if settings.present? && settings.key?('allow_quote')
true
end
def public_statuses_count def public_statuses_count
hide_statuses_count? ? 0 : statuses_count hide_statuses_count? ? 0 : statuses_count
end end
@ -407,6 +414,7 @@ class Account < ApplicationRecord
'hide_followers_count' => hide_followers_count?, 'hide_followers_count' => hide_followers_count?,
'translatable_private' => translatable_private?, 'translatable_private' => translatable_private?,
'link_preview' => link_preview?, 'link_preview' => link_preview?,
'allow_quote' => allow_quote?,
} }
if Setting.enable_emoji_reaction if Setting.enable_emoji_reaction
config = config.merge({ config = config.merge({

View file

@ -111,6 +111,22 @@ module HasUserSettings
settings['web.use_system_font'] settings['web.use_system_font']
end end
def setting_show_quote_in_home
settings['web.show_quote_in_home']
end
def setting_show_quote_in_public
settings['web.show_quote_in_public']
end
def setting_hide_blocking_quote
settings['web.hide_blocking_quote']
end
def setting_allow_quote
settings['allow_quote']
end
def setting_noindex def setting_noindex
settings['noindex'] settings['noindex']
end end
@ -127,10 +143,6 @@ module HasUserSettings
settings['link_preview'] settings['link_preview']
end end
def setting_single_ref_to_quote
settings['single_ref_to_quote']
end
def setting_dtl_force_with_tag def setting_dtl_force_with_tag
settings['dtl_force_with_tag']&.to_sym || :none settings['dtl_force_with_tag']&.to_sym || :none
end end
@ -251,6 +263,10 @@ module HasUserSettings
settings['notification_emails.pending_account'] settings['notification_emails.pending_account']
end end
def allows_pending_friend_server_emails?
settings['notification_emails.pending_friend_server']
end
def allows_appeal_emails? def allows_appeal_emails?
settings['notification_emails.appeal'] settings['notification_emails.appeal']
end end

View file

@ -5,7 +5,7 @@ module StatusSearchConcern
included do included do
scope :indexable, -> { without_reblogs.where(visibility: [:public, :login], searchability: nil).joins(:account).where(account: { indexable: true }) } scope :indexable, -> { without_reblogs.where(visibility: [:public, :login], searchability: nil).joins(:account).where(account: { indexable: true }) }
scope :remote_dynamic_searchability, -> { remote.where(searchability: [:public, :private]) } scope :remote_dynamic_searchability, -> { remote.where(searchability: [:public, :public_unlisted, :private]) }
end end
def searchable_by def searchable_by

View file

@ -14,6 +14,7 @@
# action :integer default("warn"), not null # action :integer default("warn"), not null
# exclude_follows :boolean default(FALSE), not null # exclude_follows :boolean default(FALSE), not null
# exclude_localusers :boolean default(FALSE), not null # exclude_localusers :boolean default(FALSE), not null
# with_quote :boolean default(TRUE), not null
# #
class CustomFilter < ApplicationRecord class CustomFilter < ApplicationRecord
@ -33,7 +34,7 @@ class CustomFilter < ApplicationRecord
include Expireable include Expireable
include Redisable include Redisable
enum action: { warn: 0, hide: 1 }, _suffix: :action enum action: { warn: 0, hide: 1, half_warn: 2 }, _suffix: :action
belongs_to :account belongs_to :account
has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy
@ -103,11 +104,15 @@ class CustomFilter < ApplicationRecord
if rules[:keywords].present? if rules[:keywords].present?
match = rules[:keywords].match(status.proper.searchable_text) match = rules[:keywords].match(status.proper.searchable_text)
match = rules[:keywords].match(status.proper.references.pluck(:text).join("\n\n")) if match.nil? && status.proper.references.exists? if match.nil? && filter.with_quote && status.proper.references.exists?
match = rules[:keywords].match(status.proper.references.pluck(:text).join("\n\n"))
match = rules[:keywords].match(status.proper.references.pluck(:spoiler_text).join("\n\n")) if match.nil?
end
end end
keyword_matches = [match.to_s] unless match.nil? keyword_matches = [match.to_s] unless match.nil?
status_matches = [status.id, status.reblog_of_id].compact & rules[:status_ids] if rules[:status_ids].present? reference_ids = filter.with_quote ? status.proper.references.pluck(:id) : []
status_matches = ([status.id, status.reblog_of_id] + reference_ids).compact & rules[:status_ids] if rules[:status_ids].present?
next if keyword_matches.blank? && status_matches.blank? next if keyword_matches.blank? && status_matches.blank?

View file

@ -28,6 +28,7 @@
# hidden_anonymous :boolean default(FALSE), not null # hidden_anonymous :boolean default(FALSE), not null
# detect_invalid_subscription :boolean default(FALSE), not null # detect_invalid_subscription :boolean default(FALSE), not null
# reject_reply_exclude_followers :boolean default(FALSE), not null # reject_reply_exclude_followers :boolean default(FALSE), not null
# reject_friend :boolean default(FALSE), not null
# #
class DomainBlock < ApplicationRecord class DomainBlock < ApplicationRecord
@ -44,7 +45,16 @@ class DomainBlock < ApplicationRecord
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :with_user_facing_limitations, -> { where(hidden: false) } scope :with_user_facing_limitations, -> { where(hidden: false) }
scope :with_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)).or(where(reject_favourite: true)).or(where(reject_reply: true)).or(where(reject_reply_exclude_followers: true)).or(where(reject_new_follow: true)).or(where(reject_straight_follow: true)) } scope :with_limitations, lambda {
where(severity: [:silence, :suspend])
.or(where(reject_media: true))
.or(where(reject_favourite: true))
.or(where(reject_reply: true))
.or(where(reject_reply_exclude_followers: true))
.or(where(reject_new_follow: true))
.or(where(reject_straight_follow: true))
.or(where(reject_friend: true))
}
scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), domain')) } scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), domain')) }
def to_log_human_identifier def to_log_human_identifier
@ -68,6 +78,7 @@ class DomainBlock < ApplicationRecord
reject_hashtag? ? :reject_hashtag : nil, reject_hashtag? ? :reject_hashtag : nil,
reject_straight_follow? ? :reject_straight_follow : nil, reject_straight_follow? ? :reject_straight_follow : nil,
reject_new_follow? ? :reject_new_follow : nil, reject_new_follow? ? :reject_new_follow : nil,
reject_friend? ? :reject_friend : nil,
detect_invalid_subscription? ? :detect_invalid_subscription : nil, detect_invalid_subscription? ? :detect_invalid_subscription : nil,
reject_reports? ? :reject_reports : nil].reject { |policy| policy == :noop || policy.nil? } reject_reports? ? :reject_reports : nil].reject { |policy| policy == :noop || policy.nil? }
end end
@ -110,6 +121,10 @@ class DomainBlock < ApplicationRecord
!!rule_for(domain)&.reject_new_follow? !!rule_for(domain)&.reject_new_follow?
end end
def reject_friend?(domain)
!!rule_for(domain)&.reject_friend?
end
def detect_invalid_subscription?(domain) def detect_invalid_subscription?(domain)
!!rule_for(domain)&.detect_invalid_subscription? !!rule_for(domain)&.detect_invalid_subscription?
end end

View file

@ -43,6 +43,10 @@ class EmojiReaction < ApplicationRecord
custom_emoji? && !custom_emoji.local? custom_emoji? && !custom_emoji.local?
end end
def sign?
status&.distributable_friend?
end
private private
def refresh_cache def refresh_cache

View file

@ -48,6 +48,7 @@ class Form::AdminSettings
enable_emoji_reaction enable_emoji_reaction
check_lts_version_only check_lts_version_only
enable_public_unlisted_visibility enable_public_unlisted_visibility
unlocked_friend
).freeze ).freeze
INTEGER_KEYS = %i( INTEGER_KEYS = %i(
@ -76,6 +77,7 @@ class Form::AdminSettings
enable_emoji_reaction enable_emoji_reaction
check_lts_version_only check_lts_version_only
enable_public_unlisted_visibility enable_public_unlisted_visibility
unlocked_friend
).freeze ).freeze
UPLOAD_KEYS = %i( UPLOAD_KEYS = %i(

View file

@ -43,14 +43,14 @@ class Form::Import
validate :validate_data validate :validate_data
def guessed_type def guessed_type
return :muting if csv_data.headers.include?('Hide notifications') return :muting if csv_headers_match?('Hide notifications')
return :following if csv_data.headers.include?('Show boosts') || csv_data.headers.include?('Notify on new posts') || csv_data.headers.include?('Languages') return :following if csv_headers_match?('Show boosts') || csv_headers_match?('Notify on new posts') || csv_headers_match?('Languages')
return :following if data.original_filename&.start_with?('follows') || data.original_filename&.start_with?('following_accounts') return :following if file_name_matches?('follows') || file_name_matches?('following_accounts')
return :blocking if data.original_filename&.start_with?('blocks') || data.original_filename&.start_with?('blocked_accounts') return :blocking if file_name_matches?('blocks') || file_name_matches?('blocked_accounts')
return :muting if data.original_filename&.start_with?('mutes') || data.original_filename&.start_with?('muted_accounts') return :muting if file_name_matches?('mutes') || file_name_matches?('muted_accounts')
return :domain_blocking if data.original_filename&.start_with?('domain_blocks') || data.original_filename&.start_with?('blocked_domains') return :domain_blocking if file_name_matches?('domain_blocks') || file_name_matches?('blocked_domains')
return :bookmarks if data.original_filename&.start_with?('bookmarks') return :bookmarks if file_name_matches?('bookmarks')
return :lists if data.original_filename&.start_with?('lists') return :lists if file_name_matches?('lists')
end end
# Whether the uploaded CSV file seems to correspond to a different import type than the one selected # Whether the uploaded CSV file seems to correspond to a different import type than the one selected
@ -79,6 +79,14 @@ class Form::Import
private private
def file_name_matches?(string)
data.original_filename&.start_with?(string)
end
def csv_headers_match?(string)
csv_data.headers.include?(string)
end
def default_csv_headers def default_csv_headers
case type.to_sym case type.to_sym
when :following, :blocking, :muting when :following, :blocking, :muting

180
app/models/friend_domain.rb Normal file
View file

@ -0,0 +1,180 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: friend_domains
#
# id :bigint(8) not null, primary key
# domain :string default(""), not null
# inbox_url :string default(""), not null
# active_state :integer default("idle"), not null
# passive_state :integer default("idle"), not null
# active_follow_activity_id :string
# passive_follow_activity_id :string
# available :boolean default(TRUE), not null
# pseudo_relay :boolean default(FALSE), not null
# allow_all_posts :boolean default(TRUE), not null
# created_at :datetime not null
# updated_at :datetime not null
# delivery_local :boolean default(TRUE), not null
#
class FriendDomain < ApplicationRecord
validates :domain, presence: true, uniqueness: true, if: :will_save_change_to_domain?
validates :inbox_url, presence: true, uniqueness: true, if: :will_save_change_to_inbox_url?
enum active_state: { idle: 0, pending: 1, accepted: 2, rejected: 3 }, _prefix: :i_am
enum passive_state: { idle: 0, pending: 1, accepted: 2, rejected: 3 }, _prefix: :they_are
scope :by_domain_and_subdomains, ->(domain) { where(domain: Instance.by_domain_and_subdomains(domain).select(:domain)) }
scope :enabled, -> { where(active_state: :accepted).or(FriendDomain.where(passive_state: :accepted)).where(available: true) }
scope :distributables, -> { enabled.where(pseudo_relay: true) }
scope :deliver_locals, -> { enabled.where(delivery_local: true) }
scope :free_receivings, -> { enabled.where(allow_all_posts: true) }
before_destroy :ensure_disabled
after_commit :set_default_inbox_url
def accepted?
i_am_accepted? || they_are_accepted?
end
def pending?
!accepted? && (i_am_pending? || they_are_pending?)
end
def idle?
(i_am_idle? || i_am_rejected?) && (they_are_idle? || they_are_rejected?)
end
def follow!
activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil)
payload = Oj.dump(follow_activity(activity_id))
update!(active_state: :pending, passive_state: :idle, active_follow_activity_id: activity_id)
DeliveryFailureTracker.reset!(inbox_url)
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
end
def unfollow!
activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil)
payload = Oj.dump(unfollow_activity(activity_id))
update!(active_state: :idle, passive_state: :idle, active_follow_activity_id: nil)
DeliveryFailureTracker.reset!(inbox_url)
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
end
def accept!
return if they_are_idle?
activity_id = passive_follow_activity_id
payload = Oj.dump(accept_follow_activity(activity_id))
update!(passive_state: :accepted, active_state: :idle)
DeliveryFailureTracker.reset!(inbox_url)
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
end
def reject!
return if they_are_idle?
activity_id = passive_follow_activity_id
payload = Oj.dump(reject_follow_activity(activity_id))
update!(passive_state: :rejected, active_state: :idle, passive_follow_activity_id: nil)
DeliveryFailureTracker.reset!(inbox_url)
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
end
def destroy_without_signal!
self.active_state = :idle
self.passive_state = :idle
destroy!
end
def initialize_inbox_url!
self.inbox_url = default_inbox_url
end
private
def default_inbox_url
"https://#{domain}/inbox"
end
def delete_for_friend!
activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil)
payload = Oj.dump(delete_follow_activity(activity_id))
DeliveryFailureTracker.reset!(inbox_url)
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
end
def follow_activity(activity_id)
{
'@context': ActivityPub::TagManager::CONTEXT,
id: activity_id,
type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(some_local_account),
object: ActivityPub::TagManager::COLLECTIONS[:public],
}
end
def unfollow_activity(activity_id)
{
'@context': ActivityPub::TagManager::CONTEXT,
id: activity_id,
type: 'Undo',
actor: ActivityPub::TagManager.instance.uri_for(some_local_account),
object: {
id: active_follow_activity_id,
type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(some_local_account),
object: ActivityPub::TagManager::COLLECTIONS[:public],
},
}
end
def accept_follow_activity(activity_id)
{
'@context': ActivityPub::TagManager::CONTEXT,
id: "#{activity_id}#accepts/friends",
type: 'Accept',
actor: ActivityPub::TagManager.instance.uri_for(some_local_account),
object: activity_id,
}
end
def reject_follow_activity(activity_id)
{
'@context': ActivityPub::TagManager::CONTEXT,
id: "#{activity_id}#rejects/friends",
type: 'Reject',
actor: ActivityPub::TagManager.instance.uri_for(some_local_account),
object: activity_id,
}
end
def delete_follow_activity(activity_id)
{
'@context': ActivityPub::TagManager::CONTEXT,
id: "#{activity_id}#delete/friends",
type: 'Delete',
actor: ActivityPub::TagManager.instance.uri_for(some_local_account),
object: ActivityPub::TagManager::COLLECTIONS[:public],
}
end
def some_local_account
@some_local_account ||= Account.representative
end
def ensure_disabled
delete_for_friend! unless id.nil? || (i_am_idle? && they_are_idle?)
end
def set_default_inbox_url
self.inbox_url = default_inbox_url if inbox_url.blank?
end
end

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