diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000000..70d03f6b94
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,191 @@
+version: 2
+
+aliases:
+ - &defaults
+ docker:
+ - image: circleci/ruby:2.5.1-stretch-node
+ environment: &ruby_environment
+ BUNDLE_APP_CONFIG: ./.bundle/
+ DB_HOST: localhost
+ DB_USER: root
+ RAILS_ENV: test
+ PARALLEL_TEST_PROCESSORS: 4
+ ALLOW_NOPAM: true
+ working_directory: ~/projects/mastodon/
+
+ - &attach_workspace
+ attach_workspace:
+ at: ~/projects/
+
+ - &persist_to_workspace
+ persist_to_workspace:
+ root: ~/projects/
+ paths:
+ - ./mastodon/
+
+ - &restore_ruby_dependencies
+ restore_cache:
+ keys:
+ - v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
+ - v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
+ - v2-ruby-dependencies-
+
+ - &install_steps
+ steps:
+ - checkout
+ - *attach_workspace
+
+ - restore_cache:
+ keys:
+ - v1-node-dependencies-{{ checksum "yarn.lock" }}
+ - v1-node-dependencies-
+ - run: yarn install --frozen-lockfile
+ - save_cache:
+ key: v1-node-dependencies-{{ checksum "yarn.lock" }}
+ paths:
+ - ./node_modules/
+
+ - *persist_to_workspace
+
+ - &install_system_dependencies
+ run:
+ name: Install system dependencies
+ command: |
+ sudo apt-get update
+ sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
+
+ - &install_ruby_dependencies
+ steps:
+ - *attach_workspace
+
+ - *install_system_dependencies
+
+ - run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
+ - *restore_ruby_dependencies
+ - run: bundle install --clean --jobs 16 --path ./vendor/bundle/ --retry 3 --with pam_authentication --without development production
+ - save_cache:
+ key: v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
+ paths:
+ - ./.bundle/
+ - ./vendor/bundle/
+
+ - &test_steps
+ steps:
+ - *attach_workspace
+
+ - *install_system_dependencies
+ - run: sudo apt-get install -y ffmpeg
+
+ - run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
+ - *restore_ruby_dependencies
+
+ - restore_cache:
+ keys:
+ - precompiled-assets-{{ .Branch }}-{{ .Revision }}
+ - precompiled-assets-{{ .Branch }}-
+ - precompiled-assets-
+
+ - run:
+ name: Prepare Tests
+ command: ./bin/rails parallel:create parallel:load_schema parallel:prepare
+ - run:
+ name: Run Tests
+ command: bundle exec parallel_test ./spec/ --group-by filesize --type rspec
+
+jobs:
+ install:
+ <<: *defaults
+ <<: *install_steps
+
+ install-ruby2.5:
+ <<: *defaults
+ <<: *install_ruby_dependencies
+
+ install-ruby2.4:
+ <<: *defaults
+ docker:
+ - image: circleci/ruby:2.4.4-stretch-node
+ environment: *ruby_environment
+ <<: *install_ruby_dependencies
+
+ build:
+ <<: *defaults
+ steps:
+ - *attach_workspace
+ - *install_system_dependencies
+ - run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
+ - *restore_ruby_dependencies
+ - run: ./bin/rails assets:precompile
+ - save_cache:
+ key: precompiled-assets-{{ .Branch }}-{{ .Revision }}
+ paths:
+ - ./public/assets
+ - ./public/packs-test/
+
+ test-ruby2.5:
+ <<: *defaults
+ docker:
+ - image: circleci/ruby:2.5.1-stretch-node
+ environment: *ruby_environment
+ - image: circleci/postgres:10.3-alpine
+ environment:
+ POSTGRES_USER: root
+ - image: circleci/redis:4.0.9-alpine
+ <<: *test_steps
+
+ test-ruby2.4:
+ <<: *defaults
+ docker:
+ - image: circleci/ruby:2.4.4-stretch-node
+ environment: *ruby_environment
+ - image: circleci/postgres:10.3-alpine
+ environment:
+ POSTGRES_USER: root
+ - image: circleci/redis:4.0.9-alpine
+ <<: *test_steps
+
+ test-webui:
+ <<: *defaults
+ docker:
+ - image: circleci/node:8.11.1-stretch
+ steps:
+ - *attach_workspace
+ - run: yarn test:jest
+
+ check-i18n:
+ <<: *defaults
+ steps:
+ - *attach_workspace
+ - run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
+ - *restore_ruby_dependencies
+ - run: bundle exec i18n-tasks check-normalized
+ - run: bundle exec i18n-tasks unused
+
+workflows:
+ version: 2
+ build-and-test:
+ jobs:
+ - install
+ - install-ruby2.5:
+ requires:
+ - install
+ - install-ruby2.4:
+ requires:
+ - install
+ - build:
+ requires:
+ - install-ruby2.5
+ - test-ruby2.5:
+ requires:
+ - install-ruby2.5
+ - build
+ - test-ruby2.4:
+ requires:
+ - install-ruby2.4
+ - build
+ - test-webui:
+ requires:
+ - install
+ - check-i18n:
+ requires:
+ - install-ruby2.5
diff --git a/.env.production.sample b/.env.production.sample
index 0c158b06e1..eddd38d906 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -217,3 +217,10 @@ STREAMING_CLUSTER_NUM=1
# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=
+
+# Use HTTP proxy for outgoing request (optional)
+# http_proxy=http://gateway.local:8118
+# Access control for hidden service.
+# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
+# If you use transparent proxy to access to hidden service, uncomment following for skipping private address check.
+# HIDDEN_SERVICE_VIA_TRANSPARENT_PROXY=true
diff --git a/.env.test b/.env.test
index 7da76f8ef8..726351c5e3 100644
--- a/.env.test
+++ b/.env.test
@@ -1,3 +1,5 @@
+# Node.js
+NODE_ENV=test
# Federation
LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true
diff --git a/Gemfile b/Gemfile
index 068b4874d3..c3f4a62f26 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,7 +3,7 @@
source 'https://rubygems.org'
ruby '>= 2.3.0', '< 2.6.0'
-gem 'pkg-config', '~> 1.2'
+gem 'pkg-config', '~> 1.3'
gem 'puma', '~> 3.11'
gem 'rails', '~> 5.2.0'
@@ -11,11 +11,11 @@ gem 'rails', '~> 5.2.0'
gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.0'
gem 'pghero', '~> 2.1'
-gem 'dotenv-rails', '~> 2.2'
+gem 'dotenv-rails', '~> 2.2', '< 2.3'
-gem 'aws-sdk-s3', '~> 1.8', require: false
+gem 'aws-sdk-s3', '~> 1.9', require: false
gem 'fog-core', '~> 1.45'
-gem 'fog-local', '~> 0.4', require: false
+gem 'fog-local', '~> 0.5', require: false
gem 'fog-openstack', '~> 0.1', require: false
gem 'paperclip', '~> 6.0'
gem 'paperclip-av-transcoder', '~> 0.6'
@@ -31,7 +31,7 @@ gem 'iso-639'
gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.0'
gem 'devise', '~> 4.4'
-gem 'devise-two-factor', '~> 3.0', git: 'https://github.com/ykzts/devise-two-factor.git', branch: 'rails-5.2'
+gem 'devise-two-factor', '~> 3.0'
group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.1'
@@ -50,18 +50,18 @@ gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.5'
gem 'html2text'
gem 'htmlentities', '~> 4.3'
-gem 'http', '~> 3.0'
+gem 'http', '~> 3.2'
gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 1.0'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0'
-gem 'mime-types', '~> 3.1'
+gem 'mime-types', '~> 3.1', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.8'
gem 'nsa', '~> 0.2'
-gem 'oj', '~> 3.4'
+gem 'oj', '~> 3.5'
gem 'ostatus2', '~> 2.0'
-gem 'ox', '~> 2.8'
+gem 'ox', '~> 2.9'
gem 'pundit', '~> 1.1'
gem 'premailer-rails'
gem 'rack-attack', '~> 5.2'
@@ -72,7 +72,6 @@ gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 4.0', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 0.10'
-gem 'ruby-oembed', '~> 0.12', require: 'oembed'
gem 'ruby-progressbar', '~> 1.4'
gem 'sanitize', '~> 4.6'
gem 'sidekiq', '~> 5.1'
@@ -84,20 +83,21 @@ gem 'simple_form', '~> 4.0'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.1.3'
gem 'strong_migrations', '~> 0.2'
-gem 'tty-command'
-gem 'tty-prompt'
+gem 'tty-command', '~> 0.8', require: false
+gem 'tty-prompt', '~> 0.16', require: false
gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2018'
gem 'webpacker', '~> 3.4'
gem 'webpush'
-gem 'json-ld-preloaded', '~> 2.2'
+gem 'json-ld', '~> 2.2'
gem 'rdf-normalize', '~> 0.3'
group :development, :test do
gem 'fabrication', '~> 2.20'
gem 'fuubar', '~> 2.2'
gem 'i18n-tasks', '~> 0.9', require: false
+ gem 'pry-byebug', '~> 3.6'
gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.7'
end
@@ -113,7 +113,8 @@ group :test do
gem 'microformats', '~> 4.0'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
- gem 'simplecov', '~> 0.14', require: false
+ gem 'rspec-retry', '~> 0.5', require: false
+ gem 'simplecov', '~> 0.16', require: false
gem 'webmock', '~> 3.3'
gem 'parallel_tests', '~> 2.21'
end
@@ -127,18 +128,21 @@ group :development do
gem 'letter_opener', '~> 1.4'
gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler'
- gem 'rubocop', require: false
+ gem 'rubocop', '~> 0.55', require: false
gem 'brakeman', '~> 4.2', require: false
gem 'bundler-audit', '~> 0.6', require: false
- gem 'scss_lint', '~> 0.55', require: false
+ gem 'scss_lint', '~> 0.57', require: false
gem 'capistrano', '~> 3.10'
gem 'capistrano-rails', '~> 1.3'
gem 'capistrano-rbenv', '~> 2.1'
gem 'capistrano-yarn', '~> 2.0'
+
+ gem 'derailed_benchmarks'
+ gem 'stackprof'
end
group :production do
- gem 'lograge', '~> 0.9'
+ gem 'lograge', '~> 0.10'
gem 'redis-rails', '~> 5.0'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 09ee34f89a..2e2cf1f3d0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,15 +1,3 @@
-GIT
- remote: https://github.com/ykzts/devise-two-factor.git
- revision: f60492b29c174d4c959ac02406392f8eb9c4d374
- branch: rails-5.2
- specs:
- devise-two-factor (3.0.2)
- activesupport (< 5.3)
- attr_encrypted (>= 1.3, < 4, != 2)
- devise (~> 4.0)
- railties (< 5.3)
- rotp (~> 2.0)
-
GEM
remote: https://rubygems.org/
specs:
@@ -64,7 +52,7 @@ GEM
public_suffix (>= 2.0.2, < 4.0)
airbrussh (1.3.0)
sshkit (>= 1.6.1, != 1.7.0)
- annotate (2.7.2)
+ annotate (2.7.3)
activerecord (>= 3.2, < 6.0)
rake (>= 10.4, < 13.0)
arel (9.0.0)
@@ -73,20 +61,21 @@ GEM
encryptor (~> 3.0.0)
av (0.9.0)
cocaine (~> 0.5.3)
- aws-partitions (1.70.0)
- aws-sdk-core (3.17.0)
+ aws-partitions (1.80.0)
+ aws-sdk-core (3.19.0)
aws-partitions (~> 1.0)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-kms (1.5.0)
aws-sdk-core (~> 3)
aws-sigv4 (~> 1.0)
- aws-sdk-s3 (1.8.2)
+ aws-sdk-s3 (1.9.1)
aws-sdk-core (~> 3)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0)
aws-sigv4 (1.0.2)
bcrypt (3.1.11)
+ benchmark-ips (2.7.2)
better_errors (2.4.0)
coderay (>= 1.0.0)
erubi (>= 1.0.0)
@@ -96,7 +85,7 @@ GEM
bootsnap (1.3.0)
msgpack (~> 1.0)
brakeman (4.2.1)
- browser (2.5.2)
+ browser (2.5.3)
builder (3.2.3)
bullet (5.7.5)
activesupport (>= 3.0.0)
@@ -104,7 +93,8 @@ GEM
bundler-audit (0.6.0)
bundler (~> 1.2)
thor (~> 0.18)
- capistrano (3.10.1)
+ byebug (10.0.2)
+ capistrano (3.10.2)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
@@ -150,18 +140,32 @@ GEM
css_parser (1.6.0)
addressable
debug_inspector (0.0.3)
+ derailed_benchmarks (1.3.4)
+ benchmark-ips (~> 2)
+ get_process_mem (~> 0)
+ heapy (~> 0)
+ memory_profiler (~> 0)
+ rack (>= 1)
+ rake (> 10, < 13)
+ thor (~> 0.19)
devise (4.4.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0, < 6.0)
responders
warden (~> 1.2.3)
+ devise-two-factor (3.0.3)
+ activesupport (< 5.3)
+ attr_encrypted (>= 1.3, < 4, != 2)
+ devise (~> 4.0)
+ railties (< 5.3)
+ rotp (~> 2.0)
devise_pam_authenticatable2 (9.1.0)
devise (>= 4.0.0)
rpam2 (~> 4.0)
diff-lcs (1.3)
- docile (1.1.5)
- domain_name (0.5.20170404)
+ docile (1.3.0)
+ domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.3.2)
railties (>= 4.2)
@@ -172,29 +176,29 @@ GEM
easy_translate (0.5.1)
thread
thread_safe
- elasticsearch (6.0.1)
- elasticsearch-api (= 6.0.1)
- elasticsearch-transport (= 6.0.1)
- elasticsearch-api (6.0.1)
+ elasticsearch (6.0.2)
+ elasticsearch-api (= 6.0.2)
+ elasticsearch-transport (= 6.0.2)
+ elasticsearch-api (6.0.2)
multi_json
elasticsearch-dsl (0.1.5)
- elasticsearch-transport (6.0.1)
+ elasticsearch-transport (6.0.2)
faraday
multi_json
encryptor (3.0.0)
equatable (0.5.0)
erubi (1.7.1)
- et-orbi (1.0.9)
+ et-orbi (1.1.0)
tzinfo
- excon (0.60.0)
+ excon (0.62.0)
fabrication (2.20.1)
faker (1.8.7)
i18n (>= 0.7)
- faraday (0.14.0)
+ faraday (0.15.0)
multipart-post (>= 1.2, < 3)
fast_blank (1.0.0)
fastimage (2.1.1)
- ffi (1.9.21)
+ ffi (1.9.23)
fog-core (1.45.0)
builder
excon (~> 0.58)
@@ -202,9 +206,9 @@ GEM
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
- fog-local (0.4.0)
- fog-core (~> 1.27)
- fog-openstack (0.1.23)
+ fog-local (0.5.0)
+ fog-core (>= 1.27, < 3.0)
+ fog-openstack (0.1.25)
fog-core (~> 1.40)
fog-json (>= 1.0)
ipaddress (>= 0.8)
@@ -212,6 +216,7 @@ GEM
fuubar (2.3.1)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
+ get_process_mem (0.2.1)
globalid (0.4.1)
activesupport (>= 4.2.0)
goldfinger (2.1.0)
@@ -232,6 +237,7 @@ GEM
concurrent-ruby (~> 1.0)
hashdiff (0.3.7)
hashie (3.5.7)
+ heapy (0.1.3)
highline (1.7.10)
hiredis (0.6.1)
hitimes (1.2.6)
@@ -239,20 +245,20 @@ GEM
html2text (0.2.1)
nokogiri (~> 1.6)
htmlentities (4.3.4)
- http (3.0.0)
+ http (3.2.0)
addressable (~> 2.3)
http-cookie (~> 1.0)
- http-form_data (>= 2.0.0.pre.pre2, < 3)
+ http-form_data (~> 2.0)
http_parser.rb (~> 0.6.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
- http-form_data (2.0.0)
+ http-form_data (2.1.0)
http_accept_language (2.1.1)
http_parser.rb (0.6.0)
httplog (1.0.2)
colorize (~> 0.8)
rack (>= 1.0)
- i18n (1.0.0)
+ i18n (1.0.1)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.21)
activesupport (>= 4.0.2)
@@ -267,15 +273,11 @@ GEM
idn-ruby (0.1.0)
ipaddress (0.8.3)
iso-639 (0.2.8)
- jmespath (1.3.1)
+ jmespath (1.4.0)
json (2.1.0)
json-ld (2.2.1)
multi_json (~> 1.12)
rdf (>= 2.2.8, < 4.0)
- json-ld-preloaded (2.2.3)
- json-ld (>= 2.2, < 4.0)
- multi_json (~> 1.12)
- rdf (>= 2.2, < 4.0)
jsonapi-renderer (0.2.0)
jwt (2.1.0)
kaminari (1.1.1)
@@ -299,7 +301,7 @@ GEM
letter_opener (~> 1.0)
railties (>= 3.2)
link_header (0.0.8)
- lograge (0.9.0)
+ lograge (0.10.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
@@ -343,7 +345,7 @@ GEM
concurrent-ruby (~> 1.0.0)
sidekiq (>= 3.5.0)
statsd-ruby (~> 1.2.0)
- oj (3.4.0)
+ oj (3.5.1)
omniauth (1.8.1)
hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3)
@@ -359,7 +361,7 @@ GEM
addressable (~> 2.5)
http (~> 3.0)
nokogiri (~> 1.8)
- ox (2.8.2)
+ ox (2.9.2)
paperclip (6.0.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
@@ -370,7 +372,7 @@ GEM
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.12.1)
- parallel_tests (2.21.1)
+ parallel_tests (2.21.3)
parallel
parser (2.5.1.0)
ast (~> 2.4.0)
@@ -380,7 +382,7 @@ GEM
pg (1.0.0)
pghero (2.1.0)
activerecord
- pkg-config (1.2.9)
+ pkg-config (1.3.0)
posix-spawn (0.3.13)
powerpack (0.1.1)
premailer (1.11.1)
@@ -394,10 +396,13 @@ GEM
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
+ pry-byebug (3.6.0)
+ byebug (~> 10.0)
+ pry (~> 0.10)
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.0.2)
- puma (3.11.3)
+ puma (3.11.4)
pundit (1.1.0)
activesupport (>= 3.0.0)
rack (2.0.4)
@@ -446,10 +451,10 @@ GEM
thor (>= 0.18.1, < 2.0)
rainbow (3.0.0)
rake (12.3.1)
- rb-fsevent (0.10.2)
+ rb-fsevent (0.10.3)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
- rdf (3.0.1)
+ rdf (3.0.2)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.3)
@@ -471,9 +476,9 @@ GEM
redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2)
- redis-store (1.4.1)
+ redis-store (1.5.0)
redis (>= 2.2, < 5)
- request_store (1.4.0)
+ request_store (1.4.1)
rack (>= 1.4)
responders (2.4.0)
actionpack (>= 4.2.0, < 5.3)
@@ -498,18 +503,19 @@ GEM
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-support (~> 3.7.0)
+ rspec-retry (0.5.7)
+ rspec-core (> 3.3)
rspec-sidekiq (3.0.3)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.7.1)
- rubocop (0.52.1)
+ rubocop (0.55.0)
parallel (~> 1.10)
- parser (>= 2.4.0.2, < 3.0)
+ parser (>= 2.5)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
- ruby-oembed (0.12.0)
ruby-progressbar (1.9.0)
ruby-saml (1.7.2)
nokogiri (>= 1.5.10)
@@ -520,14 +526,14 @@ GEM
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4)
- sass (3.5.5)
+ sass (3.5.6)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
- scss_lint (0.56.0)
+ scss_lint (0.57.0)
rake (>= 0.9, < 13)
- sass (~> 3.5.3)
+ sass (~> 3.5.5)
sidekiq (5.1.3)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
@@ -549,8 +555,8 @@ GEM
simple_form (4.0.0)
actionpack (> 4)
activemodel (> 4)
- simplecov (0.15.1)
- docile (~> 1.1.0)
+ simplecov (0.16.1)
+ docile (~> 1.1)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
@@ -564,6 +570,7 @@ GEM
sshkit (1.16.0)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
+ stackprof (0.2.11)
statsd-ruby (1.2.1)
stoplight (2.1.3)
streamio-ffmpeg (3.0.2)
@@ -582,10 +589,10 @@ GEM
timers (4.1.2)
hitimes
tty-color (0.4.2)
- tty-command (0.7.0)
+ tty-command (0.8.0)
pastel (~> 0.7.0)
tty-cursor (0.5.0)
- tty-prompt (0.15.0)
+ tty-prompt (0.16.0)
necromancer (~> 0.4.0)
pastel (~> 0.7.0)
timers (~> 4.0)
@@ -605,7 +612,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
- unicode-display_width (1.3.0)
+ unicode-display_width (1.3.2)
uniform_notifier (1.11.0)
warden (1.2.7)
rack (>= 1.0)
@@ -635,7 +642,7 @@ DEPENDENCIES
active_record_query_trace (~> 1.5)
addressable (~> 2.5)
annotate (~> 2.7)
- aws-sdk-s3 (~> 1.8)
+ aws-sdk-s3 (~> 1.9)
better_errors (~> 2.4)
binding_of_caller (~> 0.7)
bootsnap (~> 1.3)
@@ -652,17 +659,18 @@ DEPENDENCIES
chewy (~> 5.0)
cld3 (~> 3.2.0)
climate_control (~> 0.2)
+ derailed_benchmarks
devise (~> 4.4)
- devise-two-factor (~> 3.0)!
+ devise-two-factor (~> 3.0)
devise_pam_authenticatable2 (~> 9.1)
doorkeeper (~> 4.3)
- dotenv-rails (~> 2.2)
+ dotenv-rails (~> 2.2, < 2.3)
fabrication (~> 2.20)
faker (~> 1.8)
fast_blank (~> 1.0)
fastimage
fog-core (~> 1.45)
- fog-local (~> 0.4)
+ fog-local (~> 0.5)
fog-openstack (~> 0.1)
fuubar (~> 2.2)
goldfinger (~> 2.1)
@@ -670,18 +678,18 @@ DEPENDENCIES
hiredis (~> 0.6)
html2text
htmlentities (~> 4.3)
- http (~> 3.0)
+ http (~> 3.2)
http_accept_language (~> 2.1)
httplog (~> 1.0)
i18n-tasks (~> 0.9)
idn-ruby
iso-639
- json-ld-preloaded (~> 2.2)
+ json-ld (~> 2.2)
kaminari (~> 1.1)
letter_opener (~> 1.4)
letter_opener_web (~> 1.3)
link_header (~> 0.0)
- lograge (~> 0.9)
+ lograge (~> 0.10)
mario-redis-lock (~> 1.2)
memory_profiler
microformats (~> 4.0)
@@ -689,21 +697,22 @@ DEPENDENCIES
net-ldap (~> 0.10)
nokogiri (~> 1.8)
nsa (~> 0.2)
- oj (~> 3.4)
+ oj (~> 3.5)
omniauth (~> 1.2)
omniauth-cas (~> 1.1)
omniauth-saml (~> 1.10)
ostatus2 (~> 2.0)
- ox (~> 2.8)
+ ox (~> 2.9)
paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6)
parallel_tests (~> 2.21)
pg (~> 1.0)
pghero (~> 2.1)
- pkg-config (~> 1.2)
+ pkg-config (~> 1.3)
posix-spawn
premailer-rails
private_address_check (~> 0.4.1)
+ pry-byebug (~> 3.6)
pry-rails (~> 0.3)
puma (~> 3.11)
pundit (~> 1.1)
@@ -720,25 +729,26 @@ DEPENDENCIES
redis-rails (~> 5.0)
rqrcode (~> 0.10)
rspec-rails (~> 3.7)
+ rspec-retry (~> 0.5)
rspec-sidekiq (~> 3.0)
- rubocop
- ruby-oembed (~> 0.12)
+ rubocop (~> 0.55)
ruby-progressbar (~> 1.4)
sanitize (~> 4.6)
- scss_lint (~> 0.55)
+ scss_lint (~> 0.57)
sidekiq (~> 5.1)
sidekiq-bulk (~> 0.1.1)
sidekiq-scheduler (~> 2.2)
sidekiq-unique-jobs (~> 5.0)
simple-navigation (~> 4.0)
simple_form (~> 4.0)
- simplecov (~> 0.14)
+ simplecov (~> 0.16)
sprockets-rails (~> 3.2)
+ stackprof
stoplight (~> 2.1.3)
streamio-ffmpeg (~> 3.0)
strong_migrations (~> 0.2)
- tty-command
- tty-prompt
+ tty-command (~> 0.8)
+ tty-prompt (~> 0.16)
twitter-text (~> 1.14)
tzinfo-data (~> 1.2018)
webmock (~> 3.3)
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 1efaf619b0..50f5d0b119 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -21,9 +21,10 @@ class AccountsController < ApplicationController
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
@statuses = filtered_status_page(params)
@statuses = cache_collection(@statuses, Status)
+
unless @statuses.empty?
- @older_url = older_url if @statuses.last.id > filtered_statuses.last.id
- @newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id
+ @older_url = older_url if @statuses.last.id > filtered_statuses.last.id
+ @newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id
end
end
@@ -32,6 +33,11 @@ class AccountsController < ApplicationController
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
end
+ format.rss do
+ @statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status)
+ render xml: RSS::AccountSerializer.render(@account, @statuses)
+ end
+
format.json do
skip_session!
diff --git a/app/controllers/admin/reported_statuses_controller.rb b/app/controllers/admin/reported_statuses_controller.rb
index 535bd11d48..522f68c98e 100644
--- a/app/controllers/admin/reported_statuses_controller.rb
+++ b/app/controllers/admin/reported_statuses_controller.rb
@@ -8,7 +8,7 @@ module Admin
def create
authorize :status, :update?
- @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
+ @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report)
@@ -35,7 +35,17 @@ module Admin
end
def form_status_batch_params
- params.require(:form_status_batch).permit(:action, status_ids: [])
+ params.require(:form_status_batch).permit(status_ids: [])
+ end
+
+ def action_from_button
+ if params[:nsfw_on]
+ 'nsfw_on'
+ elsif params[:nsfw_off]
+ 'nsfw_off'
+ elsif params[:delete]
+ 'delete'
+ end
end
def set_report
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
index a4ae9507d4..d00b3d2227 100644
--- a/app/controllers/admin/reports_controller.rb
+++ b/app/controllers/admin/reports_controller.rb
@@ -11,10 +11,10 @@ module Admin
def show
authorize @report, :show?
- @report_note = @report.notes.new
- @report_notes = @report.notes.latest
- @report_history = @report.history
- @form = Form::StatusBatch.new
+
+ @report_note = @report.notes.new
+ @report_notes = (@report.notes.latest + @report.history).sort_by(&:created_at)
+ @form = Form::StatusBatch.new
end
def update
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 7b5168b314..b5c084e145 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -66,8 +66,10 @@ class Api::BaseController < ApplicationController
end
def require_user!
- if current_user
+ if current_user && !current_user.disabled?
set_user_activity
+ elsif current_user
+ render json: { error: 'Your login is currently disabled' }, status: 403
else
render json: { error: 'This method requires an authenticated user' }, status: 422
end
diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb
index 062d490a73..a3c4008e64 100644
--- a/app/controllers/api/v1/accounts/credentials_controller.rb
+++ b/app/controllers/api/v1/accounts/credentials_controller.rb
@@ -21,7 +21,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
private
def account_params
- params.permit(:display_name, :note, :avatar, :header, :locked)
+ params.permit(:display_name, :note, :avatar, :header, :locked, fields_attributes: [:name, :value])
end
def user_settings_params
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index d643259441..b7133ca8e5 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -5,6 +5,7 @@ class Api::V1::AccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
before_action :require_user!, except: [:show]
before_action :set_account
+ before_action :check_account_suspension, only: [:show]
respond_to :json
@@ -54,4 +55,8 @@ class Api::V1::AccountsController < Api::BaseController
def relationships(**options)
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
end
+
+ def check_account_suspension
+ gone if @account.suspended?
+ end
end
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index e982413236..01880565c8 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -18,7 +18,7 @@ class Api::V1::StatusesController < Api::BaseController
def context
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(DEFAULT_STATUSES_LIMIT, current_account)
- descendants_results = @status.descendants(current_account)
+ descendants_results = @status.descendants(DEFAULT_STATUSES_LIMIT, current_account)
loaded_ancestors = cache_collection(ancestors_results, Status)
loaded_descendants = cache_collection(descendants_results, Status)
diff --git a/app/controllers/api/web/embeds_controller.rb b/app/controllers/api/web/embeds_controller.rb
index f2fe74b179..987290a14c 100644
--- a/app/controllers/api/web/embeds_controller.rb
+++ b/app/controllers/api/web/embeds_controller.rb
@@ -9,9 +9,12 @@ class Api::Web::EmbedsController < Api::Web::BaseController
status = StatusFinder.new(params[:url]).status
render json: status, serializer: OEmbedSerializer, width: 400
rescue ActiveRecord::RecordNotFound
- oembed = OEmbed::Providers.get(params[:url])
- render json: Oj.dump(oembed.fields)
- rescue OEmbed::NotFound
- render json: {}, status: :not_found
+ oembed = FetchOEmbedService.new.call(params[:url])
+
+ if oembed
+ render json: oembed
+ else
+ render json: {}, status: :not_found
+ end
end
end
diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb
index abd85ea27a..145549bcd2 100644
--- a/app/controllers/concerns/localized.rb
+++ b/app/controllers/concerns/localized.rb
@@ -29,10 +29,14 @@ module Localized
end
def preferred_locale
- http_accept_language.preferred_language_from(I18n.available_locales)
+ http_accept_language.preferred_language_from(available_locales)
end
def compatible_locale
- http_accept_language.compatible_language_from(I18n.available_locales)
+ http_accept_language.compatible_language_from(available_locales)
+ end
+
+ def available_locales
+ I18n.available_locales.reverse
end
end
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 3237a15b92..2e9cf14e08 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -4,7 +4,9 @@ class StatusesController < ApplicationController
include SignatureAuthentication
include Authorization
- ANCESTORS_LIMIT = 20
+ ANCESTORS_LIMIT = 40
+ DESCENDANTS_LIMIT = 60
+ DESCENDANTS_DEPTH_LIMIT = 20
layout 'public'
@@ -20,9 +22,8 @@ class StatusesController < ApplicationController
respond_to do |format|
format.html do
use_pack 'public'
- @ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
- @next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
- @descendants = cache_collection(@status.descendants(current_account), Status)
+ set_ancestors
+ set_descendants
render 'stream_entries/show'
end
@@ -53,10 +54,77 @@ class StatusesController < ApplicationController
private
+ def create_descendant_thread(depth, statuses)
+ if depth < DESCENDANTS_DEPTH_LIMIT
+ { statuses: statuses }
+ else
+ next_status = statuses.pop
+ { statuses: statuses, next_status: next_status }
+ end
+ end
+
def set_account
@account = Account.find_local!(params[:account_username])
end
+ def set_ancestors
+ @ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
+ @next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
+ end
+
+ def set_descendants
+ @max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i
+ @since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i
+
+ descendants = cache_collection(
+ @status.descendants(
+ DESCENDANTS_LIMIT,
+ current_account,
+ @max_descendant_thread_id,
+ @since_descendant_thread_id,
+ DESCENDANTS_DEPTH_LIMIT
+ ),
+ Status
+ )
+
+ @descendant_threads = []
+
+ if descendants.present?
+ statuses = [descendants.first]
+ depth = 1
+
+ descendants.drop(1).each_with_index do |descendant, index|
+ if descendants[index].id == descendant.in_reply_to_id
+ depth += 1
+ statuses << descendant
+ else
+ @descendant_threads << create_descendant_thread(depth, statuses)
+
+ @descendant_threads.reverse_each do |descendant_thread|
+ statuses = descendant_thread[:statuses]
+
+ index = statuses.find_index do |thread_status|
+ thread_status.id == descendant.in_reply_to_id
+ end
+
+ if index.present?
+ depth += index - statuses.size
+ break
+ end
+
+ depth -= statuses.size
+ end
+
+ statuses = [descendant]
+ end
+ end
+
+ @descendant_threads << create_descendant_thread(depth, statuses)
+ end
+
+ @max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT
+ end
+
def set_link_headers
response.headers['Link'] = LinkHeader.new(
[
diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb
index 44e9c0bb83..8cb54a1487 100644
--- a/app/controllers/stream_entries_controller.rb
+++ b/app/controllers/stream_entries_controller.rb
@@ -24,6 +24,7 @@ class StreamEntriesController < ApplicationController
skip_session!
expires_in 3.minutes, public: true
end
+
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
end
end
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 5d11a81390..a76be26e56 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class TagsController < ApplicationController
+ PAGE_SIZE = 20
+
before_action :set_body_classes
before_action :set_instance_presenter
@@ -14,8 +16,15 @@ class TagsController < ApplicationController
@initial_state_json = serializable_resource.to_json
end
+ format.rss do
+ @statuses = Status.as_tag_timeline(@tag).limit(PAGE_SIZE)
+ @statuses = cache_collection(@statuses, Status)
+
+ render xml: RSS::TagSerializer.render(@tag, @statuses)
+ end
+
format.json do
- @statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
+ @statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
@statuses = cache_collection(@statuses, Status)
render json: collection_presenter,
diff --git a/app/helpers/admin/account_moderation_notes_helper.rb b/app/helpers/admin/account_moderation_notes_helper.rb
index b17c522643..fdfadef080 100644
--- a/app/helpers/admin/account_moderation_notes_helper.rb
+++ b/app/helpers/admin/account_moderation_notes_helper.rb
@@ -1,4 +1,20 @@
# frozen_string_literal: true
module Admin::AccountModerationNotesHelper
+ def admin_account_link_to(account)
+ link_to admin_account_path(account.id), class: name_tag_classes(account) do
+ safe_join([
+ image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'),
+ content_tag(:span, account.acct, class: 'username'),
+ ], ' ')
+ end
+ end
+
+ private
+
+ def name_tag_classes(account)
+ classes = ['name-tag']
+ classes << 'suspended' if account.suspended?
+ classes.join(' ')
+ end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index bab4615a18..95863ab1f0 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -63,4 +63,8 @@ module ApplicationHelper
def opengraph(property, content)
tag(:meta, content: content, property: property)
end
+
+ def react_component(name, props = {})
+ content_tag(:div, nil, data: { component: name.to_s.camelcase, props: Oj.dump(props) })
+ end
end
diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb
index dfb8fcb8b1..e9056166c1 100644
--- a/app/helpers/jsonld_helper.rb
+++ b/app/helpers/jsonld_helper.rb
@@ -5,6 +5,10 @@ module JsonLdHelper
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
end
+ def equals_or_includes_any?(haystack, needles)
+ needles.any? { |needle| equals_or_includes?(haystack, needle) }
+ end
+
def first_of_value(value)
value.is_a?(Array) ? value.first : value
end
@@ -44,7 +48,7 @@ module JsonLdHelper
end
def canonicalize(json)
- graph = RDF::Graph.new << JSON::LD::API.toRdf(json)
+ graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context))
graph.dump(:normalize)
end
@@ -86,4 +90,19 @@ module JsonLdHelper
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
request
end
+
+ def load_jsonld_context(url, _options = {}, &_block)
+ json = Rails.cache.fetch("jsonld:context:#{url}", expires_in: 30.days, raw: true) do
+ request = Request.new(:get, url)
+ request.add_headers('Accept' => 'application/ld+json')
+
+ request.perform do |res|
+ raise JSON::LD::JsonLdError::LoadingDocumentFailed unless res.code == 200 && res.mime_type == 'application/ld+json'
+ res.body_with_limit
+ end
+ end
+
+ doc = JSON::LD::API::RemoteDocument.new(url, json)
+ block_given? ? yield(doc) : doc
+ end
end
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index a2f5917f99..f78e5fbc3f 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -7,12 +7,14 @@ module SettingsHelper
bg: 'Български',
ca: 'Català',
de: 'Deutsch',
+ el: 'Ελληνικά',
eo: 'Esperanto',
es: 'Español',
+ eu: 'Euskara',
fa: 'فارسی',
- gl: 'Galego',
fi: 'Suomi',
fr: 'Français',
+ gl: 'Galego',
he: 'עברית',
hr: 'Hrvatski',
hu: 'Magyar',
@@ -33,6 +35,7 @@ module SettingsHelper
sr: 'Српски',
'sr-Latn': 'Srpski (latinica)',
sv: 'Svenska',
+ te: 'తెలుగు',
th: 'ภาษาไทย',
tr: 'Türkçe',
uk: 'Українська',
diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb
index 3992432dbd..c6f12ecd41 100644
--- a/app/helpers/stream_entries_helper.rb
+++ b/app/helpers/stream_entries_helper.rb
@@ -12,17 +12,17 @@ module StreamEntriesHelper
prepend_str = [
[
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
- t('accounts.posts'),
+ I18n.t('accounts.posts'),
].join(' '),
[
number_to_human(account.following_count, strip_insignificant_zeros: true),
- t('accounts.following'),
+ I18n.t('accounts.following'),
].join(' '),
[
number_to_human(account.followers_count, strip_insignificant_zeros: true),
- t('accounts.followers'),
+ I18n.t('accounts.followers'),
].join(' '),
].join(', ')
@@ -40,16 +40,16 @@ module StreamEntriesHelper
end
end
- text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| t("statuses.attached.#{key}", count: value) }.join(' · ')
+ text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| I18n.t("statuses.attached.#{key}", count: value) }.join(' · ')
return if text.blank?
- t('statuses.attached.description', attached: text)
+ I18n.t('statuses.attached.description', attached: text)
end
def status_text_summary(status)
return if status.spoiler_text.blank?
- t('statuses.content_warning', warning: status.spoiler_text)
+ I18n.t('statuses.content_warning', warning: status.spoiler_text)
end
def status_description(status)
@@ -113,6 +113,19 @@ module StreamEntriesHelper
end
end
+ def fa_visibility_icon(status)
+ case status.visibility
+ when 'public'
+ fa_icon 'globe fw'
+ when 'unlisted'
+ fa_icon 'unlock-alt fw'
+ when 'private'
+ fa_icon 'lock fw'
+ when 'direct'
+ fa_icon 'envelope fw'
+ end
+ end
+
private
def simplified_text(text)
diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js
index b4125e84ea..28f27fbc62 100644
--- a/app/javascript/core/admin.js
+++ b/app/javascript/core/admin.js
@@ -26,6 +26,7 @@ delegate(document, batchCheckboxClassName, 'change', () => {
const checkAllElement = document.querySelector('#batch_checkbox_all');
if (checkAllElement) {
checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
+ checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
}
});
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index eee9c6928c..fe3e831d5f 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -4,6 +4,7 @@ import { throttle } from 'lodash';
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
import { tagHistory } from '../settings';
import { useEmoji } from './emojis';
+import resizeImage from '../utils/resize_image';
import { importFetchedAccounts } from './importer';
import { updateTimeline } from './timelines';
import { showAlertForError } from './alerts';
@@ -182,18 +183,14 @@ export function uploadCompose(files) {
dispatch(uploadComposeRequest());
- let data = new FormData();
- data.append('file', files[0]);
+ resizeImage(files[0]).then(file => {
+ const data = new FormData();
+ data.append('file', file);
- api(getState).post('/api/v1/media', data, {
- onUploadProgress: function (e) {
- dispatch(uploadComposeProgress(e.loaded, e.total));
- },
- }).then(function (response) {
- dispatch(uploadComposeSuccess(response.data));
- }).catch(function (error) {
- dispatch(uploadComposeFail(error));
- });
+ return api(getState).post('/api/v1/media', data, {
+ onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)),
+ }).then(({ data }) => dispatch(uploadComposeSuccess(data)));
+ }).catch(error => dispatch(uploadComposeFail(error)));
};
};
diff --git a/app/javascript/mastodon/actions/push_notifications/registerer.js b/app/javascript/mastodon/actions/push_notifications/registerer.js
index 60b215f028..82fe4519a2 100644
--- a/app/javascript/mastodon/actions/push_notifications/registerer.js
+++ b/app/javascript/mastodon/actions/push_notifications/registerer.js
@@ -1,4 +1,5 @@
import api from '../../api';
+import { decode as decodeBase64 } from '../../utils/base64';
import { pushNotificationsSetting } from '../../settings';
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
import { me } from '../../initial_state';
@@ -10,13 +11,7 @@ const urlBase64ToUint8Array = (base64String) => {
.replace(/\-/g, '+')
.replace(/_/g, '/');
- const rawData = window.atob(base64);
- const outputArray = new Uint8Array(rawData.length);
-
- for (let i = 0; i < rawData.length; ++i) {
- outputArray[i] = rawData.charCodeAt(i);
- }
- return outputArray;
+ return decodeBase64(base64);
};
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
diff --git a/app/javascript/mastodon/base_polyfills.js b/app/javascript/mastodon/base_polyfills.js
index 8fbb177858..997813a043 100644
--- a/app/javascript/mastodon/base_polyfills.js
+++ b/app/javascript/mastodon/base_polyfills.js
@@ -5,6 +5,7 @@ import includes from 'array-includes';
import assign from 'object-assign';
import values from 'object.values';
import isNaN from 'is-nan';
+import { decode as decodeBase64 } from './utils/base64';
if (!Array.prototype.includes) {
includes.shim();
@@ -21,3 +22,23 @@ if (!Object.values) {
if (!Number.isNaN) {
Number.isNaN = isNaN;
}
+
+if (!HTMLCanvasElement.prototype.toBlob) {
+ const BASE64_MARKER = ';base64,';
+
+ Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
+ value(callback, type = 'image/png', quality) {
+ const dataURL = this.toDataURL(type, quality);
+ let data;
+
+ if (dataURL.indexOf(BASE64_MARKER) >= 0) {
+ const [, base64] = dataURL.split(BASE64_MARKER);
+ data = decodeBase64(base64);
+ } else {
+ [, data] = dataURL.split(',');
+ }
+
+ callback(new Blob([data], { type }));
+ },
+ });
+}
diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js
index 34904194f7..a4f5cf50c6 100644
--- a/app/javascript/mastodon/components/autosuggest_textarea.js
+++ b/app/javascript/mastodon/components/autosuggest_textarea.js
@@ -84,9 +84,17 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
return;
}
+ if (e.which === 229 || e.isComposing) {
+ // Ignore key events during text composition
+ // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
+ return;
+ }
+
switch(e.key) {
case 'Escape':
- if (!suggestionsHidden) {
+ if (suggestions.size === 0 || suggestionsHidden) {
+ document.querySelector('.ui').parentElement.focus();
+ } else {
e.preventDefault();
this.setState({ suggestionsHidden: true });
}
@@ -125,16 +133,6 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
this.props.onKeyDown(e);
}
- onKeyUp = e => {
- if (e.key === 'Escape' && this.state.suggestionsHidden) {
- document.querySelector('.ui').parentElement.focus();
- }
-
- if (this.props.onKeyUp) {
- this.props.onKeyUp(e);
- }
- }
-
onBlur = () => {
this.setState({ suggestionsHidden: true });
}
@@ -186,7 +184,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
}
render () {
- const { value, suggestions, disabled, placeholder, autoFocus } = this.props;
+ const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus } = this.props;
const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' };
@@ -208,7 +206,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
- onKeyUp={this.onKeyUp}
+ onKeyUp={onKeyUp}
onBlur={this.onBlur}
onPaste={this.onPaste}
style={style}
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index c5c6f73b33..982d34718e 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -63,7 +63,7 @@ class DropdownMenu extends React.PureComponent {
if (typeof action === 'function') {
e.preventDefault();
- action();
+ action(e);
} else if (to) {
e.preventDefault();
this.context.router.history.push(to);
diff --git a/app/javascript/mastodon/components/relative_timestamp.js b/app/javascript/mastodon/components/relative_timestamp.js
index 51588e78ca..3c8db70926 100644
--- a/app/javascript/mastodon/components/relative_timestamp.js
+++ b/app/javascript/mastodon/components/relative_timestamp.js
@@ -20,7 +20,7 @@ const dateFormatOptions = {
};
const shortDateFormatOptions = {
- month: 'numeric',
+ month: 'short',
day: 'numeric',
};
@@ -66,12 +66,17 @@ export default class RelativeTimestamp extends React.Component {
static propTypes = {
intl: PropTypes.object.isRequired,
timestamp: PropTypes.string.isRequired,
+ year: PropTypes.number.isRequired,
};
state = {
now: this.props.intl.now(),
};
+ static defaultProps = {
+ year: (new Date()).getFullYear(),
+ };
+
shouldComponentUpdate (nextProps, nextState) {
// As of right now the locale doesn't change without a new page load,
// but we might as well check in case that ever changes.
@@ -114,7 +119,7 @@ export default class RelativeTimestamp extends React.Component {
}
render () {
- const { timestamp, intl } = this.props;
+ const { timestamp, intl, year } = this.props;
const date = new Date(timestamp);
const delta = this.state.now - date.getTime();
@@ -123,7 +128,7 @@ export default class RelativeTimestamp extends React.Component {
if (delta < 10 * SECOND) {
relativeTime = intl.formatMessage(messages.just_now);
- } else if (delta < 3 * DAY) {
+ } else if (delta < 7 * DAY) {
if (delta < MINUTE) {
relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
} else if (delta < HOUR) {
@@ -133,8 +138,10 @@ export default class RelativeTimestamp extends React.Component {
} else {
relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
}
- } else {
+ } else if (date.getFullYear() === year) {
relativeTime = intl.formatDate(date, shortDateFormatOptions);
+ } else {
+ relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' });
}
return (
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
index fd6858d05b..f8a7f91d2e 100644
--- a/app/javascript/mastodon/components/scrollable_list.js
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -35,6 +35,7 @@ export default class ScrollableList extends PureComponent {
state = {
fullscreen: null,
+ mouseOver: false,
};
intersectionObserverWrapper = new IntersectionObserverWrapper();
@@ -71,7 +72,7 @@ export default class ScrollableList extends PureComponent {
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
- if (someItemInserted && this.node.scrollTop > 0) {
+ if (someItemInserted && this.node.scrollTop > 0 || this.state.mouseOver) {
return this.node.scrollHeight - this.node.scrollTop;
} else {
return null;
@@ -139,6 +140,14 @@ export default class ScrollableList extends PureComponent {
this.props.onLoadMore();
}
+ handleMouseEnter = () => {
+ this.setState({ mouseOver: true });
+ }
+
+ handleMouseLeave = () => {
+ this.setState({ mouseOver: false });
+ }
+
render () {
const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props;
const { fullscreen } = this.state;
@@ -149,7 +158,7 @@ export default class ScrollableList extends PureComponent {
if (isLoading || childrenCount > 0 || !emptyMessage) {
scrollableArea = (
-
+
{prepend}
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index e5f7c9399b..402d558c4c 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -114,12 +114,12 @@ export default class Status extends ImmutablePureComponent {
this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`);
}
- handleHotkeyMoveUp = () => {
- this.props.onMoveUp(this.props.status.get('id'));
+ handleHotkeyMoveUp = e => {
+ this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
}
- handleHotkeyMoveDown = () => {
- this.props.onMoveDown(this.props.status.get('id'));
+ handleHotkeyMoveDown = e => {
+ this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
}
handleHotkeyToggleHidden = () => {
@@ -233,7 +233,7 @@ export default class Status extends ImmutablePureComponent {
return (
-
+
{prepend}
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index e58625582e..d605dbc8a9 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -153,7 +153,9 @@ export default class StatusActionBar extends ImmutablePureComponent {
if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
} else {
- menu.push({ text: intl.formatMessage(status.get('reblog') ? messages.reblog_private : messages.cancel_reblog_private), action: this.handleReblogClick });
+ if (status.get('visibility') === 'private') {
+ menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
+ }
}
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js
index c98d4564e5..0c971ceb00 100644
--- a/app/javascript/mastodon/components/status_list.js
+++ b/app/javascript/mastodon/components/status_list.js
@@ -30,13 +30,25 @@ export default class StatusList extends ImmutablePureComponent {
trackScroll: true,
};
- handleMoveUp = id => {
- const elementIndex = this.props.statusIds.indexOf(id) - 1;
+ getFeaturedStatusCount = () => {
+ return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0;
+ }
+
+ getCurrentStatusIndex = (id, featured) => {
+ if (featured) {
+ return this.props.featuredStatusIds.indexOf(id);
+ } else {
+ return this.props.statusIds.indexOf(id) + this.getFeaturedStatusCount();
+ }
+ }
+
+ handleMoveUp = (id, featured) => {
+ const elementIndex = this.getCurrentStatusIndex(id, featured) - 1;
this._selectChild(elementIndex);
}
- handleMoveDown = id => {
- const elementIndex = this.props.statusIds.indexOf(id) + 1;
+ handleMoveDown = (id, featured) => {
+ const elementIndex = this.getCurrentStatusIndex(id, featured) + 1;
this._selectChild(elementIndex);
}
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
index dc8fc02ba4..84665a7e80 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
@@ -162,12 +162,12 @@ class EmojiPickerMenu extends React.PureComponent {
static defaultProps = {
style: {},
loading: true,
- placement: 'bottom',
frequentlyUsedEmojis: [],
};
state = {
modifierOpen: false,
+ placement: null,
};
handleDocumentClick = e => {
@@ -298,7 +298,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
this.dropdown = c;
}
- onShowDropdown = () => {
+ onShowDropdown = ({ target }) => {
this.setState({ active: true });
if (!EmojiPicker) {
@@ -313,6 +313,9 @@ export default class EmojiPickerDropdown extends React.PureComponent {
this.setState({ loading: false });
});
}
+
+ const { top } = target.getBoundingClientRect();
+ this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
}
onHideDropdown = () => {
@@ -324,7 +327,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
if (this.state.active) {
this.onHideDropdown();
} else {
- this.onShowDropdown();
+ this.onShowDropdown(e);
}
}
}
@@ -346,7 +349,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
render () {
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
const title = intl.formatMessage(messages.emoji);
- const { active, loading } = this.state;
+ const { active, loading, placement } = this.state;
return (
@@ -358,7 +361,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
/>
-
+
-
+
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js
index fc34c8cdc8..bb9b75505b 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.js
+++ b/app/javascript/mastodon/features/status/components/action_bar.js
@@ -123,7 +123,9 @@ export default class ActionBar extends React.PureComponent {
if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
} else {
- menu.push({ text: intl.formatMessage(status.get('reblog') ? messages.reblog_private : messages.cancel_reblog_private), action: this.handleReblogClick });
+ if (status.get('visibility') === 'private') {
+ menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
+ }
}
menu.push(null);
diff --git a/app/javascript/mastodon/load_polyfills.js b/app/javascript/mastodon/load_polyfills.js
index 815e1905b8..8cb81c1a61 100644
--- a/app/javascript/mastodon/load_polyfills.js
+++ b/app/javascript/mastodon/load_polyfills.js
@@ -12,12 +12,13 @@ function importExtraPolyfills() {
function loadPolyfills() {
const needsBasePolyfills = !(
+ Array.prototype.includes &&
+ HTMLCanvasElement.prototype.toBlob &&
window.Intl &&
+ Number.isNaN &&
Object.assign &&
Object.values &&
- Number.isNaN &&
- window.Symbol &&
- Array.prototype.includes
+ window.Symbol
);
// Latest version of Firefox and Safari do not have IntersectionObserver.
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 24c8a5b54a..947348f701 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -2,7 +2,7 @@
"account.block": "حظر @{name}",
"account.block_domain": "إخفاء كل شيئ قادم من إسم النطاق {domain}",
"account.blocked": "محظور",
- "account.direct": "Direct message @{name}",
+ "account.direct": "رسالة خاصة إلى @{name}",
"account.disclaimer_full": "قد لا تعكس المعلومات أدناه الملف الشخصي الكامل للمستخدم.",
"account.domain_blocked": "النطاق مخفي",
"account.edit_profile": "تعديل الملف الشخصي",
@@ -18,7 +18,7 @@
"account.mute_notifications": "كتم إخطارات @{name}",
"account.muted": "مكتوم",
"account.posts": "التبويقات",
- "account.posts_with_replies": "تبويقات تحتوي على رُدود",
+ "account.posts_with_replies": "التبويقات و الردود",
"account.report": "أبلغ عن @{name}",
"account.requested": "في انتظار الموافقة",
"account.share": "مشاركة @{name}'s profile",
@@ -29,8 +29,8 @@
"account.unmute": "إلغاء الكتم عن @{name}",
"account.unmute_notifications": "إلغاء كتم إخطارات @{name}",
"account.view_full_profile": "عرض الملف الشخصي كاملا",
- "alert.unexpected.message": "An unexpected error occurred.",
- "alert.unexpected.title": "Oops!",
+ "alert.unexpected.message": "لقد طرأ هناك خطأ غير متوقّع.",
+ "alert.unexpected.title": "المعذرة !",
"boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة",
"bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.",
"bundle_column_error.retry": "إعادة المحاولة",
@@ -40,8 +40,8 @@
"bundle_modal_error.retry": "إعادة المحاولة",
"column.blocks": "الحسابات المحجوبة",
"column.community": "الخيط العام المحلي",
- "column.direct": "Direct messages",
- "column.domain_blocks": "Hidden domains",
+ "column.direct": "الرسائل المباشرة",
+ "column.domain_blocks": "النطاقات المخفية",
"column.favourites": "المفضلة",
"column.follow_requests": "طلبات المتابعة",
"column.home": "الرئيسية",
@@ -59,7 +59,7 @@
"column_header.unpin": "فك التدبيس",
"column_subheading.navigation": "التصفح",
"column_subheading.settings": "الإعدادات",
- "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+ "compose_form.direct_message_warning": "لن يَظهر هذا التبويق إلا للمستخدمين المذكورين.",
"compose_form.hashtag_warning": "هذا التبويق لن يُدرَج تحت أي وسم كان بما أنه غير مُدرَج. لا يُسمح بالبحث إلّا عن التبويقات العمومية عن طريق الوسوم.",
"compose_form.lock_disclaimer": "حسابك ليس {locked}. يمكن لأي شخص متابعتك و عرض المنشورات.",
"compose_form.lock_disclaimer.lock": "مقفل",
@@ -101,7 +101,7 @@
"emoji_button.symbols": "رموز",
"emoji_button.travel": "أماكن و أسفار",
"empty_column.community": "الخط الزمني المحلي فارغ. أكتب شيئا ما للعامة كبداية !",
- "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+ "empty_column.direct": "لم تتلق أية رسالة خاصة مباشِرة بعد. سوف يتم عرض الرسائل المباشرة هنا إن قمت بإرسال واحدة أو تلقيت البعض منها.",
"empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
"empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.",
"empty_column.home.public_timeline": "الخيط العام",
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "لذِكر الناشر",
"keyboard_shortcuts.reply": "للردّ",
"keyboard_shortcuts.search": "للتركيز على البحث",
+ "keyboard_shortcuts.toggle_hidden": "لعرض أو إخفاء النص مِن وراء التحذير",
"keyboard_shortcuts.toot": "لتحرير تبويق جديد",
"keyboard_shortcuts.unfocus": "لإلغاء التركيز على حقل النص أو نافذة البحث",
"keyboard_shortcuts.up": "للإنتقال إلى أعلى القائمة",
@@ -156,8 +157,8 @@
"mute_modal.hide_notifications": "هل تود إخفاء الإخطارات القادمة من هذا المستخدم ؟",
"navigation_bar.blocks": "الحسابات المحجوبة",
"navigation_bar.community_timeline": "الخيط العام المحلي",
- "navigation_bar.direct": "Direct messages",
- "navigation_bar.domain_blocks": "Hidden domains",
+ "navigation_bar.direct": "الرسائل المباشِرة",
+ "navigation_bar.domain_blocks": "النطاقات المخفية",
"navigation_bar.edit_profile": "تعديل الملف الشخصي",
"navigation_bar.favourites": "المفضلة",
"navigation_bar.follow_requests": "طلبات المتابعة",
@@ -241,10 +242,10 @@
"search_results.total": "{count, number} {count, plural, one {result} و {results}}",
"standalone.public_title": "نظرة على ...",
"status.block": "Block @{name}",
- "status.cancel_reblog_private": "Unboost",
+ "status.cancel_reblog_private": "إلغاء الترقية",
"status.cannot_reblog": "تعذرت ترقية هذا المنشور",
"status.delete": "إحذف",
- "status.direct": "Direct message @{name}",
+ "status.direct": "رسالة خاصة إلى @{name}",
"status.embed": "إدماج",
"status.favourite": "أضف إلى المفضلة",
"status.load_more": "حمّل المزيد",
@@ -275,7 +276,7 @@
"tabs_bar.home": "الرئيسية",
"tabs_bar.local_timeline": "المحلي",
"tabs_bar.notifications": "الإخطارات",
- "tabs_bar.search": "Search",
+ "tabs_bar.search": "البحث",
"ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
"upload_area.title": "إسحب ثم أفلت للرفع",
"upload_button.label": "إضافة وسائط",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 25ef6db653..9714751140 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "to start a brand new toot",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 6a44808e04..f2e3699d59 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -2,7 +2,7 @@
"account.block": "Bloca @{name}",
"account.block_domain": "Amaga-ho tot de {domain}",
"account.blocked": "Bloquejat",
- "account.direct": "Direct message @{name}",
+ "account.direct": "Missatge directe @{name}",
"account.disclaimer_full": "La informació següent pot reflectir incompleta el perfil de l'usuari.",
"account.domain_blocked": "Domini ocult",
"account.edit_profile": "Edita el perfil",
@@ -18,7 +18,7 @@
"account.mute_notifications": "Notificacions desactivades de @{name}",
"account.muted": "Silenciat",
"account.posts": "Toots",
- "account.posts_with_replies": "Toots amb respostes",
+ "account.posts_with_replies": "Toots i respostes",
"account.report": "Informe @{name}",
"account.requested": "Esperant aprovació. Clic per a cancel·lar la petició de seguiment",
"account.share": "Comparteix el perfil de @{name}",
@@ -29,8 +29,8 @@
"account.unmute": "Treure silenci de @{name}",
"account.unmute_notifications": "Activar notificacions de @{name}",
"account.view_full_profile": "Mostra el perfil complet",
- "alert.unexpected.message": "An unexpected error occurred.",
- "alert.unexpected.title": "Oops!",
+ "alert.unexpected.message": "S'ha produït un error inesperat.",
+ "alert.unexpected.title": "Vaja!",
"boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop",
"bundle_column_error.body": "S'ha produït un error en carregar aquest component.",
"bundle_column_error.retry": "Torna-ho a provar",
@@ -40,8 +40,8 @@
"bundle_modal_error.retry": "Torna-ho a provar",
"column.blocks": "Usuaris blocats",
"column.community": "Línia de temps local",
- "column.direct": "Direct messages",
- "column.domain_blocks": "Hidden domains",
+ "column.direct": "Missatges directes",
+ "column.domain_blocks": "Dominis ocults",
"column.favourites": "Favorits",
"column.follow_requests": "Peticions per seguir-te",
"column.home": "Inici",
@@ -59,7 +59,7 @@
"column_header.unpin": "No fixis",
"column_subheading.navigation": "Navegació",
"column_subheading.settings": "Configuració",
- "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+ "compose_form.direct_message_warning": "Aquest toot només serà visible per a tots els usuaris esmentats.",
"compose_form.hashtag_warning": "Aquest toot no es mostrarà en cap etiqueta ja que no està llistat. Només els toots públics poden ser cercats per etiqueta.",
"compose_form.lock_disclaimer": "El teu compte no està bloquejat {locked}. Tothom pot seguir-te i veure els teus missatges a seguidors.",
"compose_form.lock_disclaimer.lock": "blocat",
@@ -68,7 +68,7 @@
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.marked": "Mèdia marcat com a sensible",
"compose_form.sensitive.unmarked": "Mèdia no està marcat com a sensible",
- "compose_form.spoiler.marked": "Text ocult sota l'avís",
+ "compose_form.spoiler.marked": "Text es ocult sota l'avís",
"compose_form.spoiler.unmarked": "Text no ocult",
"compose_form.spoiler_placeholder": "Escriu l'avís aquí",
"confirmation_modal.cancel": "Cancel·la",
@@ -101,7 +101,7 @@
"emoji_button.symbols": "Símbols",
"emoji_button.travel": "Viatges i Llocs",
"empty_column.community": "La línia de temps local és buida. Escriu alguna cosa públicament per fer rodar la pilota!",
- "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+ "empty_column.direct": "Encara no tens missatges directes. Quan enviïs o rebis un, es mostrarà aquí.",
"empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.",
"empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.",
"empty_column.home.public_timeline": "la línia de temps pública",
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "per esmentar l'autor",
"keyboard_shortcuts.reply": "respondre",
"keyboard_shortcuts.search": "per centrar la cerca",
+ "keyboard_shortcuts.toggle_hidden": "per a mostrar/amagar text sota CW",
"keyboard_shortcuts.toot": "per a començar un toot nou de trinca",
"keyboard_shortcuts.unfocus": "descentrar l'area de composició de text/cerca",
"keyboard_shortcuts.up": "moure amunt en la llista",
@@ -156,8 +157,8 @@
"mute_modal.hide_notifications": "Amagar notificacions d'aquest usuari?",
"navigation_bar.blocks": "Usuaris bloquejats",
"navigation_bar.community_timeline": "Línia de temps Local",
- "navigation_bar.direct": "Direct messages",
- "navigation_bar.domain_blocks": "Hidden domains",
+ "navigation_bar.direct": "Missatges directes",
+ "navigation_bar.domain_blocks": "Dominis ocults",
"navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.favourites": "Favorits",
"navigation_bar.follow_requests": "Sol·licituds de seguiment",
@@ -241,10 +242,10 @@
"search_results.total": "{count, number} {count, plural, un {result} altres {results}}",
"standalone.public_title": "Una mirada a l'interior ...",
"status.block": "Block @{name}",
- "status.cancel_reblog_private": "Unboost",
+ "status.cancel_reblog_private": "Desfer l'impuls",
"status.cannot_reblog": "Aquesta publicació no pot ser retootejada",
"status.delete": "Esborrar",
- "status.direct": "Direct message @{name}",
+ "status.direct": "Missatge directe @{name}",
"status.embed": "Incrustar",
"status.favourite": "Favorit",
"status.load_more": "Carrega més",
@@ -257,7 +258,7 @@
"status.pin": "Fixat en el perfil",
"status.pinned": "Toot fixat",
"status.reblog": "Impuls",
- "status.reblog_private": "Boost to original audience",
+ "status.reblog_private": "Impulsar a l'audiència original",
"status.reblogged_by": "{name} ha retootejat",
"status.reply": "Respondre",
"status.replyAll": "Respondre al tema",
@@ -275,7 +276,7 @@
"tabs_bar.home": "Inici",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notificacions",
- "tabs_bar.search": "Search",
+ "tabs_bar.search": "Cerca",
"ui.beforeunload": "El vostre esborrany es perdrà si sortiu de Mastodon.",
"upload_area.title": "Arrossega i deixa anar per carregar",
"upload_button.label": "Afegir multimèdia",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 69c2ae8d87..f442e06754 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -18,7 +18,7 @@
"account.mute_notifications": "Benachrichtigungen von @{name} verbergen",
"account.muted": "Stummgeschaltet",
"account.posts": "Beiträge",
- "account.posts_with_replies": "Beiträge mit Antworten",
+ "account.posts_with_replies": "Beiträge und Antworten",
"account.report": "@{name} melden",
"account.requested": "Warte auf Erlaubnis. Klicke zum Abbrechen",
"account.share": "Profil von @{name} teilen",
@@ -29,8 +29,8 @@
"account.unmute": "@{name} nicht mehr stummschalten",
"account.unmute_notifications": "Benachrichtigungen von @{name} einschalten",
"account.view_full_profile": "Vollständiges Profil anzeigen",
- "alert.unexpected.message": "An unexpected error occurred.",
- "alert.unexpected.title": "Oops!",
+ "alert.unexpected.message": "Ein unerwarteter Fehler ist aufgetreten.",
+ "alert.unexpected.title": "Hoppla!",
"boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
"bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.",
"bundle_column_error.retry": "Erneut versuchen",
@@ -40,8 +40,8 @@
"bundle_modal_error.retry": "Erneut versuchen",
"column.blocks": "Blockierte Profile",
"column.community": "Lokale Zeitleiste",
- "column.direct": "Direct messages",
- "column.domain_blocks": "Hidden domains",
+ "column.direct": "Direktnachrichten",
+ "column.domain_blocks": "Versteckte Domains",
"column.favourites": "Favoriten",
"column.follow_requests": "Folgeanfragen",
"column.home": "Startseite",
@@ -59,17 +59,17 @@
"column_header.unpin": "Lösen",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Einstellungen",
- "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+ "compose_form.direct_message_warning": "Dieser Beitrag wird nur für die erwähnten Nutzer sichtbar sein.",
"compose_form.hashtag_warning": "Dieser Beitrag wird nicht unter einen dieser Hashtags sichtbar sein, solange er ungelistet ist. Bei einer Suche kann er nicht gefunden werden.",
"compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Wer dir folgen will, kann das jederzeit tun und dann auch deine privaten Beiträge sehen.",
"compose_form.lock_disclaimer.lock": "gesperrt",
"compose_form.placeholder": "Worüber möchtest du schreiben?",
"compose_form.publish": "Tröt",
"compose_form.publish_loud": "{publish}!",
- "compose_form.sensitive.marked": "Media is marked as sensitive",
- "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
- "compose_form.spoiler.marked": "Text is hidden behind warning",
- "compose_form.spoiler.unmarked": "Text is not hidden",
+ "compose_form.sensitive.marked": "Medien sind als heikel markiert",
+ "compose_form.sensitive.unmarked": "Medien sind nicht als heikel markiert",
+ "compose_form.spoiler.marked": "Text ist hinter einer Warnung versteckt",
+ "compose_form.spoiler.unmarked": "Text ist nicht versteckt",
"compose_form.spoiler_placeholder": "Inhaltswarnung",
"confirmation_modal.cancel": "Abbrechen",
"confirmations.block.confirm": "Blockieren",
@@ -101,7 +101,7 @@
"emoji_button.symbols": "Symbole",
"emoji_button.travel": "Reisen und Orte",
"empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe einen öffentlichen Beitrag, um den Ball ins Rollen zu bringen!",
- "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+ "empty_column.direct": "Du hast noch keine Direktnachrichten erhalten. Wenn du eine sendest oder empfängst, wird sie hier zu sehen sein.",
"empty_column.hashtag": "Unter diesem Hashtag gibt es noch nichts.",
"empty_column.home": "Deine Startseite ist leer! Besuche {public} oder nutze die Suche, um loszulegen und andere Leute zu finden.",
"empty_column.home.public_timeline": "die öffentliche Zeitleiste",
@@ -130,11 +130,12 @@
"keyboard_shortcuts.enter": "um den Status zu öffnen",
"keyboard_shortcuts.favourite": "um zu favorisieren",
"keyboard_shortcuts.heading": "Tastenkombinationen",
- "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.hotkey": "Tastenkürzel",
"keyboard_shortcuts.legend": "um diese Übersicht anzuzeigen",
"keyboard_shortcuts.mention": "um Autor_in zu erwähnen",
"keyboard_shortcuts.reply": "um zu antworten",
"keyboard_shortcuts.search": "um die Suche zu fokussieren",
+ "keyboard_shortcuts.toggle_hidden": "um den Text hinter einer Inhaltswarnung zu verstecken oder ihn anzuzeigen",
"keyboard_shortcuts.toot": "um einen neuen Toot zu beginnen",
"keyboard_shortcuts.unfocus": "um das Textfeld/die Suche nicht mehr zu fokussieren",
"keyboard_shortcuts.up": "sich in der Liste hinauf bewegen",
@@ -156,8 +157,8 @@
"mute_modal.hide_notifications": "Benachrichtigungen von diesem Account verbergen?",
"navigation_bar.blocks": "Blockierte Profile",
"navigation_bar.community_timeline": "Lokale Zeitleiste",
- "navigation_bar.direct": "Direct messages",
- "navigation_bar.domain_blocks": "Hidden domains",
+ "navigation_bar.direct": "Direktnachrichten",
+ "navigation_bar.domain_blocks": "Versteckte Domains",
"navigation_bar.edit_profile": "Profil bearbeiten",
"navigation_bar.favourites": "Favoriten",
"navigation_bar.follow_requests": "Folgeanfragen",
@@ -190,8 +191,8 @@
"onboarding.page_four.home": "Die Startseite zeigt dir Beiträge von Leuten, denen du folgst.",
"onboarding.page_four.notifications": "Wenn jemand mit dir interagiert, bekommst du eine Mitteilung.",
"onboarding.page_one.federation": "Mastodon ist ein soziales Netzwerk, das aus unabhängigen Servern besteht. Diese Server nennen wir auch Instanzen.",
- "onboarding.page_one.full_handle": "Your full handle",
- "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
+ "onboarding.page_one.full_handle": "Dein vollständiger Benutzername",
+ "onboarding.page_one.handle_hint": "Das ist das, was du deinen Freunden sagst, um nach dir zu suchen.",
"onboarding.page_one.welcome": "Willkommen bei Mastodon!",
"onboarding.page_six.admin": "Für deine Instanz ist {admin} zuständig.",
"onboarding.page_six.almost_done": "Fast fertig …",
@@ -214,50 +215,50 @@
"privacy.public.short": "Öffentlich",
"privacy.unlisted.long": "Nicht in öffentlichen Zeitleisten anzeigen",
"privacy.unlisted.short": "Nicht gelistet",
- "regeneration_indicator.label": "Loading…",
- "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+ "regeneration_indicator.label": "Laden…",
+ "regeneration_indicator.sublabel": "Deine Heimzeitleiste wird gerade vorbereitet!",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
- "relative_time.just_now": "now",
+ "relative_time.just_now": "jetzt",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Abbrechen",
- "report.forward": "Forward to {target}",
- "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
- "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+ "report.forward": "An {target} weiterleiten",
+ "report.forward_hint": "Dieses Konto ist von einem anderen Server. Soll eine anonymisierte Kopie des Berichts auch dorthin geschickt werden?",
+ "report.hint": "Der Bericht wird an die Moderatoren deiner Instanz geschickt. Du kannst hier eine Erklärung angeben, warum du dieses Konto meldest:",
"report.placeholder": "Zusätzliche Kommentare",
"report.submit": "Absenden",
"report.target": "{target} melden",
"search.placeholder": "Suche",
- "search_popout.search_format": "Advanced search format",
- "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
- "search_popout.tips.hashtag": "hashtag",
+ "search_popout.search_format": "Fortgeschrittenes Suchformat",
+ "search_popout.tips.full_text": "Simpler Text gibt Beiträge, die du geschrieben, favorisiert und geteilt hast zurück. Außerdem auch Beiträge in denen du erwähnt wurdest, als auch passende Nutzernamen, Anzeigenamen oder Hashtags.",
+ "search_popout.tips.hashtag": "Hashtag",
"search_popout.tips.status": "status",
- "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
- "search_popout.tips.user": "user",
- "search_results.accounts": "People",
+ "search_popout.tips.text": "Einfacher Text gibt Anzeigenamen, Benutzernamen und Hashtags zurück",
+ "search_popout.tips.user": "Nutzer",
+ "search_results.accounts": "Personen",
"search_results.hashtags": "Hashtags",
- "search_results.statuses": "Toots",
+ "search_results.statuses": "Beiträge",
"search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}",
"standalone.public_title": "Ein kleiner Einblick …",
"status.block": "Block @{name}",
- "status.cancel_reblog_private": "Unboost",
+ "status.cancel_reblog_private": "Nicht mehr teilen",
"status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
"status.delete": "Löschen",
- "status.direct": "Direct message @{name}",
+ "status.direct": "Direktnachricht @{name}",
"status.embed": "Einbetten",
"status.favourite": "Favorisieren",
"status.load_more": "Weitere laden",
"status.media_hidden": "Medien versteckt",
"status.mention": "@{name} erwähnen",
"status.more": "Mehr",
- "status.mute": "Mute @{name}",
+ "status.mute": "@{name} stummschalten",
"status.mute_conversation": "Thread stummschalten",
"status.open": "Diesen Beitrag öffnen",
"status.pin": "Im Profil anheften",
- "status.pinned": "Pinned toot",
+ "status.pinned": "Angehefteter Beitrag",
"status.reblog": "Teilen",
- "status.reblog_private": "Boost to original audience",
+ "status.reblog_private": "An das eigentliche Publikum teilen",
"status.reblogged_by": "{name} teilte",
"status.reply": "Antworten",
"status.replyAll": "Auf Thread antworten",
@@ -266,21 +267,21 @@
"status.sensitive_warning": "Heikle Inhalte",
"status.share": "Teilen",
"status.show_less": "Weniger anzeigen",
- "status.show_less_all": "Show less for all",
+ "status.show_less_all": "Zeige weniger für alles",
"status.show_more": "Mehr anzeigen",
- "status.show_more_all": "Show more for all",
+ "status.show_more_all": "Zeige mehr für alles",
"status.unmute_conversation": "Stummschaltung von Thread aufheben",
"status.unpin": "Vom Profil lösen",
"tabs_bar.federated_timeline": "Föderation",
"tabs_bar.home": "Startseite",
"tabs_bar.local_timeline": "Lokal",
"tabs_bar.notifications": "Mitteilungen",
- "tabs_bar.search": "Search",
- "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+ "tabs_bar.search": "Suchen",
+ "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.",
"upload_area.title": "Zum Hochladen hereinziehen",
"upload_button.label": "Mediendatei hinzufügen",
"upload_form.description": "Für Menschen mit Sehbehinderung beschreiben",
- "upload_form.focus": "Crop",
+ "upload_form.focus": "Zuschneiden",
"upload_form.undo": "Entfernen",
"upload_progress.label": "Wird hochgeladen …",
"video.close": "Video schließen",
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
new file mode 100644
index 0000000000..a7e1c408f8
--- /dev/null
+++ b/app/javascript/mastodon/locales/el.json
@@ -0,0 +1,296 @@
+{
+ "account.block": "Απόκλεισε τον/την @{name}",
+ "account.block_domain": "Απόκρυψε τα πάντα από τον/την",
+ "account.blocked": "Αποκλεισμένος/η",
+ "account.direct": "Απευθείας μήνυμα προς @{name}",
+ "account.disclaimer_full": "Οι παρακάτω πληροφορίες μπορει να μην αντανακλούν το προφίλ του χρήστη επαρκως.",
+ "account.domain_blocked": "Domain hidden",
+ "account.edit_profile": "Επεξεργάσου το προφίλ",
+ "account.follow": "Ακολούθησε",
+ "account.followers": "Ακόλουθοι",
+ "account.follows": "Ακολουθεί",
+ "account.follows_you": "Σε ακολουθεί",
+ "account.hide_reblogs": "Απόκρυψη προωθήσεων από τον/την @{name}",
+ "account.media": "Πολυμέσα",
+ "account.mention": "Ανέφερε τον/την @{name}",
+ "account.moved_to": "{name} μετακόμισε στο:",
+ "account.mute": "Σώπασε τον/την @{name}",
+ "account.mute_notifications": "Σώπασε τις ειδοποιήσεις από τον/την @{name}",
+ "account.muted": "Αποσιωπημένος/η",
+ "account.posts": "Τουτ",
+ "account.posts_with_replies": "Τουτ και απαντήσεις",
+ "account.report": "Ανέφερε τον/την @{name}",
+ "account.requested": "Εκκρεμεί έγκριση. Κάνε κλικ για να ακυρώσεις το αίτημα ακολούθησης",
+ "account.share": "Μοιράσου το προφίλ του/της @{name}",
+ "account.show_reblogs": "Δείξε τις προωθήσεις του/της @{name}",
+ "account.unblock": "Unblock @{name}",
+ "account.unblock_domain": "Αποκάλυψε το {domain}",
+ "account.unfollow": "Unfollow",
+ "account.unmute": "Unmute @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
+ "account.view_full_profile": "Δες το πλήρες προφίλ",
+ "alert.unexpected.message": "Προέκυψε απροσδόκητο σφάλμα.",
+ "alert.unexpected.title": "Εεπ!",
+ "boost_modal.combo": "You can press {combo} to skip this next time",
+ "bundle_column_error.body": "Κάτι πήγε στραβά ενώ φορτωνόταν αυτό το στοιχείο.",
+ "bundle_column_error.retry": "Δοκίμασε ξανά",
+ "bundle_column_error.title": "Σφάλμα δικτύου",
+ "bundle_modal_error.close": "Κλείσε",
+ "bundle_modal_error.message": "Κάτι πήγε στραβά ενώ φορτωνόταν αυτό το στοιχείο.",
+ "bundle_modal_error.retry": "Δοκίμασε ξανά",
+ "column.blocks": "Αποκλεισμένοι χρήστες",
+ "column.community": "Τοπική ροή",
+ "column.direct": "Απευθείας μηνύματα",
+ "column.domain_blocks": "Hidden domains",
+ "column.favourites": "Αγαπημένα",
+ "column.follow_requests": "Αιτήματα παρακολούθησης",
+ "column.home": "Αρχική",
+ "column.lists": "Λίστες",
+ "column.mutes": "Αποσιωπημένοι χρήστες",
+ "column.notifications": "Ειδοποιήσεις",
+ "column.pins": "Καρφιτσωμένα τουτ",
+ "column.public": "Ομοσπονδιακή ροή",
+ "column_back_button.label": "Πίσω",
+ "column_header.hide_settings": "Απόκρυψη ρυθμίσεων",
+ "column_header.moveLeft_settings": "Μεταφορά κολώνας αριστερά",
+ "column_header.moveRight_settings": "Μεταφορά κολώνας δεξιά",
+ "column_header.pin": "Καρφίτσωμα",
+ "column_header.show_settings": "Εμφάνιση ρυθμίσεων",
+ "column_header.unpin": "Ξεκαρφίτσωμα",
+ "column_subheading.navigation": "Πλοήγηση",
+ "column_subheading.settings": "Ρυθμίσεις",
+ "compose_form.direct_message_warning": "Αυτό το τουτ θα εμφανίζεται μόνο σε όλους τους αναφερόμενους χρήστες.",
+ "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
+ "compose_form.lock_disclaimer": "Ο λογαριασμός σου δεν είναι {locked}. Οποιοσδήποτε μπορεί να σε ακολουθήσει για να δει τις δημοσιεύσεις σας προς τους ακολούθους σας.",
+ "compose_form.lock_disclaimer.lock": "κλειδωμένος",
+ "compose_form.placeholder": "Τι σκέφτεσαι;",
+ "compose_form.publish": "Τουτ",
+ "compose_form.publish_loud": "{publish}!",
+ "compose_form.sensitive.marked": "Το πολυμέσο έχει σημειωθεί ως ευαίσθητο",
+ "compose_form.sensitive.unmarked": "Το πολυμέσο δεν έχει σημειωθεί ως ευαίσθητο",
+ "compose_form.spoiler.marked": "Κείμενο κρυμμένο πίσω από προειδοποίηση",
+ "compose_form.spoiler.unmarked": "Κείμενο μη κρυμμένο",
+ "compose_form.spoiler_placeholder": "Γράψε την προειδοποίησή σου εδώ",
+ "confirmation_modal.cancel": "Άκυρο",
+ "confirmations.block.confirm": "Απόκλεισε",
+ "confirmations.block.message": "Σίγουρα θες να αποκλείσεις τον/την {name};",
+ "confirmations.delete.confirm": "Διέγραψε",
+ "confirmations.delete.message": "Σίγουρα θες να διαγράψεις αυτή την κατάσταση;",
+ "confirmations.delete_list.confirm": "Διέγραψε",
+ "confirmations.delete_list.message": "Σίγουρα θες να διαγράψεις οριστικά αυτή τη λίστα;",
+ "confirmations.domain_block.confirm": "Hide entire domain",
+ "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
+ "confirmations.mute.confirm": "Mute",
+ "confirmations.mute.message": "Are you sure you want to mute {name}?",
+ "confirmations.unfollow.confirm": "Unfollow",
+ "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+ "embed.instructions": "Embed this status on your website by copying the code below.",
+ "embed.preview": "Here is what it will look like:",
+ "emoji_button.activity": "Activity",
+ "emoji_button.custom": "Custom",
+ "emoji_button.flags": "Flags",
+ "emoji_button.food": "Food & Drink",
+ "emoji_button.label": "Insert emoji",
+ "emoji_button.nature": "Nature",
+ "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.objects": "Objects",
+ "emoji_button.people": "People",
+ "emoji_button.recent": "Frequently used",
+ "emoji_button.search": "Search...",
+ "emoji_button.search_results": "Search results",
+ "emoji_button.symbols": "Symbols",
+ "emoji_button.travel": "Travel & Places",
+ "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
+ "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+ "empty_column.hashtag": "There is nothing in this hashtag yet.",
+ "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
+ "empty_column.home.public_timeline": "the public timeline",
+ "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
+ "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
+ "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
+ "follow_request.authorize": "Authorize",
+ "follow_request.reject": "Reject",
+ "getting_started.appsshort": "Apps",
+ "getting_started.faq": "FAQ",
+ "getting_started.heading": "Getting started",
+ "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
+ "getting_started.userguide": "User Guide",
+ "home.column_settings.advanced": "Advanced",
+ "home.column_settings.basic": "Basic",
+ "home.column_settings.filter_regex": "Filter out by regular expressions",
+ "home.column_settings.show_reblogs": "Show boosts",
+ "home.column_settings.show_replies": "Show replies",
+ "home.settings": "Column settings",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
+ "lightbox.close": "Close",
+ "lightbox.next": "Next",
+ "lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
+ "loading_indicator.label": "Loading...",
+ "media_gallery.toggle_visible": "Toggle visibility",
+ "missing_indicator.label": "Not found",
+ "missing_indicator.sublabel": "This resource could not be found",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
+ "navigation_bar.blocks": "Blocked users",
+ "navigation_bar.community_timeline": "Local timeline",
+ "navigation_bar.direct": "Direct messages",
+ "navigation_bar.domain_blocks": "Hidden domains",
+ "navigation_bar.edit_profile": "Edit profile",
+ "navigation_bar.favourites": "Favourites",
+ "navigation_bar.follow_requests": "Follow requests",
+ "navigation_bar.info": "Extended information",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
+ "navigation_bar.logout": "Logout",
+ "navigation_bar.mutes": "Muted users",
+ "navigation_bar.pins": "Pinned toots",
+ "navigation_bar.preferences": "Preferences",
+ "navigation_bar.public_timeline": "Federated timeline",
+ "notification.favourite": "{name} favourited your status",
+ "notification.follow": "{name} followed you",
+ "notification.mention": "{name} mentioned you",
+ "notification.reblog": "{name} boosted your status",
+ "notifications.clear": "Clear notifications",
+ "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
+ "notifications.column_settings.alert": "Desktop notifications",
+ "notifications.column_settings.favourite": "Favourites:",
+ "notifications.column_settings.follow": "New followers:",
+ "notifications.column_settings.mention": "Mentions:",
+ "notifications.column_settings.push": "Push notifications",
+ "notifications.column_settings.push_meta": "This device",
+ "notifications.column_settings.reblog": "Boosts:",
+ "notifications.column_settings.show": "Show in column",
+ "notifications.column_settings.sound": "Play sound",
+ "onboarding.done": "Done",
+ "onboarding.next": "Next",
+ "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
+ "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
+ "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
+ "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
+ "onboarding.page_one.full_handle": "Your full handle",
+ "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
+ "onboarding.page_one.welcome": "Welcome to Mastodon!",
+ "onboarding.page_six.admin": "Your instance's admin is {admin}.",
+ "onboarding.page_six.almost_done": "Almost done...",
+ "onboarding.page_six.appetoot": "Bon Appetoot!",
+ "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
+ "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
+ "onboarding.page_six.guidelines": "community guidelines",
+ "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
+ "onboarding.page_six.various_app": "mobile apps",
+ "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
+ "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
+ "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
+ "onboarding.skip": "Skip",
+ "privacy.change": "Adjust status privacy",
+ "privacy.direct.long": "Post to mentioned users only",
+ "privacy.direct.short": "Direct",
+ "privacy.private.long": "Post to followers only",
+ "privacy.private.short": "Followers-only",
+ "privacy.public.long": "Post to public timelines",
+ "privacy.public.short": "Public",
+ "privacy.unlisted.long": "Do not show in public timelines",
+ "privacy.unlisted.short": "Unlisted",
+ "regeneration_indicator.label": "Loading…",
+ "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+ "relative_time.days": "{number}d",
+ "relative_time.hours": "{number}h",
+ "relative_time.just_now": "now",
+ "relative_time.minutes": "{number}m",
+ "relative_time.seconds": "{number}s",
+ "reply_indicator.cancel": "Cancel",
+ "report.forward": "Forward to {target}",
+ "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
+ "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+ "report.placeholder": "Additional comments",
+ "report.submit": "Submit",
+ "report.target": "Report {target}",
+ "search.placeholder": "Search",
+ "search_popout.search_format": "Advanced search format",
+ "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+ "search_popout.tips.hashtag": "hashtag",
+ "search_popout.tips.status": "status",
+ "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+ "search_popout.tips.user": "user",
+ "search_results.accounts": "People",
+ "search_results.hashtags": "Hashtags",
+ "search_results.statuses": "Toots",
+ "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+ "standalone.public_title": "A look inside...",
+ "status.block": "Block @{name}",
+ "status.cancel_reblog_private": "Unboost",
+ "status.cannot_reblog": "This post cannot be boosted",
+ "status.delete": "Delete",
+ "status.direct": "Direct message @{name}",
+ "status.embed": "Embed",
+ "status.favourite": "Favourite",
+ "status.load_more": "Load more",
+ "status.media_hidden": "Media hidden",
+ "status.mention": "Mention @{name}",
+ "status.more": "More",
+ "status.mute": "Mute @{name}",
+ "status.mute_conversation": "Mute conversation",
+ "status.open": "Expand this status",
+ "status.pin": "Pin on profile",
+ "status.pinned": "Pinned toot",
+ "status.reblog": "Boost",
+ "status.reblog_private": "Boost to original audience",
+ "status.reblogged_by": "{name} boosted",
+ "status.reply": "Reply",
+ "status.replyAll": "Reply to thread",
+ "status.report": "Report @{name}",
+ "status.sensitive_toggle": "Click to view",
+ "status.sensitive_warning": "Sensitive content",
+ "status.share": "Share",
+ "status.show_less": "Show less",
+ "status.show_less_all": "Show less for all",
+ "status.show_more": "Show more",
+ "status.show_more_all": "Show more for all",
+ "status.unmute_conversation": "Unmute conversation",
+ "status.unpin": "Unpin from profile",
+ "tabs_bar.federated_timeline": "Federated",
+ "tabs_bar.home": "Home",
+ "tabs_bar.local_timeline": "Local",
+ "tabs_bar.notifications": "Notifications",
+ "tabs_bar.search": "Search",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+ "upload_area.title": "Drag & drop to upload",
+ "upload_button.label": "Add media",
+ "upload_form.description": "Describe for the visually impaired",
+ "upload_form.focus": "Crop",
+ "upload_form.undo": "Undo",
+ "upload_progress.label": "Uploading...",
+ "video.close": "Close video",
+ "video.exit_fullscreen": "Exit full screen",
+ "video.expand": "Expand video",
+ "video.fullscreen": "Full screen",
+ "video.hide": "Hide video",
+ "video.mute": "Mute sound",
+ "video.pause": "Pause",
+ "video.play": "Play",
+ "video.unmute": "Unmute sound"
+}
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index ad6f3b7123..d8e69fd3cb 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -138,6 +138,7 @@
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "to start a brand new toot",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index e51163971e..37587c14c6 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "por mencii la aŭtoron",
"keyboard_shortcuts.reply": "por respondi",
"keyboard_shortcuts.search": "por fokusigi la serĉilon",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "por komenci tute novan mesaĝon",
"keyboard_shortcuts.unfocus": "por malfokusigi la tekstujon aŭ la serĉilon",
"keyboard_shortcuts.up": "por iri supren en la listo",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 61ea0588de..41d7db9da7 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "para mencionar al autor",
"keyboard_shortcuts.reply": "para responder",
"keyboard_shortcuts.search": "para poner el foco en la búsqueda",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "para comenzar un nuevo toot",
"keyboard_shortcuts.unfocus": "para retirar el foco de la caja de redacción/búsqueda",
"keyboard_shortcuts.up": "para ir hacia arriba en la lista",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
new file mode 100644
index 0000000000..49cdf56301
--- /dev/null
+++ b/app/javascript/mastodon/locales/eu.json
@@ -0,0 +1,296 @@
+{
+ "account.block": "Blokeatu @{name}",
+ "account.block_domain": "{domain}(e)ko guztia ezkutatu",
+ "account.blocked": "Blokeatuta",
+ "account.direct": "@{name}(e)ri mezu zuzena bidali",
+ "account.disclaimer_full": "Baliteke beheko informazioak erabiltzailearen profilaren zati bat baino ez erakustea.",
+ "account.domain_blocked": "Ezkutatutako domeinua",
+ "account.edit_profile": "Profila aldatu",
+ "account.follow": "Jarraitu",
+ "account.followers": "Jarraitzaileak",
+ "account.follows": "Jarraitzen",
+ "account.follows_you": "Jarraitzen dizu",
+ "account.hide_reblogs": "@{name}(e)k sustatutakoak ezkutatu",
+ "account.media": "Media",
+ "account.mention": "@{name} aipatu",
+ "account.moved_to": "{name} hona lekualdatu da:",
+ "account.mute": "@{name} isilarazi",
+ "account.mute_notifications": "@{name}(e)ren jakinarazpenak isilarazi",
+ "account.muted": "Isilarazita",
+ "account.posts": "Toots",
+ "account.posts_with_replies": "Toots and replies",
+ "account.report": "@{name} salatu",
+ "account.requested": "Onarpenaren zain. Klikatu jarraitzeko eskaera ezeztatzeko",
+ "account.share": "@{name}(e)ren profila elkarbanatu",
+ "account.show_reblogs": "@{name}(e)k sustatutakoak erakutsi",
+ "account.unblock": "@{name} desblokeatu",
+ "account.unblock_domain": "Berriz erakutsi {domain}",
+ "account.unfollow": "Jarraitzeari utzi",
+ "account.unmute": "Unmute @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
+ "account.view_full_profile": "View full profile",
+ "alert.unexpected.message": "An unexpected error occurred.",
+ "alert.unexpected.title": "Oops!",
+ "boost_modal.combo": "You can press {combo} to skip this next time",
+ "bundle_column_error.body": "Something went wrong while loading this component.",
+ "bundle_column_error.retry": "Try again",
+ "bundle_column_error.title": "Network error",
+ "bundle_modal_error.close": "Close",
+ "bundle_modal_error.message": "Something went wrong while loading this component.",
+ "bundle_modal_error.retry": "Try again",
+ "column.blocks": "Blocked users",
+ "column.community": "Local timeline",
+ "column.direct": "Direct messages",
+ "column.domain_blocks": "Hidden domains",
+ "column.favourites": "Favourites",
+ "column.follow_requests": "Follow requests",
+ "column.home": "Home",
+ "column.lists": "Lists",
+ "column.mutes": "Muted users",
+ "column.notifications": "Notifications",
+ "column.pins": "Pinned toot",
+ "column.public": "Federated timeline",
+ "column_back_button.label": "Back",
+ "column_header.hide_settings": "Hide settings",
+ "column_header.moveLeft_settings": "Move column to the left",
+ "column_header.moveRight_settings": "Move column to the right",
+ "column_header.pin": "Pin",
+ "column_header.show_settings": "Show settings",
+ "column_header.unpin": "Unpin",
+ "column_subheading.navigation": "Navigation",
+ "column_subheading.settings": "Settings",
+ "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+ "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
+ "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
+ "compose_form.lock_disclaimer.lock": "locked",
+ "compose_form.placeholder": "What is on your mind?",
+ "compose_form.publish": "Toot",
+ "compose_form.publish_loud": "{publish}!",
+ "compose_form.sensitive.marked": "Media is marked as sensitive",
+ "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
+ "compose_form.spoiler.marked": "Text is hidden behind warning",
+ "compose_form.spoiler.unmarked": "Text is not hidden",
+ "compose_form.spoiler_placeholder": "Write your warning here",
+ "confirmation_modal.cancel": "Cancel",
+ "confirmations.block.confirm": "Block",
+ "confirmations.block.message": "Are you sure you want to block {name}?",
+ "confirmations.delete.confirm": "Delete",
+ "confirmations.delete.message": "Are you sure you want to delete this status?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+ "confirmations.domain_block.confirm": "Hide entire domain",
+ "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
+ "confirmations.mute.confirm": "Mute",
+ "confirmations.mute.message": "Are you sure you want to mute {name}?",
+ "confirmations.unfollow.confirm": "Unfollow",
+ "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+ "embed.instructions": "Embed this status on your website by copying the code below.",
+ "embed.preview": "Here is what it will look like:",
+ "emoji_button.activity": "Activity",
+ "emoji_button.custom": "Custom",
+ "emoji_button.flags": "Flags",
+ "emoji_button.food": "Food & Drink",
+ "emoji_button.label": "Insert emoji",
+ "emoji_button.nature": "Nature",
+ "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.objects": "Objects",
+ "emoji_button.people": "People",
+ "emoji_button.recent": "Frequently used",
+ "emoji_button.search": "Search...",
+ "emoji_button.search_results": "Search results",
+ "emoji_button.symbols": "Symbols",
+ "emoji_button.travel": "Travel & Places",
+ "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
+ "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+ "empty_column.hashtag": "There is nothing in this hashtag yet.",
+ "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
+ "empty_column.home.public_timeline": "the public timeline",
+ "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
+ "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
+ "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
+ "follow_request.authorize": "Authorize",
+ "follow_request.reject": "Reject",
+ "getting_started.appsshort": "Apps",
+ "getting_started.faq": "FAQ",
+ "getting_started.heading": "Getting started",
+ "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
+ "getting_started.userguide": "User Guide",
+ "home.column_settings.advanced": "Advanced",
+ "home.column_settings.basic": "Basic",
+ "home.column_settings.filter_regex": "Filter out by regular expressions",
+ "home.column_settings.show_reblogs": "Show boosts",
+ "home.column_settings.show_replies": "Show replies",
+ "home.settings": "Column settings",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
+ "lightbox.close": "Close",
+ "lightbox.next": "Next",
+ "lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
+ "loading_indicator.label": "Loading...",
+ "media_gallery.toggle_visible": "Toggle visibility",
+ "missing_indicator.label": "Not found",
+ "missing_indicator.sublabel": "This resource could not be found",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
+ "navigation_bar.blocks": "Blocked users",
+ "navigation_bar.community_timeline": "Local timeline",
+ "navigation_bar.direct": "Direct messages",
+ "navigation_bar.domain_blocks": "Hidden domains",
+ "navigation_bar.edit_profile": "Edit profile",
+ "navigation_bar.favourites": "Favourites",
+ "navigation_bar.follow_requests": "Follow requests",
+ "navigation_bar.info": "Extended information",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
+ "navigation_bar.logout": "Logout",
+ "navigation_bar.mutes": "Muted users",
+ "navigation_bar.pins": "Pinned toots",
+ "navigation_bar.preferences": "Preferences",
+ "navigation_bar.public_timeline": "Federated timeline",
+ "notification.favourite": "{name} favourited your status",
+ "notification.follow": "{name} followed you",
+ "notification.mention": "{name} mentioned you",
+ "notification.reblog": "{name} boosted your status",
+ "notifications.clear": "Clear notifications",
+ "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
+ "notifications.column_settings.alert": "Desktop notifications",
+ "notifications.column_settings.favourite": "Favourites:",
+ "notifications.column_settings.follow": "New followers:",
+ "notifications.column_settings.mention": "Mentions:",
+ "notifications.column_settings.push": "Push notifications",
+ "notifications.column_settings.push_meta": "This device",
+ "notifications.column_settings.reblog": "Boosts:",
+ "notifications.column_settings.show": "Show in column",
+ "notifications.column_settings.sound": "Play sound",
+ "onboarding.done": "Done",
+ "onboarding.next": "Next",
+ "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
+ "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
+ "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
+ "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
+ "onboarding.page_one.full_handle": "Your full handle",
+ "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
+ "onboarding.page_one.welcome": "Welcome to Mastodon!",
+ "onboarding.page_six.admin": "Your instance's admin is {admin}.",
+ "onboarding.page_six.almost_done": "Almost done...",
+ "onboarding.page_six.appetoot": "Bon Appetoot!",
+ "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
+ "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
+ "onboarding.page_six.guidelines": "community guidelines",
+ "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
+ "onboarding.page_six.various_app": "mobile apps",
+ "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
+ "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
+ "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
+ "onboarding.skip": "Skip",
+ "privacy.change": "Adjust status privacy",
+ "privacy.direct.long": "Post to mentioned users only",
+ "privacy.direct.short": "Direct",
+ "privacy.private.long": "Post to followers only",
+ "privacy.private.short": "Followers-only",
+ "privacy.public.long": "Post to public timelines",
+ "privacy.public.short": "Public",
+ "privacy.unlisted.long": "Do not show in public timelines",
+ "privacy.unlisted.short": "Unlisted",
+ "regeneration_indicator.label": "Loading…",
+ "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+ "relative_time.days": "{number}d",
+ "relative_time.hours": "{number}h",
+ "relative_time.just_now": "now",
+ "relative_time.minutes": "{number}m",
+ "relative_time.seconds": "{number}s",
+ "reply_indicator.cancel": "Cancel",
+ "report.forward": "Forward to {target}",
+ "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
+ "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+ "report.placeholder": "Additional comments",
+ "report.submit": "Submit",
+ "report.target": "Report {target}",
+ "search.placeholder": "Search",
+ "search_popout.search_format": "Advanced search format",
+ "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+ "search_popout.tips.hashtag": "hashtag",
+ "search_popout.tips.status": "status",
+ "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+ "search_popout.tips.user": "user",
+ "search_results.accounts": "People",
+ "search_results.hashtags": "Hashtags",
+ "search_results.statuses": "Toots",
+ "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+ "standalone.public_title": "A look inside...",
+ "status.block": "Block @{name}",
+ "status.cancel_reblog_private": "Unboost",
+ "status.cannot_reblog": "This post cannot be boosted",
+ "status.delete": "Delete",
+ "status.direct": "Direct message @{name}",
+ "status.embed": "Embed",
+ "status.favourite": "Favourite",
+ "status.load_more": "Load more",
+ "status.media_hidden": "Media hidden",
+ "status.mention": "Mention @{name}",
+ "status.more": "More",
+ "status.mute": "Mute @{name}",
+ "status.mute_conversation": "Mute conversation",
+ "status.open": "Expand this status",
+ "status.pin": "Pin on profile",
+ "status.pinned": "Pinned toot",
+ "status.reblog": "Boost",
+ "status.reblog_private": "Boost to original audience",
+ "status.reblogged_by": "{name} boosted",
+ "status.reply": "Reply",
+ "status.replyAll": "Reply to thread",
+ "status.report": "Report @{name}",
+ "status.sensitive_toggle": "Click to view",
+ "status.sensitive_warning": "Sensitive content",
+ "status.share": "Share",
+ "status.show_less": "Show less",
+ "status.show_less_all": "Show less for all",
+ "status.show_more": "Show more",
+ "status.show_more_all": "Show more for all",
+ "status.unmute_conversation": "Unmute conversation",
+ "status.unpin": "Unpin from profile",
+ "tabs_bar.federated_timeline": "Federated",
+ "tabs_bar.home": "Home",
+ "tabs_bar.local_timeline": "Local",
+ "tabs_bar.notifications": "Notifications",
+ "tabs_bar.search": "Search",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+ "upload_area.title": "Drag & drop to upload",
+ "upload_button.label": "Add media",
+ "upload_form.description": "Describe for the visually impaired",
+ "upload_form.focus": "Crop",
+ "upload_form.undo": "Undo",
+ "upload_progress.label": "Uploading...",
+ "video.close": "Close video",
+ "video.exit_fullscreen": "Exit full screen",
+ "video.expand": "Expand video",
+ "video.fullscreen": "Full screen",
+ "video.hide": "Hide video",
+ "video.mute": "Mute sound",
+ "video.pause": "Pause",
+ "video.play": "Play",
+ "video.unmute": "Unmute sound"
+}
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index cfe93007d1..99aba00c35 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "برای نامبردن از نویسنده",
"keyboard_shortcuts.reply": "برای پاسخدادن",
"keyboard_shortcuts.search": "برای فعالکردن جستجو",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "برای آغاز یک بوق تازه",
"keyboard_shortcuts.unfocus": "برای برداشتن توجه از نوشتن/جستجو",
"keyboard_shortcuts.up": "برای بالا رفتن در فهرست",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 1677c3c6cc..07d4d9aa5c 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "mainitse julkaisija",
"keyboard_shortcuts.reply": "vastaa",
"keyboard_shortcuts.search": "siirry hakukenttään",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "ala kirjoittaa uutta tuuttausta",
"keyboard_shortcuts.unfocus": "siirry pois tekstikentästä tai hakukentästä",
"keyboard_shortcuts.up": "siirry listassa ylöspäin",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 98c1c43d28..a4af97ddab 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -2,7 +2,7 @@
"account.block": "Bloquer @{name}",
"account.block_domain": "Tout masquer venant de {domain}",
"account.blocked": "Bloqué",
- "account.direct": "Direct message @{name}",
+ "account.direct": "Message direct @{name}",
"account.disclaimer_full": "Les données ci-dessous peuvent ne pas refléter ce profil dans sa totalité.",
"account.domain_blocked": "Domaine caché",
"account.edit_profile": "Modifier le profil",
@@ -18,7 +18,7 @@
"account.mute_notifications": "Ignorer les notifications de @{name}",
"account.muted": "Silencé",
"account.posts": "Pouets",
- "account.posts_with_replies": "Pouets avec réponses",
+ "account.posts_with_replies": "Pouets et réponses",
"account.report": "Signaler",
"account.requested": "En attente d'approbation. Cliquez pour annuler la requête",
"account.share": "Partager le profil de @{name}",
@@ -29,8 +29,8 @@
"account.unmute": "Ne plus masquer",
"account.unmute_notifications": "Réactiver les notifications de @{name}",
"account.view_full_profile": "Afficher le profil complet",
- "alert.unexpected.message": "An unexpected error occurred.",
- "alert.unexpected.title": "Oops!",
+ "alert.unexpected.message": "Une erreur non-attendue s'est produite.",
+ "alert.unexpected.title": "Oups !",
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
"bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.",
"bundle_column_error.retry": "Réessayer",
@@ -40,8 +40,8 @@
"bundle_modal_error.retry": "Réessayer",
"column.blocks": "Comptes bloqués",
"column.community": "Fil public local",
- "column.direct": "Direct messages",
- "column.domain_blocks": "Hidden domains",
+ "column.direct": "Messages directs",
+ "column.domain_blocks": "Domaines cachés",
"column.favourites": "Favoris",
"column.follow_requests": "Demandes de suivi",
"column.home": "Accueil",
@@ -59,7 +59,7 @@
"column_header.unpin": "Retirer",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Paramètres",
- "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+ "compose_form.direct_message_warning": "Ce pouet sera uniquement visible à tous les utilisateurs mentionnés.",
"compose_form.hashtag_warning": "Ce pouet ne sera pas listé dans les recherches par hashtag car sa visibilité est réglée sur \"non-listé\". Seuls les pouets avec une visibilité \"publique\" peuvent être recherchés par hashtag.",
"compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.",
"compose_form.lock_disclaimer.lock": "verrouillé",
@@ -101,7 +101,7 @@
"emoji_button.symbols": "Symboles",
"emoji_button.travel": "Lieux & Voyages",
"empty_column.community": "Le fil public local est vide. Écrivez donc quelque chose pour le remplir !",
- "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+ "empty_column.direct": "Vous n'avez pas encore de messages directs. Lorsque vous en enverrez ou recevrez un, il s'affichera ici.",
"empty_column.hashtag": "Il n’y a encore aucun contenu associé à ce hashtag.",
"empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres personnes.",
"empty_column.home.public_timeline": "le fil public",
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "pour mentionner l'auteur",
"keyboard_shortcuts.reply": "pour répondre",
"keyboard_shortcuts.search": "pour cibler la recherche",
+ "keyboard_shortcuts.toggle_hidden": "pour afficher/cacher un texte derrière CW",
"keyboard_shortcuts.toot": "pour démarrer un tout nouveau pouet",
"keyboard_shortcuts.unfocus": "pour recentrer composer textarea/search",
"keyboard_shortcuts.up": "pour remonter dans la liste",
@@ -156,8 +157,8 @@
"mute_modal.hide_notifications": "Masquer les notifications de cette personne ?",
"navigation_bar.blocks": "Comptes bloqués",
"navigation_bar.community_timeline": "Fil public local",
- "navigation_bar.direct": "Direct messages",
- "navigation_bar.domain_blocks": "Hidden domains",
+ "navigation_bar.direct": "Messages directs",
+ "navigation_bar.domain_blocks": "Domaines cachés",
"navigation_bar.edit_profile": "Modifier le profil",
"navigation_bar.favourites": "Favoris",
"navigation_bar.follow_requests": "Demandes de suivi",
@@ -241,10 +242,10 @@
"search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
"standalone.public_title": "Un aperçu …",
"status.block": "Block @{name}",
- "status.cancel_reblog_private": "Unboost",
+ "status.cancel_reblog_private": "Dé-booster",
"status.cannot_reblog": "Cette publication ne peut être boostée",
"status.delete": "Effacer",
- "status.direct": "Direct message @{name}",
+ "status.direct": "Message direct @{name}",
"status.embed": "Intégrer",
"status.favourite": "Ajouter aux favoris",
"status.load_more": "Charger plus",
@@ -257,7 +258,7 @@
"status.pin": "Épingler sur le profil",
"status.pinned": "Pouet épinglé",
"status.reblog": "Partager",
- "status.reblog_private": "Boost to original audience",
+ "status.reblog_private": "Booster vers l'audience originale",
"status.reblogged_by": "{name} a partagé :",
"status.reply": "Répondre",
"status.replyAll": "Répondre au fil",
@@ -275,7 +276,7 @@
"tabs_bar.home": "Accueil",
"tabs_bar.local_timeline": "Fil public local",
"tabs_bar.notifications": "Notifications",
- "tabs_bar.search": "Search",
+ "tabs_bar.search": "Chercher",
"ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.",
"upload_area.title": "Glissez et déposez pour envoyer",
"upload_button.label": "Joindre un média",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index fca42374d3..652ca31d15 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -18,7 +18,7 @@
"account.mute_notifications": "Acalar as notificacións de @{name}",
"account.muted": "Muted",
"account.posts": "Toots",
- "account.posts_with_replies": "Toots with replies",
+ "account.posts_with_replies": "Toots e respostas",
"account.report": "Informar sobre @{name}",
"account.requested": "Agardando aceptación. Pulse para cancelar a solicitude de seguimento",
"account.share": "Compartir o perfil de @{name}",
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "para mencionar o autor",
"keyboard_shortcuts.reply": "para responder",
"keyboard_shortcuts.search": "para centrar a busca",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "escribir un toot novo",
"keyboard_shortcuts.unfocus": "quitar o foco do área de escritura/busca",
"keyboard_shortcuts.up": "ir hacia arriba na lista",
@@ -242,7 +243,7 @@
"standalone.public_title": "Ollada dentro...",
"status.block": "Block @{name}",
"status.cancel_reblog_private": "Unboost",
- "status.cannot_reblog": "Esta mensaxe non pode ser promocionada",
+ "status.cannot_reblog": "Esta mensaxe non pode ser promovida",
"status.delete": "Eliminar",
"status.direct": "Direct message @{name}",
"status.embed": "Incrustar",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index e3e87f1d03..0ffbb14f31 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "לאזכר את המחבר(ת)",
"keyboard_shortcuts.reply": "לענות",
"keyboard_shortcuts.search": "להתמקד בחלון החיפוש",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "להתחיל חיצרוץ חדש",
"keyboard_shortcuts.unfocus": "לצאת מתיבת חיבור/חיפוש",
"keyboard_shortcuts.up": "לנוע במעלה הרשימה",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index b41c98394d..c41cc3ea10 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "to start a brand new toot",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 956accc677..a0c1861845 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "szerző megjelenítése",
"keyboard_shortcuts.reply": "válaszolás",
"keyboard_shortcuts.search": "kereső kiemelése",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "új tülk megkezdése",
"keyboard_shortcuts.unfocus": "tülk szerkesztés/keresés fókuszpontból való kivétele",
"keyboard_shortcuts.up": "fennebb helyezés a listában",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 33e079201a..a0442bad47 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "հեղինակին նշելու համար",
"keyboard_shortcuts.reply": "պատասխանելու համար",
"keyboard_shortcuts.search": "որոնման դաշտին սեւեռվելու համար",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "թարմ թութ սկսելու համար",
"keyboard_shortcuts.unfocus": "տեքստի/որոնման տիրույթից ապասեւեռվելու համար",
"keyboard_shortcuts.up": "ցանկով վերեւ շարժվելու համար",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 412ffd3a03..2fd9225440 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.search": "untuk fokus mencari",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "to start a brand new toot",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 9730bf934d..ed45ee11ec 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "to start a brand new toot",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 5146d7ca21..a7ca620156 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -1,7 +1,7 @@
{
"account.block": "Blocca @{name}",
"account.block_domain": "Hide everything from {domain}",
- "account.blocked": "Blocked",
+ "account.blocked": "Bloccato",
"account.direct": "Direct Message @{name}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.domain_blocked": "Domain hidden",
@@ -17,8 +17,8 @@
"account.mute": "Silenzia @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
- "account.posts": "Posts",
- "account.posts_with_replies": "Toots with replies",
+ "account.posts": "Toot",
+ "account.posts_with_replies": "Toot con risposte",
"account.report": "Segnala @{name}",
"account.requested": "In attesa di approvazione",
"account.share": "Share @{name}'s profile",
@@ -105,7 +105,7 @@
"empty_column.hashtag": "Non c'è ancora nessun post con questo hashtag.",
"empty_column.home": "Non stai ancora seguendo nessuno. Visita {public} o usa la ricerca per incontrare nuove persone.",
"empty_column.home.public_timeline": "la timeline pubblica",
- "empty_column.list": "There is nothing in this list yet.",
+ "empty_column.list": "Non c'è niente in questo elenco ancora. Quando i membri di questo elenco postano nuovi stati, questi appariranno qui.",
"empty_column.notifications": "Non hai ancora nessuna notifica. Interagisci con altri per iniziare conversazioni.",
"empty_column.public": "Qui non c'è nulla! Scrivi qualcosa pubblicamente, o aggiungi utenti da altri server per riempire questo spazio.",
"follow_request.authorize": "Autorizza",
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "to start a brand new toot",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index abd18742aa..dbb4562de5 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -29,8 +29,8 @@
"account.unmute": "@{name}さんのミュートを解除",
"account.unmute_notifications": "@{name}さんからの通知を受け取るようにする",
"account.view_full_profile": "全ての情報を見る",
- "alert.unexpected.message": "不明なエラーが発生しました",
- "alert.unexpected.title": "エラー",
+ "alert.unexpected.message": "不明なエラーが発生しました。",
+ "alert.unexpected.title": "エラー!",
"boost_modal.combo": "次からは{combo}を押せばスキップできます",
"bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
"bundle_column_error.retry": "再試行",
@@ -104,7 +104,7 @@
"emoji_button.symbols": "記号",
"emoji_button.travel": "旅行と場所",
"empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
- "empty_column.direct": "あなたはまだダイレクトメッセージを受け取っていません。あなたが送ったり受け取ったりすると、ここに表示されます。",
+ "empty_column.direct": "ダイレクトメッセージはまだありません。ダイレクトメッセージをやりとりすると、ここに表示されます。",
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
"empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
"empty_column.home.public_timeline": "連合タイムライン",
@@ -138,6 +138,7 @@
"keyboard_shortcuts.mention": "メンション",
"keyboard_shortcuts.reply": "返信",
"keyboard_shortcuts.search": "検索欄に移動",
+ "keyboard_shortcuts.toggle_hidden": "CWで隠れた文を見る/隠す",
"keyboard_shortcuts.toot": "新規トゥート",
"keyboard_shortcuts.unfocus": "トゥート入力欄・検索欄から離れる",
"keyboard_shortcuts.up": "カラム内一つ上に移動",
@@ -159,7 +160,7 @@
"mute_modal.hide_notifications": "このユーザーからの通知を隠しますか?",
"navigation_bar.blocks": "ブロックしたユーザー",
"navigation_bar.community_timeline": "ローカルタイムライン",
- "navigation_bar.direct": "Direct messages",
+ "navigation_bar.direct": "ダイレクトメッセージ",
"navigation_bar.domain_blocks": "非表示にしたドメイン",
"navigation_bar.edit_profile": "プロフィールを編集",
"navigation_bar.favourites": "お気に入り",
@@ -245,7 +246,7 @@
"search_results.total": "{count, number}件の結果",
"standalone.public_title": "今こんな話をしています...",
"status.block": "@{name}さんをブロック",
- "status.cancel_reblog_private": "Unboost",
+ "status.cancel_reblog_private": "ブースト解除",
"status.cannot_reblog": "この投稿はブーストできません",
"status.delete": "削除",
"status.direct": "@{name}さんにダイレクトメッセージ",
@@ -261,7 +262,7 @@
"status.pin": "プロフィールに固定表示",
"status.pinned": "固定されたトゥート",
"status.reblog": "ブースト",
- "status.reblog_private": "Boost to original audience",
+ "status.reblog_private": "ブースト",
"status.reblogged_by": "{name}さんがブースト",
"status.reply": "返信",
"status.replyAll": "全員に返信",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 92367dc95c..2a27346736 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -2,7 +2,7 @@
"account.block": "@{name}을 차단",
"account.block_domain": "{domain} 전체를 숨김",
"account.blocked": "차단 됨",
- "account.direct": "Direct message @{name}",
+ "account.direct": "@{name}으로부터의 다이렉트 메시지",
"account.disclaimer_full": "여기 있는 정보는 유저의 프로파일을 정확히 반영하지 못 할 수도 있습니다.",
"account.domain_blocked": "도메인 숨겨짐",
"account.edit_profile": "프로필 편집",
@@ -12,7 +12,7 @@
"account.follows_you": "날 팔로우합니다",
"account.hide_reblogs": "@{name}의 부스트를 숨기기",
"account.media": "미디어",
- "account.mention": "답장",
+ "account.mention": "@{name}에게 글쓰기",
"account.moved_to": "{name}는 계정을 이동했습니다:",
"account.mute": "@{name} 뮤트",
"account.mute_notifications": "@{name}의 알림을 뮤트",
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "멘션",
"keyboard_shortcuts.reply": "답장",
"keyboard_shortcuts.search": "검색창에 포커스",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "새 툿 작성",
"keyboard_shortcuts.unfocus": "작성창에서 포커스 해제",
"keyboard_shortcuts.up": "리스트에서 위로 이동",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index c18ddbd01e..adc1d19a7c 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -18,7 +18,7 @@
"account.mute_notifications": "Negeer meldingen van @{name}",
"account.muted": "Genegeerd",
"account.posts": "Toots",
- "account.posts_with_replies": "Toots met reacties",
+ "account.posts_with_replies": "Toots en reacties",
"account.report": "Rapporteer @{name}",
"account.requested": "Wacht op goedkeuring. Klik om het volgverzoek te annuleren",
"account.share": "Profiel van @{name} delen",
@@ -29,8 +29,8 @@
"account.unmute": "@{name} niet meer negeren",
"account.unmute_notifications": "@{name} meldingen niet meer negeren",
"account.view_full_profile": "Volledig profiel tonen",
- "alert.unexpected.message": "An unexpected error occurred.",
- "alert.unexpected.title": "Oops!",
+ "alert.unexpected.message": "Er deed zich een onverwachte fout voor",
+ "alert.unexpected.title": "Oeps!",
"boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
"bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.",
"bundle_column_error.retry": "Opnieuw proberen",
@@ -40,8 +40,8 @@
"bundle_modal_error.retry": "Opnieuw proberen",
"column.blocks": "Geblokkeerde gebruikers",
"column.community": "Lokale tijdlijn",
- "column.direct": "Direct messages",
- "column.domain_blocks": "Hidden domains",
+ "column.direct": "Directe berichten",
+ "column.domain_blocks": "Verborgen domeinen",
"column.favourites": "Favorieten",
"column.follow_requests": "Volgverzoeken",
"column.home": "Start",
@@ -59,7 +59,7 @@
"column_header.unpin": "Losmaken",
"column_subheading.navigation": "Navigatie",
"column_subheading.settings": "Instellingen",
- "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+ "compose_form.direct_message_warning": "Deze toot zal alleen zichtbaar zijn voor alle vermelde gebruikers.",
"compose_form.hashtag_warning": "Deze toot valt niet onder een hashtag te bekijken, omdat deze niet op openbare tijdlijnen wordt getoond. Alleen openbare toots kunnen via hashtags gevonden worden.",
"compose_form.lock_disclaimer": "Jouw account is niet {locked}. Iedereen kan jou volgen en toots zien die je alleen aan volgers hebt gericht.",
"compose_form.lock_disclaimer.lock": "besloten",
@@ -101,7 +101,7 @@
"emoji_button.symbols": "Symbolen",
"emoji_button.travel": "Reizen en plekken",
"empty_column.community": "De lokale tijdlijn is nog leeg. Toot iets in het openbaar om de bal aan het rollen te krijgen!",
- "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+ "empty_column.direct": "Je hebt nog geen directe berichten. Wanneer je er een verzend of ontvangt, zijn deze hier te zien.",
"empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.",
"empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.",
"empty_column.home.public_timeline": "de globale tijdlijn",
@@ -127,7 +127,7 @@
"keyboard_shortcuts.compose": "om het tekstvak voor toots te focussen",
"keyboard_shortcuts.description": "Omschrijving",
"keyboard_shortcuts.down": "om naar beneden door de lijst te bewegen",
- "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.enter": "om toot volledig te tonen",
"keyboard_shortcuts.favourite": "om als favoriet te markeren",
"keyboard_shortcuts.heading": "Sneltoetsen",
"keyboard_shortcuts.hotkey": "Sneltoets",
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "om de auteur te vermelden",
"keyboard_shortcuts.reply": "om te reageren",
"keyboard_shortcuts.search": "om het zoekvak te focussen",
+ "keyboard_shortcuts.toggle_hidden": "om tekst achter een waarschuwing (CW) te tonen/verbergen",
"keyboard_shortcuts.toot": "om een nieuwe toot te starten",
"keyboard_shortcuts.unfocus": "om het tekst- en zoekvak te ontfocussen",
"keyboard_shortcuts.up": "om omhoog te bewegen in de lijst",
@@ -156,8 +157,8 @@
"mute_modal.hide_notifications": "Verberg meldingen van deze persoon?",
"navigation_bar.blocks": "Geblokkeerde gebruikers",
"navigation_bar.community_timeline": "Lokale tijdlijn",
- "navigation_bar.direct": "Direct messages",
- "navigation_bar.domain_blocks": "Hidden domains",
+ "navigation_bar.direct": "Directe berichten",
+ "navigation_bar.domain_blocks": "Verborgen domeinen",
"navigation_bar.edit_profile": "Profiel bewerken",
"navigation_bar.favourites": "Favorieten",
"navigation_bar.follow_requests": "Volgverzoeken",
@@ -241,10 +242,10 @@
"search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}",
"standalone.public_title": "Een kijkje binnenin...",
"status.block": "Blokkeer @{name}",
- "status.cancel_reblog_private": "Unboost",
+ "status.cancel_reblog_private": "Niet meer boosten",
"status.cannot_reblog": "Deze toot kan niet geboost worden",
"status.delete": "Verwijderen",
- "status.direct": "Direct message @{name}",
+ "status.direct": "Directe toot @{name}",
"status.embed": "Embed",
"status.favourite": "Favoriet",
"status.load_more": "Meer laden",
@@ -257,7 +258,7 @@
"status.pin": "Aan profielpagina vastmaken",
"status.pinned": "Vastgemaakte toot",
"status.reblog": "Boost",
- "status.reblog_private": "Boost to original audience",
+ "status.reblog_private": "Boost naar oorspronkelijke ontvangers",
"status.reblogged_by": "{name} boostte",
"status.reply": "Reageren",
"status.replyAll": "Reageer op iedereen",
@@ -275,7 +276,7 @@
"tabs_bar.home": "Start",
"tabs_bar.local_timeline": "Lokaal",
"tabs_bar.notifications": "Meldingen",
- "tabs_bar.search": "Search",
+ "tabs_bar.search": "Zoeken",
"ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.",
"upload_area.title": "Hierin slepen om te uploaden",
"upload_button.label": "Media toevoegen",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 282a72acba..0ee6d07229 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "å nevne forfatter",
"keyboard_shortcuts.reply": "for å svare",
"keyboard_shortcuts.search": "å fokusere søk",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "å starte en helt ny tut",
"keyboard_shortcuts.unfocus": "å ufokusere komponerings-/søkefeltet",
"keyboard_shortcuts.up": "å flytte opp i listen",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 7170aefb8c..d4836e9fe0 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -18,7 +18,7 @@
"account.mute_notifications": "Rescondre las notificacions de @{name}",
"account.muted": "Mes en silenci",
"account.posts": "Tuts",
- "account.posts_with_replies": "Tuts amb responsas",
+ "account.posts_with_replies": "Tuts e responsas",
"account.report": "Senhalar @{name}",
"account.requested": "Invitacion mandada. Clicatz per anullar",
"account.share": "Partejar lo perfil a @{name}",
@@ -29,8 +29,8 @@
"account.unmute": "Quitar de rescondre @{name}",
"account.unmute_notifications": "Mostrar las notificacions de @{name}",
"account.view_full_profile": "Veire lo perfil complèt",
- "alert.unexpected.message": "An unexpected error occurred.",
- "alert.unexpected.title": "Oops!",
+ "alert.unexpected.message": "Una error s’es producha.",
+ "alert.unexpected.title": "Ops !",
"boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
"bundle_column_error.body": "Quicòm a fach mèuca pendent lo cargament d’aqueste compausant.",
"bundle_column_error.retry": "Tornar ensajar",
@@ -40,8 +40,8 @@
"bundle_modal_error.retry": "Tornar ensajar",
"column.blocks": "Personas blocadas",
"column.community": "Flux public local",
- "column.direct": "Direct messages",
- "column.domain_blocks": "Hidden domains",
+ "column.direct": "Messatges dirèctes",
+ "column.domain_blocks": "Domenis blocats",
"column.favourites": "Favorits",
"column.follow_requests": "Demandas d’abonament",
"column.home": "Acuèlh",
@@ -59,7 +59,7 @@
"column_header.unpin": "Despenjar",
"column_subheading.navigation": "Navigacion",
"column_subheading.settings": "Paramètres",
- "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+ "compose_form.direct_message_warning": "Aqueste tut serà pas que visibile pel monde mencionat.",
"compose_form.hashtag_warning": "Aqueste tut serà pas ligat a cap etiqueta estant qu’es pas listat. Òm pas cercar que los tuts publics per etiqueta.",
"compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.",
"compose_form.lock_disclaimer.lock": "clavat",
@@ -73,13 +73,13 @@
"compose_form.spoiler_placeholder": "Escrivètz l’avertiment aquí",
"confirmation_modal.cancel": "Anullar",
"confirmations.block.confirm": "Blocar",
- "confirmations.block.message": "Sètz segur de voler blocar {name} ?",
+ "confirmations.block.message": "Volètz vertadièrament blocar {name} ?",
"confirmations.delete.confirm": "Escafar",
- "confirmations.delete.message": "Sètz segur de voler escafar l’estatut ?",
+ "confirmations.delete.message": "Volètz vertadièrament escafar l’estatut ?",
"confirmations.delete_list.confirm": "Suprimir",
- "confirmations.delete_list.message": "Sètz segur de voler suprimir aquesta lista per totjorn ?",
+ "confirmations.delete_list.message": "Volètz vertadièrament suprimir aquesta lista per totjorn ?",
"confirmations.domain_block.confirm": "Amagar tot lo domeni",
- "confirmations.domain_block.message": "Sètz segur segur de voler blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
+ "confirmations.domain_block.message": "Volètz vertadièrament blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
"confirmations.mute.confirm": "Rescondre",
"confirmations.mute.message": "Sètz segur de voler rescondre {name} ?",
"confirmations.unfollow.confirm": "Quitar de sègre",
@@ -101,7 +101,7 @@
"emoji_button.symbols": "Simbòls",
"emoji_button.travel": "Viatges & lòcs",
"empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !",
- "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+ "empty_column.direct": "Avètz pas encara de messatges. Quand ne mandatz un o que ne recebètz un, serà mostrat aquí.",
"empty_column.hashtag": "I a pas encara de contengut ligat a aquesta etiqueta.",
"empty_column.home": "Vòstre flux d’acuèlh es void. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
"empty_column.home.public_timeline": "lo flux public",
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "mencionar l’autor",
"keyboard_shortcuts.reply": "respondre",
"keyboard_shortcuts.search": "anar a la recèrca",
+ "keyboard_shortcuts.toggle_hidden": "mostrar/amagar lo tèxte dels avertiments",
"keyboard_shortcuts.toot": "començar un estatut tot novèl",
"keyboard_shortcuts.unfocus": "quitar lo camp tèxte/de recèrca",
"keyboard_shortcuts.up": "far montar dins la lista",
@@ -156,7 +157,7 @@
"mute_modal.hide_notifications": "Rescondre las notificacions d’aquesta persona ?",
"navigation_bar.blocks": "Personas blocadas",
"navigation_bar.community_timeline": "Flux public local",
- "navigation_bar.direct": "Direct messages",
+ "navigation_bar.direct": "Messatges dirèctes",
"navigation_bar.domain_blocks": "Hidden domains",
"navigation_bar.edit_profile": "Modificar lo perfil",
"navigation_bar.favourites": "Favorits",
@@ -216,7 +217,7 @@
"privacy.unlisted.short": "Pas-listat",
"regeneration_indicator.label": "Cargament…",
"regeneration_indicator.sublabel": "Sèm a preparar vòstre flux d’acuèlh !",
- "relative_time.days": "fa {number} d",
+ "relative_time.days": "fa {number}d",
"relative_time.hours": "fa {number}h",
"relative_time.just_now": "ara",
"relative_time.minutes": "fa {number} min",
@@ -235,16 +236,16 @@
"search_popout.tips.status": "estatut",
"search_popout.tips.text": "Lo tèxt brut tòrna escais, noms d’utilizaire e etiquetas correspondents",
"search_popout.tips.user": "utilizaire",
- "search_results.accounts": "Monde",
+ "search_results.accounts": "Gents",
"search_results.hashtags": "Etiquetas",
"search_results.statuses": "Tuts",
"search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
"standalone.public_title": "Una ulhada dedins…",
"status.block": "Blocar @{name}",
- "status.cancel_reblog_private": "Unboost",
+ "status.cancel_reblog_private": "Quitar de partejar",
"status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat",
"status.delete": "Escafar",
- "status.direct": "Direct message @{name}",
+ "status.direct": "Messatge per @{name}",
"status.embed": "Embarcar",
"status.favourite": "Apondre als favorits",
"status.load_more": "Cargar mai",
@@ -257,7 +258,7 @@
"status.pin": "Penjar al perfil",
"status.pinned": "Tut penjat",
"status.reblog": "Partejar",
- "status.reblog_private": "Boost to original audience",
+ "status.reblog_private": "Partejar al l’audiéncia d’origina",
"status.reblogged_by": "{name} a partejat",
"status.reply": "Respondre",
"status.replyAll": "Respondre a la conversacion",
@@ -275,7 +276,7 @@
"tabs_bar.home": "Acuèlh",
"tabs_bar.local_timeline": "Flux public local",
"tabs_bar.notifications": "Notificacions",
- "tabs_bar.search": "Search",
+ "tabs_bar.search": "Recèrcas",
"ui.beforeunload": "Vòstre brolhon serà perdut se quitatz Mastodon.",
"upload_area.title": "Lisatz e depausatz per mandar",
"upload_button.label": "Ajustar un mèdia",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 08aea797d4..6d6db7c820 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -106,8 +106,8 @@
"empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
"empty_column.direct": "Nie masz żadnych wiadomości bezpośrednich. Jeżeli wyślesz lub otrzymasz jakąś, będzie tu widoczna.",
"empty_column.hashtag": "Nie ma wpisów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
- "empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
- "empty_column.home.public_timeline": "publiczna oś czasu",
+ "empty_column.home": "Nie śledzisz nikogo. Odwiedź globalną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
+ "empty_column.home.public_timeline": "globalna oś czasu",
"empty_column.list": "Nie ma nic na tej liście. Kiedy członkowie listy dodadzą nowe wpisy, pojawia się one tutaj.",
"empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.",
"empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych instancji, aby to wyświetlić",
@@ -173,7 +173,7 @@
"navigation_bar.mutes": "Wyciszeni użytkownicy",
"navigation_bar.pins": "Przypięte wpisy",
"navigation_bar.preferences": "Preferencje",
- "navigation_bar.public_timeline": "Oś czasu federacji",
+ "navigation_bar.public_timeline": "Globalna oś czasu",
"notification.favourite": "{name} dodał Twój wpis do ulubionych",
"notification.follow": "{name} zaczął Cię śledzić",
"notification.mention": "{name} wspomniał o tobie",
@@ -191,7 +191,7 @@
"notifications.column_settings.sound": "Odtwarzaj dźwięk",
"onboarding.done": "Gotowe",
"onboarding.next": "Dalej",
- "onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Federalna oś czasu wyświetla publiczne wpisy śledzonych przez członków {domain}. Są to publiczne osie czasu – najlepszy sposób na poznanie nowych osób.",
+ "onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Globalna oś czasu wyświetla publiczne wpisy śledzonych przez członków {domain}. Są to publiczne osie czasu – najlepszy sposób na poznanie nowych osób.",
"onboarding.page_four.home": "Główna oś czasu wyświetla publiczne wpisy.",
"onboarding.page_four.notifications": "Kolumna powiadomień wyświetla, gdy ktoś dokonuje interakcji z tobą.",
"onboarding.page_one.federation": "Mastodon jest siecią niezależnych serwerów połączonych w jeden portal społecznościowy. Nazywamy te serwery instancjami.",
@@ -251,7 +251,7 @@
"status.delete": "Usuń",
"status.direct": "Wyślij wiadomość bezpośrednią do @{name}",
"status.embed": "Osadź",
- "status.favourite": "Ulubione",
+ "status.favourite": "Dodaj do ulubionych",
"status.load_more": "Załaduj więcej",
"status.media_hidden": "Zawartość multimedialna ukryta",
"status.mention": "Wspomnij o @{name}",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index c604476c7a..7f8690f913 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -29,7 +29,7 @@
"account.unmute": "Não silenciar @{name}",
"account.unmute_notifications": "Retirar silêncio das notificações vindas de @{name}",
"account.view_full_profile": "Ver perfil completo",
- "alert.unexpected.message": "An unexpected error occurred.",
+ "alert.unexpected.message": "Um erro inesperado ocorreu.",
"alert.unexpected.title": "Oops!",
"boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez",
"bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
@@ -40,8 +40,8 @@
"bundle_modal_error.retry": "Tente novamente",
"column.blocks": "Usuários bloqueados",
"column.community": "Local",
- "column.direct": "Direct messages",
- "column.domain_blocks": "Hidden domains",
+ "column.direct": "Mensagens diretas",
+ "column.domain_blocks": "Domínios escondidos",
"column.favourites": "Favoritos",
"column.follow_requests": "Seguidores pendentes",
"column.home": "Página inicial",
@@ -59,7 +59,7 @@
"column_header.unpin": "Desafixar",
"column_subheading.navigation": "Navegação",
"column_subheading.settings": "Configurações",
- "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+ "compose_form.direct_message_warning": "Este toot só será visível a todos os usuários mencionados.",
"compose_form.hashtag_warning": "Esse toot não será listado em nenhuma hashtag por ser não listado. Somente toots públicos podem ser pesquisados por hashtag.",
"compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar postagens direcionadas a apenas seguidores.",
"compose_form.lock_disclaimer.lock": "trancada",
@@ -101,7 +101,7 @@
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viagens & Lugares",
"empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!",
- "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+ "empty_column.direct": "Você não tem nenhuma mensagem direta ainda. Quando você enviar ou receber uma, as mensagens aparecerão por aqui.",
"empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag.",
"empty_column.home": "Você ainda não segue usuário algum. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.",
"empty_column.home.public_timeline": "global",
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "para mencionar o autor",
"keyboard_shortcuts.reply": "para responder",
"keyboard_shortcuts.search": "para focar a pesquisa",
+ "keyboard_shortcuts.toggle_hidden": "mostrar/esconder o texto com aviso de conteúdo",
"keyboard_shortcuts.toot": "para compor um novo toot",
"keyboard_shortcuts.unfocus": "para remover o foco da área de composição/pesquisa",
"keyboard_shortcuts.up": "para mover para cima na lista",
@@ -156,8 +157,8 @@
"mute_modal.hide_notifications": "Esconder notificações deste usuário?",
"navigation_bar.blocks": "Usuários bloqueados",
"navigation_bar.community_timeline": "Local",
- "navigation_bar.direct": "Direct messages",
- "navigation_bar.domain_blocks": "Hidden domains",
+ "navigation_bar.direct": "Mensagens diretas",
+ "navigation_bar.domain_blocks": "Domínios escondidos",
"navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.favourites": "Favoritos",
"navigation_bar.follow_requests": "Seguidores pendentes",
@@ -241,10 +242,10 @@
"search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
"standalone.public_title": "Dê uma espiada...",
"status.block": "Block @{name}",
- "status.cancel_reblog_private": "Unboost",
+ "status.cancel_reblog_private": "Retirar o compartilhamento",
"status.cannot_reblog": "Esta postagem não pode ser compartilhada",
"status.delete": "Excluir",
- "status.direct": "Direct message @{name}",
+ "status.direct": "Enviar mensagem direta à @{name}",
"status.embed": "Incorporar",
"status.favourite": "Adicionar aos favoritos",
"status.load_more": "Carregar mais",
@@ -257,7 +258,7 @@
"status.pin": "Fixar no perfil",
"status.pinned": "Toot fixado",
"status.reblog": "Compartilhar",
- "status.reblog_private": "Boost to original audience",
+ "status.reblog_private": "Compartilhar com a audiência original",
"status.reblogged_by": "{name} compartilhou",
"status.reply": "Responder",
"status.replyAll": "Responder à sequência",
@@ -275,7 +276,7 @@
"tabs_bar.home": "Página inicial",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notificações",
- "tabs_bar.search": "Search",
+ "tabs_bar.search": "Buscar",
"ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.",
"upload_area.title": "Arraste e solte para enviar",
"upload_button.label": "Adicionar mídia",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 826785aad1..ce816dc41c 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "para mencionar o autor",
"keyboard_shortcuts.reply": "para responder",
"keyboard_shortcuts.search": "para focar na pesquisa",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "para compor um novo post",
"keyboard_shortcuts.unfocus": "para remover o foco da área de publicação/pesquisa",
"keyboard_shortcuts.up": "para mover para cima na lista",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index bb3cc1794b..8eeebaf736 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "упомянуть автора поста",
"keyboard_shortcuts.reply": "ответить",
"keyboard_shortcuts.search": "перейти к поиску",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "начать писать новый пост",
"keyboard_shortcuts.unfocus": "убрать фокус с поля ввода/поиска",
"keyboard_shortcuts.up": "вверх по списку",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 58274fd2d8..e5e826c964 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -2,7 +2,7 @@
"account.block": "Blokovať @{name}",
"account.block_domain": "Ukryť všetko z {domain}",
"account.blocked": "Blokovaný/á",
- "account.direct": "Direct message @{name}",
+ "account.direct": "Súkromná správa pre @{name}",
"account.disclaimer_full": "Inofrmácie nižšie nemusia byť úplným odrazom uživateľovho účtu.",
"account.domain_blocked": "Doména ukrytá",
"account.edit_profile": "Upraviť profil",
@@ -29,7 +29,7 @@
"account.unmute": "Prestať ignorovať @{name}",
"account.unmute_notifications": "Odtĺmiť notifikácie od @{name}",
"account.view_full_profile": "Pozri celý profil",
- "alert.unexpected.message": "An unexpected error occurred.",
+ "alert.unexpected.message": "Vyskytla sa neočakávaná chyba.",
"alert.unexpected.title": "Oops!",
"boost_modal.combo": "Nabudúce môžete kliknúť {combo} aby ste preskočili",
"bundle_column_error.body": "Nastala chyba pri načítaní tohto komponentu.",
@@ -40,8 +40,8 @@
"bundle_modal_error.retry": "Skúsiť znova",
"column.blocks": "Blokovaní užívatelia",
"column.community": "Lokálna časová os",
- "column.direct": "Direct messages",
- "column.domain_blocks": "Hidden domains",
+ "column.direct": "Súkromné správy",
+ "column.domain_blocks": "Skryté domény",
"column.favourites": "Obľúbené",
"column.follow_requests": "Žiadosti o sledovanie",
"column.home": "Domov",
@@ -59,7 +59,7 @@
"column_header.unpin": "Odopnúť",
"column_subheading.navigation": "Navigácia",
"column_subheading.settings": "Nastavenia",
- "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+ "compose_form.direct_message_warning": "Tento príspevok bude videný výhradne iba spomenutými užívateľmi.",
"compose_form.hashtag_warning": "Tento toot nebude zobrazený pod žiadným haštagom lebo nieje listovaný. Iba verejné tooty môžu byť nájdené podľa haštagu.",
"compose_form.lock_disclaimer": "Váš účet nie je zamknutý. Ktokoľvek ťa môže nasledovať a vidieť tvoje správy pre sledujúcich.",
"compose_form.lock_disclaimer.lock": "zamknutý",
@@ -101,7 +101,7 @@
"emoji_button.symbols": "Symboly",
"emoji_button.travel": "Cestovanie a miesta",
"empty_column.community": "Lokálna časová os je prázdna. Napíšte niečo, aby sa to tu začalo hýbať!",
- "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+ "empty_column.direct": "Ešte nemáš žiadne súkromné správy. Keď nejakú pošleš, alebo dostaneš, ukáže sa tu.",
"empty_column.hashtag": "Pod týmto hashtagom sa ešte nič nenachádza.",
"empty_column.home": "Vaša lokálna osa je zatiaľ prázdna! Pre začiatok pozrite {public} alebo použite vyhľadávanie a nájdite tak ostatných používateľov.",
"empty_column.home.public_timeline": "verejná časová os",
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "spomenúť autora",
"keyboard_shortcuts.reply": "odpovedať",
"keyboard_shortcuts.search": "zamerať sa na vyhľadávanie",
+ "keyboard_shortcuts.toggle_hidden": "ukáž/skry text za CW",
"keyboard_shortcuts.toot": "začať úplne novú hlášku",
"keyboard_shortcuts.unfocus": "nesústrediť sa na písaciu plochu, alebo hľadanie",
"keyboard_shortcuts.up": "posunúť sa vyššie v zozname",
@@ -156,8 +157,8 @@
"mute_modal.hide_notifications": "Skryť notifikácie od tohoto užívateľa?",
"navigation_bar.blocks": "Blokovaní užívatelia",
"navigation_bar.community_timeline": "Lokálna časová os",
- "navigation_bar.direct": "Direct messages",
- "navigation_bar.domain_blocks": "Hidden domains",
+ "navigation_bar.direct": "Súkromné správy",
+ "navigation_bar.domain_blocks": "Skryté domény",
"navigation_bar.edit_profile": "Upraviť profil",
"navigation_bar.favourites": "Obľúbené",
"navigation_bar.follow_requests": "Žiadosti o sledovanie",
@@ -244,7 +245,7 @@
"status.cancel_reblog_private": "Unboost",
"status.cannot_reblog": "Tento príspevok nemôže byť re-tootnutý",
"status.delete": "Zmazať",
- "status.direct": "Direct message @{name}",
+ "status.direct": "Súkromná správa @{name}",
"status.embed": "Vložiť",
"status.favourite": "Páči sa mi",
"status.load_more": "Zobraz viac",
@@ -275,7 +276,7 @@
"tabs_bar.home": "Domov",
"tabs_bar.local_timeline": "Lokálna",
"tabs_bar.notifications": "Notifikácie",
- "tabs_bar.search": "Search",
+ "tabs_bar.search": "Hľadaj",
"ui.beforeunload": "Čo máte rozpísané sa stratí, ak opustíte Mastodon.",
"upload_area.title": "Ťahaj a pusti pre nahratie",
"upload_button.label": "Pridať médiá",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index e4d07edd14..b1ea0d1795 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "da pomenete autora",
"keyboard_shortcuts.reply": "da odgovorite",
"keyboard_shortcuts.search": "da se prebacite na pretragu",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "da započnete skroz novi tut",
"keyboard_shortcuts.unfocus": "da ne budete više na pretrazi/pravljenju novog tuta",
"keyboard_shortcuts.up": "da se pomerite na gore u listi",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index 60c781e9dc..aa978675fe 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "да поменете аутора",
"keyboard_shortcuts.reply": "да одговорите",
"keyboard_shortcuts.search": "да се пребаците на претрагу",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "да започнете скроз нови тут",
"keyboard_shortcuts.unfocus": "да не будете више на претрази/прављењу новог тута",
"keyboard_shortcuts.up": "да се померите на горе у листи",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 8fa6992f1d..4efe88a7e7 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -18,7 +18,7 @@
"account.mute_notifications": "Stäng av notifieringar från @{name}",
"account.muted": "Nertystad",
"account.posts": "Inlägg",
- "account.posts_with_replies": "Toots med svar",
+ "account.posts_with_replies": "Toots och svar",
"account.report": "Rapportera @{name}",
"account.requested": "Inväntar godkännande. Klicka för att avbryta följförfrågan",
"account.share": "Dela @{name}'s profil",
@@ -29,7 +29,7 @@
"account.unmute": "Ta bort tystad @{name}",
"account.unmute_notifications": "Återaktivera notifikationer från @{name}",
"account.view_full_profile": "Visa hela profilen",
- "alert.unexpected.message": "An unexpected error occurred.",
+ "alert.unexpected.message": "Ett oväntat fel uppstod.",
"alert.unexpected.title": "Oops!",
"boost_modal.combo": "Du kan trycka {combo} för att slippa denna nästa gång",
"bundle_column_error.body": "Något gick fel när du laddade denna komponent.",
@@ -40,8 +40,8 @@
"bundle_modal_error.retry": "Försök igen",
"column.blocks": "Blockerade användare",
"column.community": "Lokal tidslinje",
- "column.direct": "Direct messages",
- "column.domain_blocks": "Hidden domains",
+ "column.direct": "Direktmeddelande",
+ "column.domain_blocks": "Dolda domäner",
"column.favourites": "Favoriter",
"column.follow_requests": "Följ förfrågningar",
"column.home": "Hem",
@@ -59,7 +59,7 @@
"column_header.unpin": "Ångra fäst",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Inställningar",
- "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+ "compose_form.direct_message_warning": "Denna toot kommer endast vara synlig för nämnda användare.",
"compose_form.hashtag_warning": "Denna toot kommer inte att listas under någon hashtag eftersom den är onoterad. Endast offentliga toots kan sökas med hashtag.",
"compose_form.lock_disclaimer": "Ditt konto är inte {locked}. Vemsomhelst kan följa dig och även se dina inlägg skrivna för endast dina följare.",
"compose_form.lock_disclaimer.lock": "låst",
@@ -101,7 +101,7 @@
"emoji_button.symbols": "Symboler",
"emoji_button.travel": "Resor & Platser",
"empty_column.community": "Den lokala tidslinjen är tom. Skriv något offentligt för att få bollen att rulla!",
- "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+ "empty_column.direct": "Du har inga direktmeddelanden än. När du skickar eller tar emot kommer den att dyka upp här.",
"empty_column.hashtag": "Det finns inget i denna hashtag ännu.",
"empty_column.home": "Din hemma-tidslinje är tom! Besök {public} eller använd sökning för att komma igång och träffa andra användare.",
"empty_column.home.public_timeline": "den publika tidslinjen",
@@ -113,7 +113,7 @@
"getting_started.appsshort": "Appar",
"getting_started.faq": "FAQ",
"getting_started.heading": "Kom igång",
- "getting_started.open_source_notice": "Mastodon är programvara med öppen källkod. Du kan bidra eller rapportera problem på GitHub på {github}.",
+ "getting_started.open_source_notice": "Mastodon är programvara med öppen källkod. Du kan bidra eller rapportera problem via GitHub på {github}.",
"getting_started.userguide": "Användarguide",
"home.column_settings.advanced": "Avancerad",
"home.column_settings.basic": "Grundläggande",
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "att nämna författaren",
"keyboard_shortcuts.reply": "att svara",
"keyboard_shortcuts.search": "att fokusera sökfältet",
+ "keyboard_shortcuts.toggle_hidden": "att visa/gömma text bakom CW",
"keyboard_shortcuts.toot": "att börja en helt ny toot",
"keyboard_shortcuts.unfocus": "att avfokusera komponera text fält / sökfält",
"keyboard_shortcuts.up": "att flytta upp i listan",
@@ -156,8 +157,8 @@
"mute_modal.hide_notifications": "Dölj notifikationer från denna användare?",
"navigation_bar.blocks": "Blockerade användare",
"navigation_bar.community_timeline": "Lokal tidslinje",
- "navigation_bar.direct": "Direct messages",
- "navigation_bar.domain_blocks": "Hidden domains",
+ "navigation_bar.direct": "Direktmeddelanden",
+ "navigation_bar.domain_blocks": "Dolda domäner",
"navigation_bar.edit_profile": "Redigera profil",
"navigation_bar.favourites": "Favoriter",
"navigation_bar.follow_requests": "Följförfrågningar",
@@ -205,7 +206,7 @@
"onboarding.page_three.search": "Använd sökfältet för att hitta personer och titta på hashtags, till exempel {illustration} och {introductions}. För att leta efter en person som inte befinner sig i detta fall använd deras fulla handhavande.",
"onboarding.page_two.compose": "Skriv inlägg från skrivkolumnen. Du kan ladda upp bilder, ändra integritetsinställningar och lägga till varningar med ikonerna nedan.",
"onboarding.skip": "Hoppa över",
- "privacy.change": "Justera status sekretess",
+ "privacy.change": "Justera sekretess",
"privacy.direct.long": "Skicka endast till nämnda användare",
"privacy.direct.short": "Direkt",
"privacy.private.long": "Skicka endast till följare",
@@ -241,10 +242,10 @@
"search_results.total": "{count, number} {count, plural, ett {result} andra {results}}",
"standalone.public_title": "En titt inuti...",
"status.block": "Block @{name}",
- "status.cancel_reblog_private": "Unboost",
+ "status.cancel_reblog_private": "Ta bort knuff",
"status.cannot_reblog": "Detta inlägg kan inte knuffas",
"status.delete": "Ta bort",
- "status.direct": "Direct message @{name}",
+ "status.direct": "Direktmeddela @{name}",
"status.embed": "Bädda in",
"status.favourite": "Favorit",
"status.load_more": "Ladda fler",
@@ -257,7 +258,7 @@
"status.pin": "Fäst i profil",
"status.pinned": "Fäst toot",
"status.reblog": "Knuff",
- "status.reblog_private": "Boost to original audience",
+ "status.reblog_private": "Knuffa till de ursprungliga åhörarna",
"status.reblogged_by": "{name} knuffade",
"status.reply": "Svara",
"status.replyAll": "Svara på tråden",
@@ -275,7 +276,7 @@
"tabs_bar.home": "Hem",
"tabs_bar.local_timeline": "Lokal",
"tabs_bar.notifications": "Meddelanden",
- "tabs_bar.search": "Search",
+ "tabs_bar.search": "Sök",
"ui.beforeunload": "Ditt utkast kommer att förloras om du lämnar Mastodon.",
"upload_area.title": "Dra & släpp för att ladda upp",
"upload_button.label": "Lägg till media",
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
new file mode 100644
index 0000000000..a56720fee3
--- /dev/null
+++ b/app/javascript/mastodon/locales/te.json
@@ -0,0 +1,296 @@
+{
+ "account.block": "Block @{name}",
+ "account.block_domain": "Hide everything from {domain}",
+ "account.blocked": "Blocked",
+ "account.direct": "Direct message @{name}",
+ "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+ "account.domain_blocked": "Domain hidden",
+ "account.edit_profile": "Edit profile",
+ "account.follow": "Follow",
+ "account.followers": "Followers",
+ "account.follows": "Follows",
+ "account.follows_you": "Follows you",
+ "account.hide_reblogs": "Hide boosts from @{name}",
+ "account.media": "Media",
+ "account.mention": "Mention @{name}",
+ "account.moved_to": "{name} has moved to:",
+ "account.mute": "Mute @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
+ "account.muted": "Muted",
+ "account.posts": "Toots",
+ "account.posts_with_replies": "Toots and replies",
+ "account.report": "Report @{name}",
+ "account.requested": "Awaiting approval. Click to cancel follow request",
+ "account.share": "Share @{name}'s profile",
+ "account.show_reblogs": "Show boosts from @{name}",
+ "account.unblock": "Unblock @{name}",
+ "account.unblock_domain": "Unhide {domain}",
+ "account.unfollow": "Unfollow",
+ "account.unmute": "Unmute @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
+ "account.view_full_profile": "View full profile",
+ "alert.unexpected.message": "An unexpected error occurred.",
+ "alert.unexpected.title": "Oops!",
+ "boost_modal.combo": "You can press {combo} to skip this next time",
+ "bundle_column_error.body": "Something went wrong while loading this component.",
+ "bundle_column_error.retry": "Try again",
+ "bundle_column_error.title": "Network error",
+ "bundle_modal_error.close": "Close",
+ "bundle_modal_error.message": "Something went wrong while loading this component.",
+ "bundle_modal_error.retry": "Try again",
+ "column.blocks": "Blocked users",
+ "column.community": "Local timeline",
+ "column.direct": "Direct messages",
+ "column.domain_blocks": "Hidden domains",
+ "column.favourites": "Favourites",
+ "column.follow_requests": "Follow requests",
+ "column.home": "Home",
+ "column.lists": "Lists",
+ "column.mutes": "Muted users",
+ "column.notifications": "Notifications",
+ "column.pins": "Pinned toot",
+ "column.public": "Federated timeline",
+ "column_back_button.label": "Back",
+ "column_header.hide_settings": "Hide settings",
+ "column_header.moveLeft_settings": "Move column to the left",
+ "column_header.moveRight_settings": "Move column to the right",
+ "column_header.pin": "Pin",
+ "column_header.show_settings": "Show settings",
+ "column_header.unpin": "Unpin",
+ "column_subheading.navigation": "Navigation",
+ "column_subheading.settings": "Settings",
+ "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+ "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
+ "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
+ "compose_form.lock_disclaimer.lock": "locked",
+ "compose_form.placeholder": "What is on your mind?",
+ "compose_form.publish": "Toot",
+ "compose_form.publish_loud": "{publish}!",
+ "compose_form.sensitive.marked": "Media is marked as sensitive",
+ "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
+ "compose_form.spoiler.marked": "Text is hidden behind warning",
+ "compose_form.spoiler.unmarked": "Text is not hidden",
+ "compose_form.spoiler_placeholder": "Write your warning here",
+ "confirmation_modal.cancel": "Cancel",
+ "confirmations.block.confirm": "Block",
+ "confirmations.block.message": "Are you sure you want to block {name}?",
+ "confirmations.delete.confirm": "Delete",
+ "confirmations.delete.message": "Are you sure you want to delete this status?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+ "confirmations.domain_block.confirm": "Hide entire domain",
+ "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
+ "confirmations.mute.confirm": "Mute",
+ "confirmations.mute.message": "Are you sure you want to mute {name}?",
+ "confirmations.unfollow.confirm": "Unfollow",
+ "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+ "embed.instructions": "Embed this status on your website by copying the code below.",
+ "embed.preview": "Here is what it will look like:",
+ "emoji_button.activity": "Activity",
+ "emoji_button.custom": "Custom",
+ "emoji_button.flags": "Flags",
+ "emoji_button.food": "Food & Drink",
+ "emoji_button.label": "Insert emoji",
+ "emoji_button.nature": "Nature",
+ "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.objects": "Objects",
+ "emoji_button.people": "People",
+ "emoji_button.recent": "Frequently used",
+ "emoji_button.search": "Search...",
+ "emoji_button.search_results": "Search results",
+ "emoji_button.symbols": "Symbols",
+ "emoji_button.travel": "Travel & Places",
+ "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
+ "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+ "empty_column.hashtag": "There is nothing in this hashtag yet.",
+ "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
+ "empty_column.home.public_timeline": "the public timeline",
+ "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
+ "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
+ "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
+ "follow_request.authorize": "Authorize",
+ "follow_request.reject": "Reject",
+ "getting_started.appsshort": "Apps",
+ "getting_started.faq": "FAQ",
+ "getting_started.heading": "Getting started",
+ "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
+ "getting_started.userguide": "User Guide",
+ "home.column_settings.advanced": "Advanced",
+ "home.column_settings.basic": "Basic",
+ "home.column_settings.filter_regex": "Filter out by regular expressions",
+ "home.column_settings.show_reblogs": "Show boosts",
+ "home.column_settings.show_replies": "Show replies",
+ "home.settings": "Column settings",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
+ "lightbox.close": "Close",
+ "lightbox.next": "Next",
+ "lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
+ "loading_indicator.label": "Loading...",
+ "media_gallery.toggle_visible": "Toggle visibility",
+ "missing_indicator.label": "Not found",
+ "missing_indicator.sublabel": "This resource could not be found",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
+ "navigation_bar.blocks": "Blocked users",
+ "navigation_bar.community_timeline": "Local timeline",
+ "navigation_bar.direct": "Direct messages",
+ "navigation_bar.domain_blocks": "Hidden domains",
+ "navigation_bar.edit_profile": "Edit profile",
+ "navigation_bar.favourites": "Favourites",
+ "navigation_bar.follow_requests": "Follow requests",
+ "navigation_bar.info": "Extended information",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
+ "navigation_bar.logout": "Logout",
+ "navigation_bar.mutes": "Muted users",
+ "navigation_bar.pins": "Pinned toots",
+ "navigation_bar.preferences": "Preferences",
+ "navigation_bar.public_timeline": "Federated timeline",
+ "notification.favourite": "{name} favourited your status",
+ "notification.follow": "{name} followed you",
+ "notification.mention": "{name} mentioned you",
+ "notification.reblog": "{name} boosted your status",
+ "notifications.clear": "Clear notifications",
+ "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
+ "notifications.column_settings.alert": "Desktop notifications",
+ "notifications.column_settings.favourite": "Favourites:",
+ "notifications.column_settings.follow": "New followers:",
+ "notifications.column_settings.mention": "Mentions:",
+ "notifications.column_settings.push": "Push notifications",
+ "notifications.column_settings.push_meta": "This device",
+ "notifications.column_settings.reblog": "Boosts:",
+ "notifications.column_settings.show": "Show in column",
+ "notifications.column_settings.sound": "Play sound",
+ "onboarding.done": "Done",
+ "onboarding.next": "Next",
+ "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
+ "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
+ "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
+ "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
+ "onboarding.page_one.full_handle": "Your full handle",
+ "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
+ "onboarding.page_one.welcome": "Welcome to Mastodon!",
+ "onboarding.page_six.admin": "Your instance's admin is {admin}.",
+ "onboarding.page_six.almost_done": "Almost done...",
+ "onboarding.page_six.appetoot": "Bon Appetoot!",
+ "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
+ "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
+ "onboarding.page_six.guidelines": "community guidelines",
+ "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
+ "onboarding.page_six.various_app": "mobile apps",
+ "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
+ "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
+ "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
+ "onboarding.skip": "Skip",
+ "privacy.change": "Adjust status privacy",
+ "privacy.direct.long": "Post to mentioned users only",
+ "privacy.direct.short": "Direct",
+ "privacy.private.long": "Post to followers only",
+ "privacy.private.short": "Followers-only",
+ "privacy.public.long": "Post to public timelines",
+ "privacy.public.short": "Public",
+ "privacy.unlisted.long": "Do not show in public timelines",
+ "privacy.unlisted.short": "Unlisted",
+ "regeneration_indicator.label": "Loading…",
+ "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+ "relative_time.days": "{number}d",
+ "relative_time.hours": "{number}h",
+ "relative_time.just_now": "now",
+ "relative_time.minutes": "{number}m",
+ "relative_time.seconds": "{number}s",
+ "reply_indicator.cancel": "Cancel",
+ "report.forward": "Forward to {target}",
+ "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
+ "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+ "report.placeholder": "Additional comments",
+ "report.submit": "Submit",
+ "report.target": "Report {target}",
+ "search.placeholder": "Search",
+ "search_popout.search_format": "Advanced search format",
+ "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+ "search_popout.tips.hashtag": "hashtag",
+ "search_popout.tips.status": "status",
+ "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+ "search_popout.tips.user": "user",
+ "search_results.accounts": "People",
+ "search_results.hashtags": "Hashtags",
+ "search_results.statuses": "Toots",
+ "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+ "standalone.public_title": "A look inside...",
+ "status.block": "Block @{name}",
+ "status.cancel_reblog_private": "Unboost",
+ "status.cannot_reblog": "This post cannot be boosted",
+ "status.delete": "Delete",
+ "status.direct": "Direct message @{name}",
+ "status.embed": "Embed",
+ "status.favourite": "Favourite",
+ "status.load_more": "Load more",
+ "status.media_hidden": "Media hidden",
+ "status.mention": "Mention @{name}",
+ "status.more": "More",
+ "status.mute": "Mute @{name}",
+ "status.mute_conversation": "Mute conversation",
+ "status.open": "Expand this status",
+ "status.pin": "Pin on profile",
+ "status.pinned": "Pinned toot",
+ "status.reblog": "Boost",
+ "status.reblog_private": "Boost to original audience",
+ "status.reblogged_by": "{name} boosted",
+ "status.reply": "Reply",
+ "status.replyAll": "Reply to thread",
+ "status.report": "Report @{name}",
+ "status.sensitive_toggle": "Click to view",
+ "status.sensitive_warning": "Sensitive content",
+ "status.share": "Share",
+ "status.show_less": "Show less",
+ "status.show_less_all": "Show less for all",
+ "status.show_more": "Show more",
+ "status.show_more_all": "Show more for all",
+ "status.unmute_conversation": "Unmute conversation",
+ "status.unpin": "Unpin from profile",
+ "tabs_bar.federated_timeline": "Federated",
+ "tabs_bar.home": "Home",
+ "tabs_bar.local_timeline": "Local",
+ "tabs_bar.notifications": "Notifications",
+ "tabs_bar.search": "Search",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+ "upload_area.title": "Drag & drop to upload",
+ "upload_button.label": "Add media",
+ "upload_form.description": "Describe for the visually impaired",
+ "upload_form.focus": "Crop",
+ "upload_form.undo": "Undo",
+ "upload_progress.label": "Uploading...",
+ "video.close": "Close video",
+ "video.exit_fullscreen": "Exit full screen",
+ "video.expand": "Expand video",
+ "video.fullscreen": "Full screen",
+ "video.hide": "Hide video",
+ "video.mute": "Mute sound",
+ "video.pause": "Pause",
+ "video.play": "Play",
+ "video.unmute": "Unmute sound"
+}
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 3b91c0d2ce..82b44fe307 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "to start a brand new toot",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index cdf6f46a3b..056fbfe8fe 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "to start a brand new toot",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 261e5795e0..1a7b587893 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "to start a brand new toot",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/whitelist_el.json b/app/javascript/mastodon/locales/whitelist_el.json
new file mode 100644
index 0000000000..0d4f101c7a
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_el.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/whitelist_eu.json b/app/javascript/mastodon/locales/whitelist_eu.json
new file mode 100644
index 0000000000..0d4f101c7a
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_eu.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/whitelist_te.json b/app/javascript/mastodon/locales/whitelist_te.json
new file mode 100644
index 0000000000..0d4f101c7a
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_te.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index aba0bde83a..a3a4de0af4 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "提及嘟文作者",
"keyboard_shortcuts.reply": "回复嘟文",
"keyboard_shortcuts.search": "选择搜索框",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "发送新嘟文",
"keyboard_shortcuts.unfocus": "取消输入",
"keyboard_shortcuts.up": "在列表中让光标上移",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index b5ebd20fc4..7719e08a64 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "提及作者",
"keyboard_shortcuts.reply": "回覆",
"keyboard_shortcuts.search": "把標示移動到搜索",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "新的推文",
"keyboard_shortcuts.unfocus": "把標示移離文字輸入和搜索",
"keyboard_shortcuts.up": "在列表往上移動",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 28d6346007..84ff25e037 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -135,6 +135,7 @@
"keyboard_shortcuts.mention": "到提到的作者",
"keyboard_shortcuts.reply": "到回應",
"keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "to start a brand new toot",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index da9b8c420e..84d4fc6988 100644
--- a/app/javascript/mastodon/reducers/notifications.js
+++ b/app/javascript/mastodon/reducers/notifications.js
@@ -105,7 +105,7 @@ export default function notifications(state = initialState, action) {
return expandNormalizedNotifications(state, action.notifications, action.next);
case ACCOUNT_BLOCK_SUCCESS:
case ACCOUNT_MUTE_SUCCESS:
- return filterNotifications(state, action.relationship);
+ return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state;
case NOTIFICATIONS_CLEAR:
return state.set('items', ImmutableList()).set('hasMore', false);
case TIMELINE_DELETE:
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index ad897bcc97..dd675d78fb 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -34,7 +34,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial) =>
mMap.update('items', ImmutableList(), oldIds => {
const newIds = statuses.map(status => status.get('id'));
const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
- const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) >= 0);
+ const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);
if (firstIndex < 0) {
return (isPartial ? newIds.unshift(null) : newIds).concat(oldIds.skip(lastIndex));
diff --git a/app/javascript/mastodon/utils/__tests__/base64-test.js b/app/javascript/mastodon/utils/__tests__/base64-test.js
new file mode 100644
index 0000000000..1b3260faaf
--- /dev/null
+++ b/app/javascript/mastodon/utils/__tests__/base64-test.js
@@ -0,0 +1,10 @@
+import * as base64 from '../base64';
+
+describe('base64', () => {
+ describe('decode', () => {
+ it('returns a uint8 array', () => {
+ const arr = base64.decode('dGVzdA==');
+ expect(arr).toEqual(new Uint8Array([116, 101, 115, 116]));
+ });
+ });
+});
diff --git a/app/javascript/mastodon/utils/base64.js b/app/javascript/mastodon/utils/base64.js
new file mode 100644
index 0000000000..8226e2c54e
--- /dev/null
+++ b/app/javascript/mastodon/utils/base64.js
@@ -0,0 +1,10 @@
+export const decode = base64 => {
+ const rawData = window.atob(base64);
+ const outputArray = new Uint8Array(rawData.length);
+
+ for (let i = 0; i < rawData.length; ++i) {
+ outputArray[i] = rawData.charCodeAt(i);
+ }
+
+ return outputArray;
+};
diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js
new file mode 100644
index 0000000000..6442eda38c
--- /dev/null
+++ b/app/javascript/mastodon/utils/resize_image.js
@@ -0,0 +1,66 @@
+const MAX_IMAGE_DIMENSION = 1280;
+
+const getImageUrl = inputFile => new Promise((resolve, reject) => {
+ if (window.URL && URL.createObjectURL) {
+ try {
+ resolve(URL.createObjectURL(inputFile));
+ } catch (error) {
+ reject(error);
+ }
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onerror = (...args) => reject(...args);
+ reader.onload = ({ target }) => resolve(target.result);
+
+ reader.readAsDataURL(inputFile);
+});
+
+const loadImage = inputFile => new Promise((resolve, reject) => {
+ getImageUrl(inputFile).then(url => {
+ const img = new Image();
+
+ img.onerror = (...args) => reject(...args);
+ img.onload = () => resolve(img);
+
+ img.src = url;
+ }).catch(reject);
+});
+
+export default inputFile => new Promise((resolve, reject) => {
+ if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') {
+ resolve(inputFile);
+ return;
+ }
+
+ loadImage(inputFile).then(img => {
+ const canvas = document.createElement('canvas');
+ const { width, height } = img;
+
+ let newWidth, newHeight;
+
+ if (width < MAX_IMAGE_DIMENSION && height < MAX_IMAGE_DIMENSION) {
+ resolve(inputFile);
+ return;
+ }
+
+ if (width > height) {
+ newHeight = height * MAX_IMAGE_DIMENSION / width;
+ newWidth = MAX_IMAGE_DIMENSION;
+ } else if (height > width) {
+ newWidth = width * MAX_IMAGE_DIMENSION / height;
+ newHeight = MAX_IMAGE_DIMENSION;
+ } else {
+ newWidth = MAX_IMAGE_DIMENSION;
+ newHeight = MAX_IMAGE_DIMENSION;
+ }
+
+ canvas.width = newWidth;
+ canvas.height = newHeight;
+
+ canvas.getContext('2d').drawImage(img, 0, 0, newWidth, newHeight);
+
+ canvas.toBlob(resolve, inputFile.type);
+ }).catch(reject);
+});
diff --git a/app/javascript/styles/contrast.scss b/app/javascript/styles/contrast.scss
new file mode 100644
index 0000000000..5b43aecbe7
--- /dev/null
+++ b/app/javascript/styles/contrast.scss
@@ -0,0 +1,3 @@
+@import 'contrast/variables';
+@import 'application';
+@import 'contrast/diff';
diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss
new file mode 100644
index 0000000000..eee9ecc3ef
--- /dev/null
+++ b/app/javascript/styles/contrast/diff.scss
@@ -0,0 +1,14 @@
+// components.scss
+.compose-form {
+ .compose-form__modifiers {
+ .compose-form__upload {
+ &-description {
+ input {
+ &::placeholder {
+ opacity: 1.0;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/javascript/styles/contrast/variables.scss b/app/javascript/styles/contrast/variables.scss
new file mode 100644
index 0000000000..f6cadf0298
--- /dev/null
+++ b/app/javascript/styles/contrast/variables.scss
@@ -0,0 +1,24 @@
+// Dependent colors
+$black: #000000;
+
+$classic-base-color: #282c37;
+$classic-primary-color: #9baec8;
+$classic-secondary-color: #d9e1e8;
+$classic-highlight-color: #2b90d9;
+
+$ui-base-color: $classic-base-color !default;
+$ui-primary-color: $classic-primary-color !default;
+$ui-secondary-color: $classic-secondary-color !default;
+
+// Differences
+$ui-highlight-color: #2b5fd9;
+
+$darker-text-color: lighten($ui-primary-color, 20%) !default;
+$dark-text-color: lighten($ui-primary-color, 12%) !default;
+$secondary-text-color: lighten($ui-secondary-color, 6%) !default;
+$highlight-text-color: $classic-highlight-color !default;
+$action-button-color: #8d9ac2;
+
+$inverted-text-color: $black !default;
+$lighter-text-color: darken($ui-base-color,6%) !default;
+$light-text-color: darken($ui-primary-color, 40%) !default;
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index 0a09a38d24..c9c0e3081a 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -225,7 +225,7 @@ $small-breakpoint: 960px;
font-family: inherit;
font-size: inherit;
line-height: inherit;
- color: transparentize($darker-text-color, 0.1);
+ color: lighten($darker-text-color, 10%);
}
h1 {
@@ -234,14 +234,14 @@ $small-breakpoint: 960px;
line-height: 30px;
font-weight: 500;
margin-bottom: 20px;
- color: $primary-text-color;
+ color: $secondary-text-color;
small {
font-family: 'mastodon-font-sans-serif', sans-serif;
display: block;
font-size: 18px;
font-weight: 400;
- color: opacify($darker-text-color, 0.1);
+ color: lighten($darker-text-color, 10%);
}
}
@@ -251,7 +251,7 @@ $small-breakpoint: 960px;
line-height: 26px;
font-weight: 500;
margin-bottom: 20px;
- color: $primary-text-color;
+ color: $secondary-text-color;
}
h3 {
@@ -260,7 +260,7 @@ $small-breakpoint: 960px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
- color: $primary-text-color;
+ color: $secondary-text-color;
}
h4 {
@@ -269,7 +269,7 @@ $small-breakpoint: 960px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
- color: $primary-text-color;
+ color: $secondary-text-color;
}
h5 {
@@ -278,7 +278,7 @@ $small-breakpoint: 960px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
- color: $primary-text-color;
+ color: $secondary-text-color;
}
h6 {
@@ -287,7 +287,7 @@ $small-breakpoint: 960px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
- color: $primary-text-color;
+ color: $secondary-text-color;
}
ul,
@@ -405,7 +405,7 @@ $small-breakpoint: 960px;
font-size: 14px;
&:hover {
- color: $darker-text-color;
+ color: $secondary-text-color;
}
}
@@ -517,7 +517,7 @@ $small-breakpoint: 960px;
span {
&:last-child {
- color: $darker-text-color;
+ color: $secondary-text-color;
}
}
@@ -559,7 +559,7 @@ $small-breakpoint: 960px;
a,
span {
font-weight: 400;
- color: opacify($darker-text-color, 0.1);
+ color: darken($darker-text-color, 10%);
}
a {
@@ -775,7 +775,7 @@ $small-breakpoint: 960px;
}
p a {
- color: $darker-text-color;
+ color: $secondary-text-color;
}
h1 {
@@ -787,7 +787,7 @@ $small-breakpoint: 960px;
color: $darker-text-color;
span {
- color: $darker-text-color;
+ color: $secondary-text-color;
}
}
}
@@ -896,7 +896,7 @@ $small-breakpoint: 960px;
}
a {
- color: $darker-text-color;
+ color: $secondary-text-color;
text-decoration: none;
}
}
@@ -980,7 +980,7 @@ $small-breakpoint: 960px;
.footer-links {
padding-bottom: 50px;
text-align: right;
- color: $darker-text-color;
+ color: $dark-text-color;
p {
font-size: 14px;
@@ -995,7 +995,7 @@ $small-breakpoint: 960px;
&__footer {
margin-top: 10px;
text-align: center;
- color: $darker-text-color;
+ color: $dark-text-color;
p {
font-size: 14px;
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index f9af6f2888..c2d0de4b99 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -178,7 +178,7 @@
font-size: 14px;
line-height: 18px;
padding: 0 15px;
- color: $darker-text-color;
+ color: $secondary-text-color;
}
@media screen and (max-width: 480px) {
@@ -256,7 +256,7 @@
.current {
background: $simple-background-color;
border-radius: 100px;
- color: $lighter-text-color;
+ color: $inverted-text-color;
cursor: default;
margin: 0 10px;
}
@@ -268,7 +268,7 @@
.older,
.newer {
text-transform: uppercase;
- color: $primary-text-color;
+ color: $secondary-text-color;
}
.older {
@@ -293,7 +293,7 @@
.disabled {
cursor: default;
- color: opacify($lighter-text-color, 0.1);
+ color: lighten($inverted-text-color, 10%);
}
@media screen and (max-width: 700px) {
@@ -332,7 +332,7 @@
width: 335px;
background: $simple-background-color;
border-radius: 4px;
- color: $lighter-text-color;
+ color: $inverted-text-color;
margin: 0 5px 10px;
position: relative;
@@ -344,7 +344,7 @@
overflow: hidden;
height: 100px;
border-radius: 4px 4px 0 0;
- background-color: opacify($lighter-text-color, 0.04);
+ background-color: lighten($inverted-text-color, 4%);
background-size: cover;
background-position: center;
position: relative;
@@ -422,7 +422,7 @@
.account__header__content {
padding: 10px 15px;
padding-top: 15px;
- color: transparentize($lighter-text-color, 0.1);
+ color: $lighter-text-color;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
@@ -434,7 +434,7 @@
.nothing-here {
width: 100%;
display: block;
- color: $lighter-text-color;
+ color: $light-text-color;
font-size: 14px;
font-weight: 500;
text-align: center;
@@ -493,7 +493,7 @@
span {
font-size: 14px;
- color: $inverted-text-color;
+ color: $light-text-color;
}
}
@@ -508,7 +508,7 @@
.account__header__content {
font-size: 14px;
- color: $darker-text-color;
+ color: $inverted-text-color;
}
}
@@ -586,7 +586,7 @@
font-weight: 500;
text-align: center;
width: 94px;
- color: opacify($darker-text-color, 0.1);
+ color: $secondary-text-color;
background: rgba(darken($ui-base-color, 8%), 0.5);
}
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 348f72078e..a6cc8b62ba 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -90,7 +90,7 @@
padding-left: 25px;
h2 {
- color: $primary-text-color;
+ color: $secondary-text-color;
font-size: 24px;
line-height: 28px;
font-weight: 400;
@@ -98,7 +98,7 @@
}
h3 {
- color: $primary-text-color;
+ color: $secondary-text-color;
font-size: 20px;
line-height: 28px;
font-weight: 400;
@@ -109,7 +109,7 @@
text-transform: uppercase;
font-size: 13px;
font-weight: 500;
- color: $primary-text-color;
+ color: $darker-text-color;
padding-bottom: 8px;
margin-bottom: 8px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
@@ -117,7 +117,7 @@
h6 {
font-size: 16px;
- color: $primary-text-color;
+ color: $secondary-text-color;
line-height: 28px;
font-weight: 400;
}
@@ -125,7 +125,7 @@
& > p {
font-size: 14px;
line-height: 18px;
- color: $darker-text-color;
+ color: $secondary-text-color;
margin-bottom: 20px;
strong {
@@ -141,14 +141,15 @@
}
hr {
- margin: 20px 0;
+ width: 100%;
+ height: 0;
border: 0;
- background: transparent;
- border-bottom: 1px solid $ui-base-color;
+ border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
+ margin: 20px 0;
- &.section-break {
- margin: 30px 0;
- border-bottom: 2px solid $ui-base-lighter-color;
+ &.spacer {
+ height: 1px;
+ border: 0;
}
}
@@ -291,7 +292,7 @@
font-weight: 500;
font-size: 14px;
line-height: 18px;
- color: $primary-text-color;
+ color: $secondary-text-color;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
@@ -335,34 +336,8 @@
}
}
-.report-note__comment {
- margin-bottom: 20px;
-}
-
-.report-note__form {
- margin-bottom: 20px;
-
- .report-note__textarea {
- box-sizing: border-box;
- border: 0;
- padding: 7px 4px;
- margin-bottom: 10px;
- font-size: 16px;
- color: $inverted-text-color;
- display: block;
- width: 100%;
- outline: 0;
- font-family: inherit;
- resize: vertical;
- }
-
- .report-note__buttons {
- text-align: right;
- }
-
- .report-note__button {
- margin: 0 0 5px 5px;
- }
+.simple_form.new_report_note {
+ max-width: 100%;
}
.batch-form-box {
@@ -390,13 +365,6 @@
}
}
-.batch-checkbox,
-.batch-checkbox-all {
- display: flex;
- align-items: center;
- margin-right: 5px;
-}
-
.back-link {
margin-bottom: 10px;
font-size: 14px;
@@ -416,7 +384,7 @@
}
.log-entry {
- margin-bottom: 8px;
+ margin-bottom: 20px;
line-height: 20px;
&__header {
@@ -452,7 +420,7 @@
}
&__timestamp {
- color: $darker-text-color;
+ color: $dark-text-color;
}
&__extras {
@@ -469,7 +437,7 @@
&__icon {
font-size: 28px;
margin-right: 10px;
- color: $darker-text-color;
+ color: $dark-text-color;
}
&__icon__overlay {
@@ -496,7 +464,7 @@
a,
.username,
.target {
- color: $primary-text-color;
+ color: $secondary-text-color;
text-decoration: none;
font-weight: 500;
}
@@ -506,7 +474,7 @@
}
.diff-neutral {
- color: $darker-text-color;
+ color: $secondary-text-color;
}
.diff-new {
@@ -514,9 +482,12 @@
}
}
+a.name-tag,
.name-tag {
display: flex;
align-items: center;
+ text-decoration: none;
+ color: $secondary-text-color;
.avatar {
display: block;
@@ -528,4 +499,52 @@
.username {
font-weight: 500;
}
+
+ &.suspended {
+ .username {
+ text-decoration: line-through;
+ color: lighten($error-red, 12%);
+ }
+
+ .avatar {
+ filter: grayscale(100%);
+ opacity: 0.8;
+ }
+ }
+}
+
+.speech-bubble {
+ margin-bottom: 20px;
+ border-left: 4px solid $ui-highlight-color;
+
+ &.positive {
+ border-left-color: $success-green;
+ }
+
+ &.negative {
+ border-left-color: lighten($error-red, 12%);
+ }
+
+ &__bubble {
+ padding: 16px;
+ padding-left: 14px;
+ font-size: 15px;
+ line-height: 20px;
+ border-radius: 4px 4px 4px 0;
+ position: relative;
+ font-weight: 500;
+
+ a {
+ color: $darker-text-color;
+ }
+ }
+
+ &__owner {
+ padding: 8px;
+ padding-left: 12px;
+ }
+
+ time {
+ color: $dark-text-color;
+ }
}
diff --git a/app/javascript/styles/mastodon/compact_header.scss b/app/javascript/styles/mastodon/compact_header.scss
index 83ac7a8d04..4980ab5f1a 100644
--- a/app/javascript/styles/mastodon/compact_header.scss
+++ b/app/javascript/styles/mastodon/compact_header.scss
@@ -2,7 +2,7 @@
h1 {
font-size: 24px;
line-height: 28px;
- color: $primary-text-color;
+ color: $darker-text-color;
font-weight: 500;
margin-bottom: 20px;
padding: 0 10px;
@@ -20,7 +20,7 @@
small {
font-weight: 400;
- color: $darker-text-color;
+ color: $secondary-text-color;
}
img {
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index f0fde66668..a982585c33 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -31,7 +31,7 @@
&:active,
&:focus,
&:hover {
- background-color: lighten($ui-highlight-color, 4%);
+ background-color: lighten($ui-highlight-color, 10%);
transition: all 200ms ease-out;
}
@@ -83,7 +83,7 @@
}
&.button-secondary {
- color: $ui-primary-color;
+ color: $darker-text-color;
background: transparent;
padding: 3px 15px;
border: 1px solid $ui-primary-color;
@@ -92,7 +92,7 @@
&:focus,
&:hover {
border-color: lighten($ui-primary-color, 4%);
- color: lighten($ui-primary-color, 4%);
+ color: lighten($darker-text-color, 4%);
}
}
@@ -149,18 +149,18 @@
&:hover,
&:active,
&:focus {
- color: transparentize($lighter-text-color, 0.07);
+ color: darken($lighter-text-color, 7%);
}
&.disabled {
- color: opacify($lighter-text-color, 0.07);
+ color: lighten($lighter-text-color, 7%);
}
&.active {
color: $highlight-text-color;
&.disabled {
- color: opacify($lighter-text-color, 0.13);
+ color: lighten($highlight-text-color, 13%);
}
}
}
@@ -193,12 +193,12 @@
&:hover,
&:active,
&:focus {
- color: opacify($lighter-text-color, 0.07);
+ color: darken($lighter-text-color, 7%);
transition: color 200ms ease-out;
}
&.disabled {
- color: transparentize($lighter-text-color, 0.2);
+ color: lighten($lighter-text-color, 20%);
cursor: default;
}
@@ -349,7 +349,7 @@
box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
background: $ui-secondary-color;
border-radius: 0 0 4px 4px;
- color: $lighter-text-color;
+ color: $inverted-text-color;
font-size: 14px;
padding: 6px;
@@ -457,7 +457,7 @@
input {
background: transparent;
- color: $primary-text-color;
+ color: $secondary-text-color;
border: 0;
padding: 0;
margin: 0;
@@ -471,8 +471,8 @@
}
&::placeholder {
- opacity: 0.54;
- color: $darker-text-color;
+ opacity: 0.75;
+ color: $secondary-text-color;
}
}
@@ -556,7 +556,6 @@
}
.emojione {
- display: inline-block;
font-size: inherit;
vertical-align: middle;
object-fit: contain;
@@ -588,7 +587,7 @@
}
.reply-indicator__display-name {
- color: $lighter-text-color;
+ color: $inverted-text-color;
display: block;
max-width: 100%;
line-height: 24px;
@@ -643,14 +642,14 @@
}
a {
- color: $ui-secondary-color;
+ color: $secondary-text-color;
text-decoration: none;
&:hover {
text-decoration: underline;
.fa {
- color: lighten($action-button-color, 7%);
+ color: lighten($dark-text-color, 7%);
}
}
@@ -665,7 +664,7 @@
}
.fa {
- color: $action-button-color;
+ color: $dark-text-color;
}
}
@@ -702,7 +701,7 @@
border-radius: 2px;
background: transparent;
border: 0;
- color: $lighter-text-color;
+ color: $inverted-text-color;
font-weight: 700;
font-size: 11px;
padding: 0 6px;
@@ -769,7 +768,7 @@
&.light {
.status__relative-time {
- color: $lighter-text-color;
+ color: $light-text-color;
}
.status__display-name {
@@ -782,7 +781,7 @@
}
span {
- color: $lighter-text-color;
+ color: $light-text-color;
}
}
@@ -816,13 +815,13 @@
}
.status__relative-time {
- color: $darker-text-color;
+ color: $dark-text-color;
float: right;
font-size: 14px;
}
.status__display-name {
- color: $darker-text-color;
+ color: $dark-text-color;
}
.status__info .status__display-name {
@@ -873,14 +872,14 @@
.status__prepend {
margin-left: 68px;
- color: $darker-text-color;
+ color: $dark-text-color;
padding: 8px 0;
padding-bottom: 2px;
font-size: 14px;
position: relative;
.status__display-name strong {
- color: $darker-text-color;
+ color: $dark-text-color;
}
> span {
@@ -942,7 +941,7 @@
.detailed-status__meta {
margin-top: 15px;
- color: $darker-text-color;
+ color: $dark-text-color;
font-size: 14px;
line-height: 18px;
}
@@ -1006,6 +1005,15 @@
padding: 10px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
+ &.compact {
+ padding: 0;
+ border-bottom: 0;
+
+ .account__avatar-wrapper {
+ margin-left: 0;
+ }
+ }
+
.account__display-name {
flex: 1 1 auto;
display: block;
@@ -1029,7 +1037,6 @@
.account__avatar {
@include avatar-radius();
position: relative;
- cursor: pointer;
&-inline {
display: inline-block;
@@ -1038,6 +1045,10 @@
}
}
+a .account__avatar {
+ cursor: pointer;
+}
+
.account__avatar-overlay {
@include avatar-size(48px);
@@ -1079,7 +1090,7 @@
}
.account__header__username {
- color: $darker-text-color;
+ color: $secondary-text-color;
}
}
@@ -1089,7 +1100,7 @@
}
.account__header__content {
- color: $darker-text-color;
+ color: $secondary-text-color;
}
.account__header__display-name {
@@ -1117,7 +1128,7 @@
.account__disclaimer {
padding: 10px;
border-top: 1px solid lighten($ui-base-color, 8%);
- color: $darker-text-color;
+ color: $dark-text-color;
strong {
font-weight: 500;
@@ -1286,7 +1297,7 @@
.status__display-name,
.reply-indicator__display-name,
.detailed-status__display-name,
-.account__display-name {
+a.account__display-name {
&:hover strong {
text-decoration: underline;
}
@@ -1304,7 +1315,7 @@
}
.detailed-status__display-name {
- color: $darker-text-color;
+ color: $secondary-text-color;
display: block;
line-height: 24px;
margin-bottom: 15px;
@@ -1339,11 +1350,11 @@
.muted {
.status__content p,
.status__content a {
- color: $darker-text-color;
+ color: $dark-text-color;
}
.status__display-name strong {
- color: $darker-text-color;
+ color: $dark-text-color;
}
.status__avatar {
@@ -1351,11 +1362,11 @@
}
a.status__content__spoiler-link {
- background: $darker-text-color;
- color: lighten($ui-base-color, 4%);
+ background: $ui-base-lighter-color;
+ color: $inverted-text-color;
&:hover {
- background: transparentize($darker-text-color, 0.07);
+ background: lighten($ui-base-lighter-color, 7%);
text-decoration: none;
}
}
@@ -1366,7 +1377,7 @@
padding: 8px 0;
padding-bottom: 0;
cursor: default;
- color: $ui-primary-color;
+ color: $darker-text-color;
font-size: 15px;
position: relative;
@@ -1477,7 +1488,7 @@
color: $darker-text-color;
strong {
- color: $primary-text-color;
+ color: $secondary-text-color;
}
a {
@@ -1591,7 +1602,7 @@
&:hover,
&:active {
background: $ui-highlight-color;
- color: $primary-text-color;
+ color: $secondary-text-color;
outline: 0;
}
}
@@ -1644,7 +1655,7 @@
&:hover {
background: $ui-highlight-color;
- color: $primary-text-color;
+ color: $secondary-text-color;
}
}
}
@@ -1656,7 +1667,7 @@
.static-content {
padding: 10px;
padding-top: 20px;
- color: $darker-text-color;
+ color: $dark-text-color;
h1 {
font-size: 16px;
@@ -1743,7 +1754,7 @@
display: block;
flex: 1 1 auto;
padding: 15px 5px 13px;
- color: $ui-primary-color;
+ color: $darker-text-color;
text-decoration: none;
text-align: center;
font-size: 16px;
@@ -2155,7 +2166,7 @@
.column-subheading {
background: $ui-base-color;
- color: $darker-text-color;
+ color: $dark-text-color;
padding: 8px 20px;
font-size: 12px;
font-weight: 500;
@@ -2178,11 +2189,11 @@
flex: 1 0 auto;
p {
- color: $darker-text-color;
+ color: $secondary-text-color;
}
a {
- color: opacify($darker-text-color, 0.07);
+ color: $dark-text-color;
}
}
@@ -2263,7 +2274,7 @@
font-size: 14px;
border: 1px solid lighten($ui-base-color, 8%);
border-radius: 4px;
- color: $darker-text-color;
+ color: $dark-text-color;
margin-top: 14px;
text-decoration: none;
overflow: hidden;
@@ -2343,7 +2354,7 @@ a.status-card {
display: block;
font-weight: 500;
margin-bottom: 5px;
- color: $ui-primary-color;
+ color: $darker-text-color;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -2357,7 +2368,7 @@ a.status-card {
}
.status-card__description {
- color: $ui-primary-color;
+ color: $darker-text-color;
}
.status-card__host {
@@ -2401,7 +2412,7 @@ a.status-card {
.load-more {
display: block;
- color: $darker-text-color;
+ color: $dark-text-color;
background-color: transparent;
border: 0;
font-size: inherit;
@@ -2425,7 +2436,7 @@ a.status-card {
text-align: center;
font-size: 16px;
font-weight: 500;
- color: opacify($darker-text-color, 0.07);
+ color: $dark-text-color;
background: $ui-base-color;
cursor: default;
display: flex;
@@ -2465,7 +2476,7 @@ a.status-card {
strong {
display: block;
margin-bottom: 10px;
- color: $darker-text-color;
+ color: $dark-text-color;
}
span {
@@ -2553,13 +2564,13 @@ a.status-card {
.column-header__button {
background: lighten($ui-base-color, 4%);
border: 0;
- color: $ui-primary-color;
+ color: $darker-text-color;
cursor: pointer;
font-size: 16px;
padding: 0 15px;
&:hover {
- color: lighten($ui-primary-color, 7%);
+ color: lighten($darker-text-color, 7%);
}
&.active {
@@ -2640,7 +2651,7 @@ a.status-card {
}
.loading-indicator {
- color: $darker-text-color;
+ color: $dark-text-color;
font-size: 12px;
font-weight: 400;
text-transform: uppercase;
@@ -2737,7 +2748,7 @@ a.status-card {
&:active,
&:focus {
padding: 0;
- color: transparentize($darker-text-color, 0.07);
+ color: lighten($darker-text-color, 8%);
}
}
@@ -2861,7 +2872,7 @@ a.status-card {
.empty-column-indicator,
.error-column {
- color: $darker-text-color;
+ color: $dark-text-color;
background: $ui-base-color;
text-align: center;
padding: 20px;
@@ -3063,7 +3074,7 @@ a.status-card {
display: flex;
align-items: center;
justify-content: center;
- color: $primary-text-color;
+ color: $secondary-text-color;
font-size: 18px;
font-weight: 500;
border: 2px dashed $ui-base-lighter-color;
@@ -3161,7 +3172,7 @@ a.status-card {
}
.privacy-dropdown__option {
- color: $lighter-text-color;
+ color: $inverted-text-color;
padding: 10px;
cursor: pointer;
display: flex;
@@ -3283,7 +3294,7 @@ a.status-card {
font-size: 18px;
width: 18px;
height: 18px;
- color: $ui-secondary-color;
+ color: $secondary-text-color;
cursor: default;
pointer-events: none;
@@ -3319,7 +3330,7 @@ a.status-card {
}
.search-results__header {
- color: $darker-text-color;
+ color: $dark-text-color;
background: lighten($ui-base-color, 2%);
border-bottom: 1px solid darken($ui-base-color, 4%);
padding: 15px 10px;
@@ -3367,13 +3378,13 @@ a.status-card {
.search-results__hashtag {
display: block;
padding: 10px;
- color: darken($primary-text-color, 4%);
+ color: $secondary-text-color;
text-decoration: none;
&:hover,
&:active,
&:focus {
- color: $primary-text-color;
+ color: lighten($secondary-text-color, 4%);
text-decoration: underline;
}
}
@@ -3638,7 +3649,7 @@ a.status-card {
&:hover,
&:focus,
&:active {
- color: transparentize($lighter-text-color, 0.04);
+ color: darken($lighter-text-color, 4%);
background-color: darken($ui-secondary-color, 16%);
}
@@ -3732,7 +3743,7 @@ a.status-card {
strong {
font-weight: 500;
background: $ui-base-color;
- color: $primary-text-color;
+ color: $secondary-text-color;
border-radius: 4px;
font-size: 14px;
padding: 3px 6px;
@@ -3792,7 +3803,7 @@ a.status-card {
&__case {
background: $ui-base-color;
- color: $primary-text-color;
+ color: $secondary-text-color;
font-weight: 500;
padding: 10px;
border-radius: 4px;
@@ -3809,7 +3820,7 @@ a.status-card {
.figure {
background: darken($ui-base-color, 8%);
- color: $darker-text-color;
+ color: $secondary-text-color;
margin-bottom: 20px;
border-radius: 4px;
padding: 10px;
@@ -3921,7 +3932,7 @@ a.status-card {
}
.status__content__spoiler-link {
- color: lighten($ui-secondary-color, 8%);
+ color: lighten($secondary-text-color, 8%);
}
}
@@ -4026,6 +4037,10 @@ a.status-card {
overflow-y: auto;
overflow-x: hidden;
+ .status__content a {
+ color: $highlight-text-color;
+ }
+
@media screen and (max-width: 480px) {
max-height: 10vh;
}
@@ -4151,7 +4166,7 @@ a.status-card {
&:hover,
&:focus,
&:active {
- color: transparentize($lighter-text-color, 0.04);
+ color: darken($lighter-text-color, 4%);
}
}
}
@@ -4232,7 +4247,7 @@ a.status-card {
&__icon {
flex: 0 0 auto;
- color: $darker-text-color;
+ color: $dark-text-color;
padding: 8px 18px;
cursor: default;
border-right: 1px solid lighten($ui-base-color, 8%);
@@ -4262,7 +4277,7 @@ a.status-card {
a {
text-decoration: none;
- color: $darker-text-color;
+ color: $dark-text-color;
font-weight: 500;
&:hover {
@@ -4281,7 +4296,7 @@ a.status-card {
}
.fa {
- color: $darker-text-color;
+ color: $dark-text-color;
}
}
}
@@ -4317,7 +4332,7 @@ a.status-card {
cursor: zoom-in;
display: block;
text-decoration: none;
- color: $ui-secondary-color;
+ color: $secondary-text-color;
line-height: 0;
&,
@@ -4431,6 +4446,8 @@ a.status-card {
video {
max-width: 100% !important;
max-height: 100% !important;
+ width: 100% !important;
+ height: 100% !important;
}
}
@@ -4488,7 +4505,7 @@ a.status-card {
&:hover,
&:active,
&:focus {
- color: transparentize($darker-text-color, 0.07);
+ color: lighten($darker-text-color, 7%);
}
}
@@ -4693,7 +4710,7 @@ a.status-card {
&:active,
&:focus {
outline: 0;
- color: transparentize($darker-text-color, 0.07);
+ color: $secondary-text-color;
&::before {
content: "";
@@ -4733,7 +4750,7 @@ a.status-card {
position: relative;
&.active {
- color: transparentize($darker-text-color, 0.07);
+ color: $secondary-text-color;
&::before,
&::after {
@@ -4768,12 +4785,12 @@ a.status-card {
padding: 10px 14px;
padding-bottom: 14px;
margin-top: 10px;
- color: $lighter-text-color;
+ color: $light-text-color;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
h4 {
text-transform: uppercase;
- color: $lighter-text-color;
+ color: $light-text-color;
font-size: 13px;
font-weight: 500;
margin-bottom: 10px;
@@ -4805,7 +4822,7 @@ noscript {
div {
font-size: 14px;
margin: 30px auto;
- color: $primary-text-color;
+ color: $secondary-text-color;
max-width: 400px;
a {
@@ -4958,7 +4975,7 @@ noscript {
&__message {
position: relative;
margin-left: 58px;
- color: $darker-text-color;
+ color: $dark-text-color;
padding: 8px 0;
padding-top: 0;
padding-bottom: 4px;
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index 8df2902d23..9d5ab66a48 100644
--- a/app/javascript/styles/mastodon/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
@@ -100,7 +100,7 @@
.name {
flex: 1 1 auto;
- color: $darker-text-color;
+ color: $secondary-text-color;
width: calc(100% - 88px);
.username {
diff --git a/app/javascript/styles/mastodon/emoji_picker.scss b/app/javascript/styles/mastodon/emoji_picker.scss
index 3620a6f54d..cf9547586d 100644
--- a/app/javascript/styles/mastodon/emoji_picker.scss
+++ b/app/javascript/styles/mastodon/emoji_picker.scss
@@ -50,7 +50,7 @@
cursor: pointer;
&:hover {
- color: opacify($lighter-text-color, 0.04);
+ color: darken($lighter-text-color, 4%);
}
}
@@ -184,7 +184,7 @@
font-size: 14px;
text-align: center;
padding-top: 70px;
- color: $lighter-text-color;
+ color: $light-text-color;
.emoji-mart-category-label {
display: none;
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 3a3b4c3269..f978901870 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -248,7 +248,7 @@ code {
}
&:required:valid {
- border-bottom-color: lighten($error-red, 12%);
+ border-bottom-color: $valid-value-color;
}
&:active,
@@ -266,7 +266,7 @@ code {
input[type=text],
input[type=email],
input[type=password] {
- border-bottom-color: lighten($error-red, 12%);
+ border-bottom-color: $valid-value-color;
}
.error {
@@ -356,7 +356,7 @@ code {
padding: 7px 4px;
padding-bottom: 9px;
font-size: 16px;
- color: $darker-text-color;
+ color: $dark-text-color;
font-family: inherit;
pointer-events: none;
cursor: default;
@@ -446,7 +446,7 @@ code {
}
strong {
- color: $primary-text-color;
+ color: $secondary-text-color;
font-weight: 500;
@each $lang in $cjk-langs {
@@ -483,7 +483,7 @@ code {
.qr-alternative {
margin-bottom: 20px;
- color: $darker-text-color;
+ color: $secondary-text-color;
flex: 150px;
samp {
diff --git a/app/javascript/styles/mastodon/landing_strip.scss b/app/javascript/styles/mastodon/landing_strip.scss
index 651c06ced7..86614b89bc 100644
--- a/app/javascript/styles/mastodon/landing_strip.scss
+++ b/app/javascript/styles/mastodon/landing_strip.scss
@@ -45,7 +45,7 @@
padding: 14px;
border-radius: 4px;
background: rgba(darken($ui-base-color, 7%), 0.8);
- color: $darker-text-color;
+ color: $secondary-text-color;
font-weight: 400;
margin-bottom: 20px;
diff --git a/app/javascript/styles/mastodon/stream_entries.scss b/app/javascript/styles/mastodon/stream_entries.scss
index c39163ba8d..281cbaf83a 100644
--- a/app/javascript/styles/mastodon/stream_entries.scss
+++ b/app/javascript/styles/mastodon/stream_entries.scss
@@ -93,7 +93,7 @@
display: block;
max-width: 100%;
padding-right: 25px;
- color: $lighter-text-color;
+ color: $inverted-text-color;
}
.status__avatar {
@@ -134,7 +134,7 @@
span {
font-size: 14px;
- color: $inverted-text-color;
+ color: $light-text-color;
}
}
@@ -191,7 +191,7 @@
span {
font-size: 14px;
- color: $lighter-text-color;
+ color: $light-text-color;
}
}
}
@@ -225,7 +225,7 @@
.detailed-status__meta {
margin-top: 15px;
- color: $lighter-text-color;
+ color: $light-text-color;
font-size: 14px;
line-height: 18px;
@@ -270,7 +270,7 @@
padding-left: (48px + 14px * 2);
padding-bottom: 0;
margin-bottom: -4px;
- color: $lighter-text-color;
+ color: $light-text-color;
font-size: 14px;
position: relative;
@@ -280,7 +280,7 @@
}
.status__display-name.muted strong {
- color: $lighter-text-color;
+ color: $light-text-color;
}
}
@@ -293,7 +293,7 @@
}
.more {
- color: $classic-primary-color;
+ color: $darker-text-color;
display: block;
padding: 14px;
text-align: center;
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index c12d84f1c0..fa876e6031 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -11,6 +11,7 @@
vertical-align: top;
border-top: 1px solid $ui-base-color;
text-align: left;
+ background: darken($ui-base-color, 4%);
}
& > thead > tr > th {
@@ -48,9 +49,38 @@
}
}
- &.inline-table > tbody > tr:nth-child(odd) > td,
- &.inline-table > tbody > tr:nth-child(odd) > th {
- background: transparent;
+ &.inline-table {
+ & > tbody > tr:nth-child(odd) {
+ & > td,
+ & > th {
+ background: transparent;
+ }
+ }
+
+ & > tbody > tr:first-child {
+ & > td,
+ & > th {
+ border-top: 0;
+ }
+ }
+ }
+
+ &.batch-table {
+ & > thead > tr > th {
+ background: $ui-base-color;
+ border-top: 1px solid darken($ui-base-color, 8%);
+ border-bottom: 1px solid darken($ui-base-color, 8%);
+
+ &:first-child {
+ border-radius: 4px 0 0;
+ border-left: 1px solid darken($ui-base-color, 8%);
+ }
+
+ &:last-child {
+ border-radius: 0 4px 0 0;
+ border-right: 1px solid darken($ui-base-color, 8%);
+ }
+ }
}
}
@@ -63,6 +93,13 @@ samp {
font-family: 'mastodon-font-monospace', monospace;
}
+button.table-action-link {
+ background: transparent;
+ border: 0;
+ font: inherit;
+}
+
+button.table-action-link,
a.table-action-link {
text-decoration: none;
display: inline-block;
@@ -79,4 +116,77 @@ a.table-action-link {
font-weight: 400;
margin-right: 5px;
}
+
+ &:first-child {
+ padding-left: 0;
+ }
+}
+
+.batch-table {
+ &__toolbar,
+ &__row {
+ display: flex;
+
+ &__select {
+ box-sizing: border-box;
+ padding: 8px 16px;
+ cursor: pointer;
+ min-height: 100%;
+
+ input {
+ margin-top: 8px;
+ }
+ }
+
+ &__actions,
+ &__content {
+ padding: 8px 0;
+ padding-right: 16px;
+ flex: 1 1 auto;
+ }
+ }
+
+ &__toolbar {
+ border: 1px solid darken($ui-base-color, 8%);
+ background: $ui-base-color;
+ border-radius: 4px 0 0;
+ height: 47px;
+ align-items: center;
+
+ &__actions {
+ text-align: right;
+ padding-right: 16px - 5px;
+ }
+ }
+
+ &__row {
+ border: 1px solid darken($ui-base-color, 8%);
+ border-top: 0;
+ background: darken($ui-base-color, 4%);
+
+ &:hover {
+ background: darken($ui-base-color, 2%);
+ }
+
+ &:nth-child(even) {
+ background: $ui-base-color;
+
+ &:hover {
+ background: lighten($ui-base-color, 2%);
+ }
+ }
+
+ &__content {
+ padding-top: 12px;
+ padding-bottom: 16px;
+ }
+ }
+
+ .status__content {
+ padding-top: 0;
+
+ strong {
+ font-weight: 700;
+ }
+ }
}
diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss
index dc4e72a2ea..cbefe35b4d 100644
--- a/app/javascript/styles/mastodon/variables.scss
+++ b/app/javascript/styles/mastodon/variables.scss
@@ -17,12 +17,6 @@ $base-shadow-color: $black !default;
$base-overlay-background: $black !default;
$base-border-color: $white !default;
$simple-background-color: $white !default;
-$primary-text-color: $white !default;
-$darker-text-color: rgba($primary-text-color, 0.7) !default;
-$highlight-text-color: $classic-highlight-color !default;
-$inverted-text-color: $black !default;
-$lighter-text-color: rgba($inverted-text-color, 0.7) !default;
-$action-button-color: #8d9ac2;
$valid-value-color: $success-green !default;
$error-value-color: $error-red !default;
@@ -31,7 +25,19 @@ $ui-base-color: $classic-base-color !default; // Darkest
$ui-base-lighter-color: lighten($ui-base-color, 26%) !default; // Lighter darkest
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
-$ui-highlight-color: #2b5fd9;
+$ui-highlight-color: $classic-highlight-color !default;
+
+// Variables for texts
+$primary-text-color: $white !default;
+$darker-text-color: $ui-primary-color !default;
+$dark-text-color: $ui-base-lighter-color !default;
+$secondary-text-color: $ui-secondary-color !default;
+$highlight-text-color: $ui-highlight-color !default;
+$action-button-color: $ui-base-lighter-color !default;
+// For texts on inverted backgrounds
+$inverted-text-color: $ui-base-color !default;
+$lighter-text-color: $ui-base-lighter-color !default;
+$light-text-color: $ui-primary-color !default;
// Language codes that uses CJK fonts
$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 9b00f0f522..5b97a6208d 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -80,7 +80,7 @@ class ActivityPub::Activity
# Only continue if the status is supposed to have
# arrived in real-time
- return unless @options[:override_timestamps] || status.within_realtime_window?
+ return unless status.within_realtime_window?
distribute_to_followers(status)
end
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index c8a3581950..8840a450cb 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -15,7 +15,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
account: @account,
reblog: original_status,
uri: @json['id'],
- created_at: @options[:override_timestamps] ? nil : @json['published'],
+ created_at: @json['published'],
visibility: original_status.visibility
)
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 45c0e91cb0..edee2691ff 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -47,7 +47,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
text: text_from_content || '',
language: detected_language,
spoiler_text: @object['summary'] || '',
- created_at: @options[:override_timestamps] ? nil : @object['published'],
+ created_at: @object['published'],
reply: @object['inReplyTo'].present?,
sensitive: @object['sensitive'] || false,
visibility: visibility_from_audience,
@@ -61,12 +61,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
return if @object['tag'].nil?
as_array(@object['tag']).each do |tag|
- case tag['type']
- when 'Hashtag'
+ if equals_or_includes?(tag['type'], 'Hashtag')
process_hashtag tag, status
- when 'Mention'
+ elsif equals_or_includes?(tag['type'], 'Mention')
process_mention tag, status
- when 'Emoji'
+ elsif equals_or_includes?(tag['type'], 'Emoji')
process_emoji tag, status
end
end
@@ -235,11 +234,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
def supported_object_type?
- SUPPORTED_TYPES.include?(@object['type'])
+ equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
end
def converted_object_type?
- CONVERTED_TYPES.include?(@object['type'])
+ equals_or_includes_any?(@object['type'], CONVERTED_TYPES)
end
def skip_download?
diff --git a/app/lib/activitypub/activity/update.rb b/app/lib/activitypub/activity/update.rb
index 0134b4015f..aa5907f033 100644
--- a/app/lib/activitypub/activity/update.rb
+++ b/app/lib/activitypub/activity/update.rb
@@ -1,11 +1,10 @@
# frozen_string_literal: true
class ActivityPub::Activity::Update < ActivityPub::Activity
+ SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
+
def perform
- case @object['type']
- when 'Person'
- update_account
- end
+ update_account if equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
end
private
diff --git a/app/lib/entity_cache.rb b/app/lib/entity_cache.rb
new file mode 100644
index 0000000000..2aa37389ca
--- /dev/null
+++ b/app/lib/entity_cache.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'singleton'
+
+class EntityCache
+ include Singleton
+
+ MAX_EXPIRATION = 7.days.freeze
+
+ def mention(username, domain)
+ Rails.cache.fetch(to_key(:mention, username, domain), expires_in: MAX_EXPIRATION) { Account.select(:username, :domain, :url).find_remote(username, domain) }
+ end
+
+ def emoji(shortcodes, domain)
+ shortcodes = [shortcodes] unless shortcodes.is_a?(Array)
+ cached = Rails.cache.read_multi(*shortcodes.map { |shortcode| to_key(:emoji, shortcode, domain) })
+ uncached_ids = []
+
+ shortcodes.each do |shortcode|
+ uncached_ids << shortcode unless cached.key?(to_key(:emoji, shortcode, domain))
+ end
+
+ unless uncached_ids.empty?
+ uncached = CustomEmoji.where(shortcode: shortcodes, domain: domain, disabled: false).map { |item| [item.shortcode, item] }.to_h
+ uncached.each_value { |item| Rails.cache.write(to_key(:emoji, item.shortcode, domain), item, expires_in: MAX_EXPIRATION) }
+ end
+
+ shortcodes.map { |shortcode| cached[to_key(:emoji, shortcode, domain)] || uncached[shortcode] }.compact
+ end
+
+ def to_key(type, *ids)
+ "#{type}:#{ids.compact.map(&:downcase).join(':')}"
+ end
+end
diff --git a/app/lib/exceptions.rb b/app/lib/exceptions.rb
index e88e98eae5..01346bfe5a 100644
--- a/app/lib/exceptions.rb
+++ b/app/lib/exceptions.rb
@@ -6,6 +6,7 @@ module Mastodon
class ValidationError < Error; end
class HostValidationError < ValidationError; end
class LengthValidationError < ValidationError; end
+ class DimensionsValidationError < ValidationError; end
class RaceConditionError < Error; end
class UnexpectedResponseError < Error
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 700fd61c46..3a2dcac68e 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -145,10 +145,14 @@ class FeedManager
redis.exists("subscribed:#{timeline_id}")
end
+ def blocks_or_mutes?(receiver_id, account_ids, context)
+ Block.where(account_id: receiver_id, target_account_id: account_ids).any? ||
+ (context == :home ? Mute.where(account_id: receiver_id, target_account_id: account_ids).any? : Mute.where(account_id: receiver_id, target_account_id: account_ids, hide_notifications: true).any?)
+ end
+
def filter_from_home?(status, receiver_id)
return false if receiver_id == status.account_id
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
-
return true if keyword_filter?(status, receiver_id)
check_for_mutes = [status.account_id]
@@ -158,9 +162,10 @@ class FeedManager
return true if Mute.where(account_id: receiver_id, target_account_id: check_for_mutes).any?
check_for_blocks = status.mentions.pluck(:account_id)
+ check_for_blocks.concat([status.account_id])
check_for_blocks.concat([status.reblog.account_id]) if status.reblog?
- return true if Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any?
+ return true if blocks_or_mutes?(receiver_id, check_for_blocks, :home)
if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply
should_filter = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists? # and I'm not following the person it's a reply to
@@ -184,11 +189,13 @@ class FeedManager
def filter_from_mentions?(status, receiver_id)
return true if receiver_id == status.account_id
- check_for_blocks = [status.account_id]
- check_for_blocks.concat(status.mentions.pluck(:account_id))
+ # This filter is called from NotifyService, but already after the sender of
+ # the notification has been checked for mute/block. Therefore, it's not
+ # necessary to check the author of the toot for mute/block again
+ check_for_blocks = status.mentions.pluck(:account_id)
check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil?
- should_filter = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any? # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked
+ should_filter = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted)
should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them
should_filter ||= keyword_filter?(status, receiver_id) # or if the mention contains a muted keyword
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 4124f1660c..050c651ee9 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -52,12 +52,8 @@ class Formatter
end
def simplified_format(account, **options)
- html = if account.local?
- linkify(account.note)
- else
- reformat(account.note)
- end
- html = encode_custom_emojis(html, CustomEmoji.from_text(account.note, account.domain)) if options[:custom_emojify]
+ html = account.local? ? linkify(account.note) : reformat(account.note)
+ html = encode_custom_emojis(html, account.emojis) if options[:custom_emojify]
html.html_safe # rubocop:disable Rails/OutputSafety
end
@@ -211,7 +207,7 @@ class Formatter
username, domain = acct.split('@')
domain = nil if TagManager.instance.local_domain?(domain)
- account = Account.find_remote(username, domain)
+ account = EntityCache.instance.mention(username, domain)
account ? mention_html(account) : "@#{acct}"
end
diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb
index 6235127b20..a24a0093c3 100644
--- a/app/lib/ostatus/activity/creation.rb
+++ b/app/lib/ostatus/activity/creation.rb
@@ -39,7 +39,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
reblog: cached_reblog,
text: content,
spoiler_text: content_warning,
- created_at: @options[:override_timestamps] ? nil : published,
+ created_at: published,
reply: thread?,
language: content_language,
visibility: visibility_scope,
@@ -61,7 +61,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution"
LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text?
- DistributionWorker.perform_async(status.id) if @options[:override_timestamps] || status.within_realtime_window?
+ DistributionWorker.perform_async(status.id) if status.within_realtime_window?
status
end
diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb
index 055b4649c4..7c66f2066e 100644
--- a/app/lib/ostatus/atom_serializer.rb
+++ b/app/lib/ostatus/atom_serializer.rb
@@ -364,8 +364,6 @@ class OStatus::AtomSerializer
append_element(entry, 'category', nil, term: tag.name)
end
- append_element(entry, 'category', nil, term: 'nsfw') if status.sensitive?
-
status.media_attachments.each do |media|
append_element(entry, 'link', nil, rel: :enclosure, type: media.file_content_type, length: media.file_file_size, href: full_asset_url(media.file.url(:original, false)))
end
diff --git a/app/lib/provider_discovery.rb b/app/lib/provider_discovery.rb
deleted file mode 100644
index 3bec7211bd..0000000000
--- a/app/lib/provider_discovery.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-class ProviderDiscovery < OEmbed::ProviderDiscovery
- class << self
- def get(url, **options)
- provider = discover_provider(url, options)
-
- options.delete(:html)
-
- provider.get(url, options)
- end
-
- def discover_provider(url, **options)
- format = options[:format]
-
- html = if options[:html]
- Nokogiri::HTML(options[:html])
- else
- Request.new(:get, url).perform do |res|
- raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
- Nokogiri::HTML(res.body_with_limit)
- end
- end
-
- if format.nil? || format == :json
- provider_endpoint ||= html.at_xpath('//link[@type="application/json+oembed"]')&.attribute('href')&.value
- format ||= :json if provider_endpoint
- end
-
- if format.nil? || format == :xml
- provider_endpoint ||= html.at_xpath('//link[@type="text/xml+oembed"]')&.attribute('href')&.value
- format ||= :xml if provider_endpoint
- end
-
- raise OEmbed::NotFound, url if provider_endpoint.nil?
- begin
- provider_endpoint = Addressable::URI.parse(provider_endpoint)
- provider_endpoint.query = nil
- provider_endpoint = provider_endpoint.to_s
- rescue Addressable::URI::InvalidURIError
- raise OEmbed::NotFound, url
- end
-
- OEmbed::Provider.new(provider_endpoint, format)
- end
- end
-end
diff --git a/app/lib/request.rb b/app/lib/request.rb
index dca93a6e95..00f94dacf5 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -9,11 +9,15 @@ class Request
include RoutingHelper
def initialize(verb, url, **options)
+ raise ArgumentError if url.blank?
+
@verb = verb
@url = Addressable::URI.parse(url).normalize
- @options = options.merge(socket_class: Socket)
+ @options = options.merge(use_proxy? ? Rails.configuration.x.http_client_proxy : { socket_class: Socket })
@headers = {}
+ raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service?
+
set_common_headers!
set_digest! if options.key?(:body)
end
@@ -99,6 +103,14 @@ class Request
@http_client ||= HTTP.timeout(:per_operation, timeout).follow(max_hops: 2)
end
+ def use_proxy?
+ Rails.configuration.x.http_client_proxy.present?
+ end
+
+ def block_hidden_service?
+ !Rails.configuration.x.access_to_hidden_service && /\.(onion|i2p)$/.match(@url.host)
+ end
+
module ClientLimit
def body_with_limit(limit = 1.megabyte)
raise Mastodon::LengthValidationError if content_length.present? && content_length > limit
@@ -129,6 +141,7 @@ class Request
class Socket < TCPSocket
class << self
def open(host, *args)
+ return super host, *args if thru_hidden_service? host
outer_e = nil
Addrinfo.foreach(host, nil, nil, :SOCK_STREAM) do |address|
begin
@@ -142,6 +155,10 @@ class Request
end
alias new open
+
+ def thru_hidden_service?(host)
+ Rails.configuration.x.hidden_service_via_transparent_proxy && /\.(onion|i2p)$/.match(host)
+ end
end
end
diff --git a/app/lib/rss_builder.rb b/app/lib/rss_builder.rb
new file mode 100644
index 0000000000..63ddba2e8e
--- /dev/null
+++ b/app/lib/rss_builder.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+class RSSBuilder
+ class ItemBuilder
+ def initialize
+ @item = Ox::Element.new('item')
+ end
+
+ def title(str)
+ @item << (Ox::Element.new('title') << str)
+
+ self
+ end
+
+ def link(str)
+ @item << Ox::Element.new('guid').tap do |guid|
+ guid['isPermalink'] = 'true'
+ guid << str
+ end
+
+ @item << (Ox::Element.new('link') << str)
+
+ self
+ end
+
+ def pub_date(date)
+ @item << (Ox::Element.new('pubDate') << date.to_formatted_s(:rfc822))
+
+ self
+ end
+
+ def description(str)
+ @item << (Ox::Element.new('description') << str)
+
+ self
+ end
+
+ def enclosure(url, type, size)
+ @item << Ox::Element.new('enclosure').tap do |enclosure|
+ enclosure['url'] = url
+ enclosure['length'] = size
+ enclosure['type'] = type
+ end
+
+ self
+ end
+
+ def to_element
+ @item
+ end
+ end
+
+ def initialize
+ @document = Ox::Document.new(version: '1.0')
+ @channel = Ox::Element.new('channel')
+
+ @document << (rss << @channel)
+ end
+
+ def title(str)
+ @channel << (Ox::Element.new('title') << str)
+
+ self
+ end
+
+ def link(str)
+ @channel << (Ox::Element.new('link') << str)
+
+ self
+ end
+
+ def image(str)
+ @channel << Ox::Element.new('image').tap do |image|
+ image << (Ox::Element.new('url') << str)
+ image << (Ox::Element.new('title') << '')
+ image << (Ox::Element.new('link') << '')
+ end
+
+ @channel << (Ox::Element.new('webfeeds:icon') << str)
+
+ self
+ end
+
+ def cover(str)
+ @channel << Ox::Element.new('webfeeds:cover').tap do |cover|
+ cover['image'] = str
+ end
+
+ self
+ end
+
+ def logo(str)
+ @channel << (Ox::Element.new('webfeeds:logo') << str)
+
+ self
+ end
+
+ def accent_color(str)
+ @channel << (Ox::Element.new('webfeeds:accentColor') << str)
+
+ self
+ end
+
+ def description(str)
+ @channel << (Ox::Element.new('description') << str)
+
+ self
+ end
+
+ def item
+ @channel << ItemBuilder.new.tap do |item|
+ yield item
+ end.to_element
+
+ self
+ end
+
+ def to_xml
+ ('' + Ox.dump(@document, effort: :tolerant)).force_encoding('UTF-8')
+ end
+
+ private
+
+ def rss
+ Ox::Element.new('rss').tap do |rss|
+ rss['version'] = '2.0'
+ rss['xmlns:webfeeds'] = 'http://webfeeds.org/rss/1.0'
+ end
+ end
+end
diff --git a/app/lib/status_filter.rb b/app/lib/status_filter.rb
index 41d4381e56..b6c80b801c 100644
--- a/app/lib/status_filter.rb
+++ b/app/lib/status_filter.rb
@@ -3,9 +3,10 @@
class StatusFilter
attr_reader :status, :account
- def initialize(status, account)
- @status = status
- @account = account
+ def initialize(status, account, preloaded_relations = {})
+ @status = status
+ @account = account
+ @preloaded_relations = preloaded_relations
end
def filtered?
@@ -24,15 +25,15 @@ class StatusFilter
end
def blocking_account?
- account.blocking? status.account_id
+ @preloaded_relations[:blocking] ? @preloaded_relations[:blocking][status.account_id] : account.blocking?(status.account_id)
end
def blocking_domain?
- account.domain_blocking? status.account_domain
+ @preloaded_relations[:domain_blocking_by_domain] ? @preloaded_relations[:domain_blocking_by_domain][status.account_domain] : account.domain_blocking?(status.account_domain)
end
def muting_account?
- account.muting? status.account_id
+ @preloaded_relations[:muting] ? @preloaded_relations[:muting][status.account_id] : account.muting?(status.account_id)
end
def silenced_account?
@@ -44,7 +45,7 @@ class StatusFilter
end
def account_following_status_account?
- account&.following? status.account_id
+ @preloaded_relations[:following] ? @preloaded_relations[:following][status.account_id] : account&.following?(status.account_id)
end
def blocked_by_policy?
@@ -52,6 +53,6 @@ class StatusFilter
end
def policy_allows_show?
- StatusPolicy.new(account, status).show?
+ StatusPolicy.new(account, status, @preloaded_relations).show?
end
end
diff --git a/app/models/account.rb b/app/models/account.rb
index db21711021..c1ce1e99e8 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -3,7 +3,7 @@
#
# Table name: accounts
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# username :string default(""), not null
# domain :string
# secret :string default(""), not null
@@ -42,7 +42,7 @@
# followers_url :string default(""), not null
# protocol :integer default("ostatus"), not null
# memorial :boolean default(FALSE), not null
-# moved_to_account_id :integer
+# moved_to_account_id :bigint(8)
# featured_collection_url :string
# fields :jsonb
#
@@ -120,6 +120,7 @@ class Account < ApplicationRecord
scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) }
scope :silenced, -> { where(silenced: true) }
scope :suspended, -> { where(suspended: true) }
+ scope :without_suspended, -> { where(suspended: false) }
scope :recent, -> { reorder(id: :desc) }
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
@@ -275,6 +276,10 @@ class Account < ApplicationRecord
@value = attr['value']
@errors = {}
end
+
+ def to_h
+ { name: @name, value: @value }
+ end
end
class << self
@@ -393,7 +398,7 @@ class Account < ApplicationRecord
end
def emojis
- CustomEmoji.from_text(note, domain)
+ @emojis ||= CustomEmoji.from_text(note, domain)
end
before_create :generate_keys
@@ -408,9 +413,9 @@ class Account < ApplicationRecord
end
def generate_keys
- return unless local?
+ return unless local? && !Rails.env.test?
- keypair = OpenSSL::PKey::RSA.new(Rails.env.test? ? 512 : 2048)
+ keypair = OpenSSL::PKey::RSA.new(2048)
self.private_key = keypair.to_pem
self.public_key = keypair.public_key.to_pem
end
diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb
index bc00b4f32b..e352000c3a 100644
--- a/app/models/account_domain_block.rb
+++ b/app/models/account_domain_block.rb
@@ -3,11 +3,11 @@
#
# Table name: account_domain_blocks
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# domain :string
# created_at :datetime not null
# updated_at :datetime not null
-# account_id :integer
+# account_id :bigint(8)
#
class AccountDomainBlock < ApplicationRecord
diff --git a/app/models/account_moderation_note.rb b/app/models/account_moderation_note.rb
index 3ac9b1ac14..22e312bb22 100644
--- a/app/models/account_moderation_note.rb
+++ b/app/models/account_moderation_note.rb
@@ -3,10 +3,10 @@
#
# Table name: account_moderation_notes
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# content :text not null
-# account_id :integer not null
-# target_account_id :integer not null
+# account_id :bigint(8) not null
+# target_account_id :bigint(8) not null
# created_at :datetime not null
# updated_at :datetime not null
#
diff --git a/app/models/admin/action_log.rb b/app/models/admin/action_log.rb
index 81f278e073..1d1db1b7a9 100644
--- a/app/models/admin/action_log.rb
+++ b/app/models/admin/action_log.rb
@@ -3,11 +3,11 @@
#
# Table name: admin_action_logs
#
-# id :integer not null, primary key
-# account_id :integer
+# id :bigint(8) not null, primary key
+# account_id :bigint(8)
# action :string default(""), not null
# target_type :string
-# target_id :integer
+# target_id :bigint(8)
# recorded_changes :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
diff --git a/app/models/backup.rb b/app/models/backup.rb
index 5a7e6a14d5..c2651313b1 100644
--- a/app/models/backup.rb
+++ b/app/models/backup.rb
@@ -3,8 +3,8 @@
#
# Table name: backups
#
-# id :integer not null, primary key
-# user_id :integer
+# id :bigint(8) not null, primary key
+# user_id :bigint(8)
# dump_file_name :string
# dump_content_type :string
# dump_file_size :integer
diff --git a/app/models/block.rb b/app/models/block.rb
index d6ecabd3b8..df4a6bbacb 100644
--- a/app/models/block.rb
+++ b/app/models/block.rb
@@ -3,11 +3,11 @@
#
# Table name: blocks
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
-# account_id :integer not null
-# target_account_id :integer not null
+# account_id :bigint(8) not null
+# target_account_id :bigint(8) not null
#
class Block < ApplicationRecord
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index 3830ba9b05..20fc74ba6a 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -20,6 +20,10 @@ module AccountInteractions
follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
end
+ def blocked_by_map(target_account_ids, account_id)
+ follow_mapping(Block.where(account_id: target_account_ids, target_account_id: account_id), :account_id)
+ end
+
def muting_map(target_account_ids, account_id)
Mute.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |mute, mapping|
mapping[mute.target_account_id] = {
@@ -38,8 +42,12 @@ module AccountInteractions
def domain_blocking_map(target_account_ids, account_id)
accounts_map = Account.where(id: target_account_ids).select('id, domain').map { |a| [a.id, a.domain] }.to_h
- blocked_domains = AccountDomainBlock.where(account_id: account_id, domain: accounts_map.values).pluck(:domain)
- accounts_map.map { |id, domain| [id, blocked_domains.include?(domain)] }.to_h
+ blocked_domains = domain_blocking_map_by_domain(accounts_map.values.compact, account_id)
+ accounts_map.map { |id, domain| [id, blocked_domains[domain]] }.to_h
+ end
+
+ def domain_blocking_map_by_domain(target_domains, account_id)
+ follow_mapping(AccountDomainBlock.where(account_id: account_id, domain: target_domains), :domain)
end
private
@@ -93,6 +101,7 @@ module AccountInteractions
if mute.hide_notifications? != notifications
mute.update!(hide_notifications: notifications)
end
+ mute
end
def mute_conversation!(conversation)
diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb
index 90ce884634..6f8489b89b 100644
--- a/app/models/concerns/attachmentable.rb
+++ b/app/models/concerns/attachmentable.rb
@@ -1,10 +1,15 @@
# frozen_string_literal: true
+require 'mime/types'
+
module Attachmentable
extend ActiveSupport::Concern
+ MAX_MATRIX_LIMIT = 16_777_216 # 4096x4096px or approx. 16MB
+
included do
before_post_process :set_file_extensions
+ before_post_process :check_image_dimensions
end
private
@@ -12,10 +17,31 @@ module Attachmentable
def set_file_extensions
self.class.attachment_definitions.each_key do |attachment_name|
attachment = send(attachment_name)
+
next if attachment.blank?
- extension = Paperclip::Interpolations.content_type_extension(attachment, :original)
- basename = Paperclip::Interpolations.basename(attachment, :original)
- attachment.instance_write :file_name, [basename, extension].delete_if(&:blank?).join('.')
+
+ attachment.instance_write :file_name, [Paperclip::Interpolations.basename(attachment, :original), appropriate_extension(attachment)].delete_if(&:blank?).join('.')
end
end
+
+ def check_image_dimensions
+ self.class.attachment_definitions.each_key do |attachment_name|
+ attachment = send(attachment_name)
+
+ next if attachment.blank? || !attachment.content_type.match?(/image.*/) || attachment.queued_for_write[:original].blank?
+
+ width, height = FastImage.size(attachment.queued_for_write[:original].path)
+
+ raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported" if width.present? && height.present? && (width * height >= MAX_MATRIX_LIMIT)
+ end
+ end
+
+ def appropriate_extension(attachment)
+ mime_type = MIME::Types[attachment.content_type]
+
+ extensions_for_mime_type = mime_type.empty? ? [] : mime_type.first.extensions
+ original_extension = Paperclip::Interpolations.extension(attachment, :original)
+
+ extensions_for_mime_type.include?(original_extension) ? original_extension : extensions_for_mime_type.first
+ end
end
diff --git a/app/models/concerns/cacheable.rb b/app/models/concerns/cacheable.rb
index 51451d2607..d7524cdfd0 100644
--- a/app/models/concerns/cacheable.rb
+++ b/app/models/concerns/cacheable.rb
@@ -3,14 +3,19 @@
module Cacheable
extend ActiveSupport::Concern
- class_methods do
+ module ClassMethods
+ @cache_associated = []
+
def cache_associated(*associations)
@cache_associated = associations
end
- end
- included do
- scope :with_includes, -> { includes(@cache_associated) }
- scope :cache_ids, -> { select(:id, :updated_at) }
+ def with_includes
+ includes(@cache_associated)
+ end
+
+ def cache_ids
+ select(:id, :updated_at)
+ end
end
end
diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb
index 3b8c507c31..7f1ef5191b 100644
--- a/app/models/concerns/remotable.rb
+++ b/app/models/concerns/remotable.rb
@@ -38,7 +38,7 @@ module Remotable
self[attribute_name] = url if has_attribute?(attribute_name)
end
- rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError => e
+ rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e
Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}"
nil
end
diff --git a/app/models/concerns/status_threading_concern.rb b/app/models/concerns/status_threading_concern.rb
index fffc095ee0..8e817be00c 100644
--- a/app/models/concerns/status_threading_concern.rb
+++ b/app/models/concerns/status_threading_concern.rb
@@ -7,8 +7,8 @@ module StatusThreadingConcern
find_statuses_from_tree_path(ancestor_ids(limit), account)
end
- def descendants(account = nil)
- find_statuses_from_tree_path(descendant_ids, account)
+ def descendants(limit, account = nil, max_child_id = nil, since_child_id = nil, depth = nil)
+ find_statuses_from_tree_path(descendant_ids(limit, max_child_id, since_child_id, depth), account)
end
private
@@ -46,34 +46,46 @@ module StatusThreadingConcern
SQL
end
- def descendant_ids
- descendant_statuses.pluck(:id)
+ def descendant_ids(limit, max_child_id, since_child_id, depth)
+ descendant_statuses(limit, max_child_id, since_child_id, depth).pluck(:id)
end
- def descendant_statuses
- Status.find_by_sql([<<-SQL.squish, id: id])
+ def descendant_statuses(limit, max_child_id, since_child_id, depth)
+ Status.find_by_sql([<<-SQL.squish, id: id, limit: limit, max_child_id: max_child_id, since_child_id: since_child_id, depth: depth])
WITH RECURSIVE search_tree(id, path)
AS (
SELECT id, ARRAY[id]
FROM statuses
- WHERE in_reply_to_id = :id
+ WHERE in_reply_to_id = :id AND COALESCE(id < :max_child_id, TRUE) AND COALESCE(id > :since_child_id, TRUE)
UNION ALL
SELECT statuses.id, path || statuses.id
FROM search_tree
JOIN statuses ON statuses.in_reply_to_id = search_tree.id
- WHERE NOT statuses.id = ANY(path)
+ WHERE COALESCE(array_length(path, 1) < :depth, TRUE) AND NOT statuses.id = ANY(path)
)
SELECT id
FROM search_tree
ORDER BY path
+ LIMIT :limit
SQL
end
def find_statuses_from_tree_path(ids, account)
- statuses = statuses_with_accounts(ids).to_a
+ statuses = statuses_with_accounts(ids).to_a
+ account_ids = statuses.map(&:account_id).uniq
+ domains = statuses.map(&:account_domain).compact.uniq
- # FIXME: n+1 bonanza
- statuses.reject! { |status| filter_from_context?(status, account) }
+ relations = if account.present?
+ {
+ blocking: Account.blocking_map(account_ids, account.id),
+ blocked_by: Account.blocked_by_map(account_ids, account.id),
+ muting: Account.muting_map(account_ids, account.id),
+ following: Account.following_map(account_ids, account.id),
+ domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id),
+ }
+ end
+
+ statuses.reject! { |status| filter_from_context?(status, account, relations) }
# Order ancestors/descendants by tree path
statuses.sort_by! { |status| ids.index(status.id) }
@@ -83,7 +95,7 @@ module StatusThreadingConcern
Status.where(id: ids).includes(:account)
end
- def filter_from_context?(status, account)
- StatusFilter.new(status, account).filtered?
+ def filter_from_context?(status, account, relations)
+ StatusFilter.new(status, account, relations).filtered?
end
end
diff --git a/app/models/conversation.rb b/app/models/conversation.rb
index 08c1ce9458..4dfaea889d 100644
--- a/app/models/conversation.rb
+++ b/app/models/conversation.rb
@@ -3,7 +3,7 @@
#
# Table name: conversations
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# uri :string
# created_at :datetime not null
# updated_at :datetime not null
diff --git a/app/models/conversation_mute.rb b/app/models/conversation_mute.rb
index 272eb81afd..52c1a33e07 100644
--- a/app/models/conversation_mute.rb
+++ b/app/models/conversation_mute.rb
@@ -3,9 +3,9 @@
#
# Table name: conversation_mutes
#
-# id :integer not null, primary key
-# conversation_id :integer not null
-# account_id :integer not null
+# id :bigint(8) not null, primary key
+# conversation_id :bigint(8) not null
+# account_id :bigint(8) not null
#
class ConversationMute < ApplicationRecord
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index 1ec21d1a0b..b99ed01f08 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -3,7 +3,7 @@
#
# Table name: custom_emojis
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# shortcode :string default(""), not null
# domain :string
# image_file_name :string
@@ -40,6 +40,10 @@ class CustomEmoji < ApplicationRecord
remotable_attachment :image, LIMIT
+ include Attachmentable
+
+ after_commit :remove_entity_cache
+
def local?
domain.nil?
end
@@ -56,11 +60,17 @@ class CustomEmoji < ApplicationRecord
return [] if shortcodes.empty?
- where(shortcode: shortcodes, domain: domain, disabled: false)
+ EntityCache.instance.emoji(shortcodes, domain)
end
def search(shortcode)
where('"custom_emojis"."shortcode" ILIKE ?', "%#{shortcode}%")
end
end
+
+ private
+
+ def remove_entity_cache
+ Rails.cache.delete(EntityCache.instance.to_key(:emoji, shortcode, domain))
+ end
end
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index aea8919af8..93658793bd 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -3,7 +3,7 @@
#
# Table name: domain_blocks
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# domain :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb
index a104810d13..10490375bc 100644
--- a/app/models/email_domain_block.rb
+++ b/app/models/email_domain_block.rb
@@ -3,7 +3,7 @@
#
# Table name: email_domain_blocks
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# domain :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
diff --git a/app/models/favourite.rb b/app/models/favourite.rb
index fa1884b866..c998a67eb5 100644
--- a/app/models/favourite.rb
+++ b/app/models/favourite.rb
@@ -3,11 +3,11 @@
#
# Table name: favourites
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
-# account_id :integer not null
-# status_id :integer not null
+# account_id :bigint(8) not null
+# status_id :bigint(8) not null
#
class Favourite < ApplicationRecord
diff --git a/app/models/follow.rb b/app/models/follow.rb
index 8e6fe537a5..2ca42ff70b 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -3,11 +3,11 @@
#
# Table name: follows
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
-# account_id :integer not null
-# target_account_id :integer not null
+# account_id :bigint(8) not null
+# target_account_id :bigint(8) not null
# show_reblogs :boolean default(TRUE), not null
#
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index cde26ceed7..d559a8f62f 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -3,11 +3,11 @@
#
# Table name: follow_requests
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
-# account_id :integer not null
-# target_account_id :integer not null
+# account_id :bigint(8) not null
+# target_account_id :bigint(8) not null
# show_reblogs :boolean default(TRUE), not null
#
diff --git a/app/models/import.rb b/app/models/import.rb
index fdb4c6b80f..55e970b0d8 100644
--- a/app/models/import.rb
+++ b/app/models/import.rb
@@ -3,7 +3,7 @@
#
# Table name: imports
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# type :integer not null
# approved :boolean default(FALSE), not null
# created_at :datetime not null
@@ -12,7 +12,7 @@
# data_content_type :string
# data_file_size :integer
# data_updated_at :datetime
-# account_id :integer not null
+# account_id :bigint(8) not null
#
class Import < ApplicationRecord
diff --git a/app/models/invite.rb b/app/models/invite.rb
index 4ba5432d23..2250e588e1 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -3,8 +3,8 @@
#
# Table name: invites
#
-# id :integer not null, primary key
-# user_id :integer not null
+# id :bigint(8) not null, primary key
+# user_id :bigint(8) not null
# code :string default(""), not null
# expires_at :datetime
# max_uses :integer
diff --git a/app/models/list.rb b/app/models/list.rb
index a2ec7e84a2..c9c94fca1d 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -3,8 +3,8 @@
#
# Table name: lists
#
-# id :integer not null, primary key
-# account_id :integer not null
+# id :bigint(8) not null, primary key
+# account_id :bigint(8) not null
# title :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
diff --git a/app/models/list_account.rb b/app/models/list_account.rb
index da46cf0325..87b498224c 100644
--- a/app/models/list_account.rb
+++ b/app/models/list_account.rb
@@ -3,10 +3,10 @@
#
# Table name: list_accounts
#
-# id :integer not null, primary key
-# list_id :integer not null
-# account_id :integer not null
-# follow_id :integer not null
+# id :bigint(8) not null, primary key
+# list_id :bigint(8) not null
+# account_id :bigint(8) not null
+# follow_id :bigint(8) not null
#
class ListAccount < ApplicationRecord
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 3b16944cef..62abc876eb 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -3,8 +3,8 @@
#
# Table name: media_attachments
#
-# id :integer not null, primary key
-# status_id :integer
+# id :bigint(8) not null, primary key
+# status_id :bigint(8)
# file_file_name :string
# file_content_type :string
# file_file_size :integer
@@ -15,12 +15,10 @@
# shortcode :string
# type :integer default("image"), not null
# file_meta :json
-# account_id :integer
+# account_id :bigint(8)
# description :text
#
-require 'mime/types'
-
class MediaAttachment < ApplicationRecord
self.inheritance_column = nil
@@ -90,6 +88,8 @@ class MediaAttachment < ApplicationRecord
validates_attachment_size :file, less_than: LIMIT
remotable_attachment :file, LIMIT
+ include Attachmentable
+
validates :account, presence: true
validates :description, length: { maximum: 420 }, if: :local?
@@ -247,13 +247,4 @@ class MediaAttachment < ApplicationRecord
bitrate: movie.bitrate,
}
end
-
- def appropriate_extension
- mime_type = MIME::Types[file.content_type]
-
- extensions_for_mime_type = mime_type.empty? ? [] : mime_type.first.extensions
- original_extension = Paperclip::Interpolations.extension(file, :original)
-
- extensions_for_mime_type.include?(original_extension) ? original_extension : extensions_for_mime_type.first
- end
end
diff --git a/app/models/mention.rb b/app/models/mention.rb
index f864bf8e15..8ab886b184 100644
--- a/app/models/mention.rb
+++ b/app/models/mention.rb
@@ -3,11 +3,11 @@
#
# Table name: mentions
#
-# id :integer not null, primary key
-# status_id :integer
+# id :bigint(8) not null, primary key
+# status_id :bigint(8)
# created_at :datetime not null
# updated_at :datetime not null
-# account_id :integer
+# account_id :bigint(8)
#
class Mention < ApplicationRecord
diff --git a/app/models/mute.rb b/app/models/mute.rb
index ebb3818c7a..639120f7dd 100644
--- a/app/models/mute.rb
+++ b/app/models/mute.rb
@@ -3,12 +3,12 @@
#
# Table name: mutes
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# hide_notifications :boolean default(TRUE), not null
-# account_id :integer not null
-# target_account_id :integer not null
+# account_id :bigint(8) not null
+# target_account_id :bigint(8) not null
#
class Mute < ApplicationRecord
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 0b0f01aa8b..4f6ec8e8ea 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -3,13 +3,13 @@
#
# Table name: notifications
#
-# id :integer not null, primary key
-# activity_id :integer not null
+# id :bigint(8) not null, primary key
+# activity_id :bigint(8) not null
# activity_type :string not null
# created_at :datetime not null
# updated_at :datetime not null
-# account_id :integer not null
-# from_account_id :integer not null
+# account_id :bigint(8) not null
+# from_account_id :bigint(8) not null
#
class Notification < ApplicationRecord
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index 0c82f06ce0..a792b352bd 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -3,7 +3,7 @@
#
# Table name: preview_cards
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# url :string default(""), not null
# title :string default(""), not null
# description :string default(""), not null
@@ -34,7 +34,7 @@ class PreviewCard < ApplicationRecord
has_and_belongs_to_many :statuses
- has_attached_file :image, styles: { original: { geometry: '400x400>', file_geometry_parser: FastGeometryParser } }, convert_options: { all: '-quality 80 -strip' }
+ has_attached_file :image, styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }
include Attachmentable
@@ -52,6 +52,23 @@ class PreviewCard < ApplicationRecord
save!
end
+ class << self
+ private
+
+ def image_styles(f)
+ styles = {
+ original: {
+ geometry: '400x400>',
+ file_geometry_parser: FastGeometryParser,
+ convert_options: '-coalesce -strip',
+ },
+ }
+
+ styles[:original][:format] = 'jpg' if f.instance.image_content_type == 'image/gif'
+ styles
+ end
+ end
+
private
def extract_dimensions
diff --git a/app/models/report.rb b/app/models/report.rb
index 5b90c7bcea..efe385b2db 100644
--- a/app/models/report.rb
+++ b/app/models/report.rb
@@ -3,16 +3,16 @@
#
# Table name: reports
#
-# id :integer not null, primary key
-# status_ids :integer default([]), not null, is an Array
+# id :bigint(8) not null, primary key
+# status_ids :bigint(8) default([]), not null, is an Array
# comment :text default(""), not null
# action_taken :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
-# account_id :integer not null
-# action_taken_by_account_id :integer
-# target_account_id :integer not null
-# assigned_account_id :integer
+# account_id :bigint(8) not null
+# action_taken_by_account_id :bigint(8)
+# target_account_id :bigint(8) not null
+# assigned_account_id :bigint(8)
#
class Report < ApplicationRecord
diff --git a/app/models/report_note.rb b/app/models/report_note.rb
index 6d9dec80aa..54b416577a 100644
--- a/app/models/report_note.rb
+++ b/app/models/report_note.rb
@@ -3,10 +3,10 @@
#
# Table name: report_notes
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# content :text not null
-# report_id :integer not null
-# account_id :integer not null
+# report_id :bigint(8) not null
+# account_id :bigint(8) not null
# created_at :datetime not null
# updated_at :datetime not null
#
diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb
index d364f03dfd..34d25c83db 100644
--- a/app/models/session_activation.rb
+++ b/app/models/session_activation.rb
@@ -3,15 +3,15 @@
#
# Table name: session_activations
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# session_id :string not null
# created_at :datetime not null
# updated_at :datetime not null
# user_agent :string default(""), not null
# ip :inet
-# access_token_id :integer
-# user_id :integer not null
-# web_push_subscription_id :integer
+# access_token_id :bigint(8)
+# user_id :bigint(8) not null
+# web_push_subscription_id :bigint(8)
#
class SessionActivation < ApplicationRecord
diff --git a/app/models/setting.rb b/app/models/setting.rb
index df93590ce8..033d09fd58 100644
--- a/app/models/setting.rb
+++ b/app/models/setting.rb
@@ -3,13 +3,13 @@
#
# Table name: settings
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# var :string not null
# value :text
# thing_type :string
# created_at :datetime
# updated_at :datetime
-# thing_id :integer
+# thing_id :bigint(8)
#
class Setting < RailsSettings::Base
diff --git a/app/models/site_upload.rb b/app/models/site_upload.rb
index 641128adfc..14d6837672 100644
--- a/app/models/site_upload.rb
+++ b/app/models/site_upload.rb
@@ -3,7 +3,7 @@
#
# Table name: site_uploads
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# var :string default(""), not null
# file_file_name :string
# file_content_type :string
diff --git a/app/models/status.rb b/app/models/status.rb
index 9526611692..44238ca6b7 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -3,13 +3,13 @@
#
# Table name: statuses
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# uri :string
# text :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
-# in_reply_to_id :integer
-# reblog_of_id :integer
+# in_reply_to_id :bigint(8)
+# reblog_of_id :bigint(8)
# url :string
# sensitive :boolean default(FALSE), not null
# visibility :integer default("public"), not null
@@ -18,11 +18,11 @@
# favourites_count :integer default(0), not null
# reblogs_count :integer default(0), not null
# language :string
-# conversation_id :integer
+# conversation_id :bigint(8)
# local :boolean
-# account_id :integer not null
-# application_id :integer
-# in_reply_to_account_id :integer
+# account_id :bigint(8) not null
+# application_id :bigint(8)
+# in_reply_to_account_id :bigint(8)
# local_only :boolean
# full_status_text :text default(""), not null
#
@@ -62,6 +62,7 @@ class Status < ApplicationRecord
validates :uri, uniqueness: true, presence: true, unless: :local?
validates :text, presence: true, unless: -> { with_media? || reblog? }
validates_with StatusLengthValidator
+ validates_with DisallowedHashtagsValidator
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
default_scope { recent }
@@ -164,7 +165,7 @@ class Status < ApplicationRecord
end
def emojis
- CustomEmoji.from_text([spoiler_text, text].join(' '), account.domain)
+ @emojis ||= CustomEmoji.from_text([spoiler_text, text].join(' '), account.domain)
end
after_create_commit :store_uri, if: :local?
diff --git a/app/models/status_pin.rb b/app/models/status_pin.rb
index d3a98d8bd3..afc76bded3 100644
--- a/app/models/status_pin.rb
+++ b/app/models/status_pin.rb
@@ -3,9 +3,9 @@
#
# Table name: status_pins
#
-# id :integer not null, primary key
-# account_id :integer not null
-# status_id :integer not null
+# id :bigint(8) not null, primary key
+# account_id :bigint(8) not null
+# status_id :bigint(8) not null
# created_at :datetime not null
# updated_at :datetime not null
#
diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb
index 36fe487dc0..dd383eb816 100644
--- a/app/models/stream_entry.rb
+++ b/app/models/stream_entry.rb
@@ -3,13 +3,13 @@
#
# Table name: stream_entries
#
-# id :integer not null, primary key
-# activity_id :integer
+# id :bigint(8) not null, primary key
+# activity_id :bigint(8)
# activity_type :string
# created_at :datetime not null
# updated_at :datetime not null
# hidden :boolean default(FALSE), not null
-# account_id :integer
+# account_id :bigint(8)
#
class StreamEntry < ApplicationRecord
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index ea11731607..79b81828da 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -3,7 +3,7 @@
#
# Table name: subscriptions
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# callback_url :string default(""), not null
# secret :string
# expires_at :datetime
@@ -12,7 +12,7 @@
# updated_at :datetime not null
# last_successful_delivery_at :datetime
# domain :string
-# account_id :integer not null
+# account_id :bigint(8) not null
#
class Subscription < ApplicationRecord
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 9fa9405d75..8b1b024120 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -3,7 +3,7 @@
#
# Table name: tags
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# name :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
diff --git a/app/models/user.rb b/app/models/user.rb
index 803eb8a332..24beb77b21 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -3,7 +3,7 @@
#
# Table name: users
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# email :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
@@ -30,10 +30,10 @@
# last_emailed_at :datetime
# otp_backup_codes :string is an Array
# filtered_languages :string default([]), not null, is an Array
-# account_id :integer not null
+# account_id :bigint(8) not null
# disabled :boolean default(FALSE), not null
# moderator :boolean default(FALSE), not null
-# invite_id :integer
+# invite_id :bigint(8)
# remember_token :string
#
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index 5aee92d27b..1736106f79 100644
--- a/app/models/web/push_subscription.rb
+++ b/app/models/web/push_subscription.rb
@@ -3,7 +3,7 @@
#
# Table name: web_push_subscriptions
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# endpoint :string not null
# key_p256dh :string not null
# key_auth :string not null
diff --git a/app/models/web/setting.rb b/app/models/web/setting.rb
index 0a5129d17c..99588d26c5 100644
--- a/app/models/web/setting.rb
+++ b/app/models/web/setting.rb
@@ -3,11 +3,11 @@
#
# Table name: web_settings
#
-# id :integer not null, primary key
+# id :bigint(8) not null, primary key
# data :json
# created_at :datetime not null
# updated_at :datetime not null
-# user_id :integer not null
+# user_id :bigint(8) not null
#
class Web::Setting < ApplicationRecord
diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb
index 3078768561..96cdee8c71 100644
--- a/app/policies/status_policy.rb
+++ b/app/policies/status_policy.rb
@@ -1,6 +1,12 @@
# frozen_string_literal: true
class StatusPolicy < ApplicationPolicy
+ def initialize(current_account, record, preloaded_relations = {})
+ super(current_account, record)
+
+ @preloaded_relations = preloaded_relations
+ end
+
def index?
staff?
end
@@ -9,16 +15,20 @@ class StatusPolicy < ApplicationPolicy
return false if local_only? && current_account.nil?
if direct?
- owned? || record.mentions.where(account: current_account).exists?
+ owned? || mention_exists?
elsif private?
- owned? || current_account&.following?(author) || record.mentions.where(account: current_account).exists?
+ owned? || following_author? || mention_exists?
else
- current_account.nil? || !author.blocking?(current_account)
+ current_account.nil? || !author_blocking?
end
end
def reblog?
- !direct? && (!private? || owned?) && show?
+ !direct? && (!private? || owned?) && show? && !blocking_author?
+ end
+
+ def favourite?
+ show? && !blocking_author?
end
def destroy?
@@ -45,6 +55,34 @@ class StatusPolicy < ApplicationPolicy
record.private_visibility?
end
+ def mention_exists?
+ return false if current_account.nil?
+
+ if record.mentions.loaded?
+ record.mentions.any? { |mention| mention.account_id == current_account.id }
+ else
+ record.mentions.where(account: current_account).exists?
+ end
+ end
+
+ def blocking_author?
+ return false if current_account.nil?
+
+ @preloaded_relations[:blocking] ? @preloaded_relations[:blocking][author.id] : current_account.blocking?(author)
+ end
+
+ def author_blocking?
+ return false if current_account.nil?
+
+ @preloaded_relations[:blocked_by] ? @preloaded_relations[:blocked_by][author.id] : author.blocking?(current_account)
+ end
+
+ def following_author?
+ return false if current_account.nil?
+
+ @preloaded_relations[:following] ? @preloaded_relations[:following][author.id] : current_account.following?(author)
+ end
+
def author
record.account
end
diff --git a/app/serializers/rest/credential_account_serializer.rb b/app/serializers/rest/credential_account_serializer.rb
index 870d8b71f0..56857cba88 100644
--- a/app/serializers/rest/credential_account_serializer.rb
+++ b/app/serializers/rest/credential_account_serializer.rb
@@ -5,10 +5,12 @@ class REST::CredentialAccountSerializer < REST::AccountSerializer
def source
user = object.user
+
{
privacy: user.setting_default_privacy,
sensitive: user.setting_default_sensitive,
note: object.note,
+ fields: object.fields.map(&:to_h),
}
end
end
diff --git a/app/serializers/rss/account_serializer.rb b/app/serializers/rss/account_serializer.rb
new file mode 100644
index 0000000000..bde360a41f
--- /dev/null
+++ b/app/serializers/rss/account_serializer.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class RSS::AccountSerializer
+ include ActionView::Helpers::NumberHelper
+ include StreamEntriesHelper
+ include RoutingHelper
+
+ def render(account, statuses)
+ builder = RSSBuilder.new
+
+ builder.title("#{display_name(account)} (@#{account.local_username_and_domain})")
+ .description(account_description(account))
+ .link(TagManager.instance.url_for(account))
+ .logo(full_asset_url(asset_pack_path('logo.svg')))
+ .accent_color('2b90d9')
+
+ builder.image(full_asset_url(account.avatar.url(:original))) if account.avatar?
+ builder.cover(full_asset_url(account.header.url(:original))) if account.header?
+
+ statuses.each do |status|
+ builder.item do |item|
+ item.title(status.title)
+ .link(TagManager.instance.url_for(status))
+ .pub_date(status.created_at)
+ .description(status.spoiler_text.presence || Formatter.instance.format(status).to_str)
+
+ status.media_attachments.each do |media|
+ item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, length: media.file.size)
+ end
+ end
+ end
+
+ builder.to_xml
+ end
+
+ def self.render(account, statuses)
+ new.render(account, statuses)
+ end
+end
diff --git a/app/serializers/rss/tag_serializer.rb b/app/serializers/rss/tag_serializer.rb
new file mode 100644
index 0000000000..7680a8da55
--- /dev/null
+++ b/app/serializers/rss/tag_serializer.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class RSS::TagSerializer
+ include ActionView::Helpers::NumberHelper
+ include ActionView::Helpers::SanitizeHelper
+ include StreamEntriesHelper
+ include RoutingHelper
+
+ def render(tag, statuses)
+ builder = RSSBuilder.new
+
+ builder.title("##{tag.name}")
+ .description(strip_tags(I18n.t('about.about_hashtag_html', hashtag: tag.name)))
+ .link(tag_url(tag))
+ .logo(full_asset_url(asset_pack_path('logo.svg')))
+ .accent_color('2b90d9')
+
+ statuses.each do |status|
+ builder.item do |item|
+ item.title(status.title)
+ .link(TagManager.instance.url_for(status))
+ .pub_date(status.created_at)
+ .description(status.spoiler_text.presence || Formatter.instance.format(status).to_str)
+
+ status.media_attachments.each do |media|
+ item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, length: media.file.size)
+ end
+ end
+ end
+
+ builder.to_xml
+ end
+
+ def self.render(tag, statuses)
+ new.render(tag, statuses)
+ end
+end
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index 3860a9cbd0..7edbd9b479 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -65,9 +65,9 @@ class AccountSearchService < BaseService
def exact_match
@_exact_match ||= begin
if domain_is_local?
- search_from.find_local(query_username)
+ search_from.without_suspended.find_local(query_username)
else
- search_from.find_remote(query_username, query_domain)
+ search_from.without_suspended.find_remote(query_username, query_domain)
end
end
end
diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb
index 40714e9801..6a137b520b 100644
--- a/app/services/activitypub/fetch_featured_collection_service.rb
+++ b/app/services/activitypub/fetch_featured_collection_service.rb
@@ -4,6 +4,8 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
include JsonLdHelper
def call(account)
+ return if account.featured_collection_url.blank?
+
@account = account
@json = fetch_resource(@account.featured_collection_url, true)
diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb
index 5024853ca5..867e708760 100644
--- a/app/services/activitypub/fetch_remote_account_service.rb
+++ b/app/services/activitypub/fetch_remote_account_service.rb
@@ -56,6 +56,6 @@ class ActivityPub::FetchRemoteAccountService < BaseService
end
def expected_type?
- SUPPORTED_TYPES.include?(@json['type'])
+ equals_or_includes_any?(@json['type'], SUPPORTED_TYPES)
end
end
diff --git a/app/services/activitypub/fetch_remote_key_service.rb b/app/services/activitypub/fetch_remote_key_service.rb
index 41837d4620..505baccd46 100644
--- a/app/services/activitypub/fetch_remote_key_service.rb
+++ b/app/services/activitypub/fetch_remote_key_service.rb
@@ -43,7 +43,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
end
def person?
- ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(@json['type'])
+ equals_or_includes_any?(@json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
end
def public_key?
@@ -55,6 +55,6 @@ class ActivityPub::FetchRemoteKeyService < BaseService
end
def confirmed_owner?
- ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(@owner['type']) && value_or_id(@owner['publicKey']) == @json['id']
+ equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && value_or_id(@owner['publicKey']) == @json['id']
end
end
diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb
index 503c175d8a..930fbad1f1 100644
--- a/app/services/activitypub/fetch_remote_status_service.rb
+++ b/app/services/activitypub/fetch_remote_status_service.rb
@@ -42,7 +42,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
end
def expected_type?
- (ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES).include? @json['type']
+ equals_or_includes_any?(@json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
end
def needs_update(actor)
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index da32f9615f..f67ebb443a 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -201,10 +201,7 @@ class ActivityPub::ProcessAccountService < BaseService
return if @json['tag'].blank?
as_array(@json['tag']).each do |tag|
- case tag['type']
- when 'Emoji'
- process_emoji tag
- end
+ process_emoji tag if equals_or_includes?(tag['type'], 'Emoji')
end
end
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 0f77556dcf..510b80c823 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'sidekiq-bulk'
-
class FanOutOnWriteService < BaseService
# Push a status into home and mentions feeds
# @param [Status] status
diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb
index 44df3ed13d..bc2d1547a0 100644
--- a/app/services/favourite_service.rb
+++ b/app/services/favourite_service.rb
@@ -8,7 +8,7 @@ class FavouriteService < BaseService
# @param [Status] status
# @return [Favourite]
def call(account, status)
- authorize_with account, status, :show?
+ authorize_with account, status, :favourite?
favourite = Favourite.find_by(account: account, status: status)
diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb
index 0444baf74a..550e75f334 100644
--- a/app/services/fetch_atom_service.rb
+++ b/app/services/fetch_atom_service.rb
@@ -42,7 +42,7 @@ class FetchAtomService < BaseService
elsif ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(response.mime_type)
body = response.body_with_limit
json = body_to_json(body)
- if supported_context?(json) && ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(json['type']) && json['inbox'].present?
+ if supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && json['inbox'].present?
[json['id'], { prefetched_body: body, id: true }, :activitypub]
elsif supported_context?(json) && expected_type?(json)
[json['id'], { prefetched_body: body, id: true }, :activitypub]
@@ -62,7 +62,7 @@ class FetchAtomService < BaseService
end
def expected_type?(json)
- (ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES).include? json['type']
+ equals_or_includes_any?(json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
end
def process_html(response)
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index d5920a417e..77d4aa5381 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -85,42 +85,40 @@ class FetchLinkCardService < BaseService
end
def attempt_oembed
- embed = OEmbed::Providers.get(@url, html: @html)
+ embed = FetchOEmbedService.new.call(@url, html: @html)
- return false unless embed.respond_to?(:type)
+ return false if embed.nil?
- @card.type = embed.type
- @card.title = embed.respond_to?(:title) ? embed.title : ''
- @card.author_name = embed.respond_to?(:author_name) ? embed.author_name : ''
- @card.author_url = embed.respond_to?(:author_url) ? embed.author_url : ''
- @card.provider_name = embed.respond_to?(:provider_name) ? embed.provider_name : ''
- @card.provider_url = embed.respond_to?(:provider_url) ? embed.provider_url : ''
+ @card.type = embed[:type]
+ @card.title = embed[:title] || ''
+ @card.author_name = embed[:author_name] || ''
+ @card.author_url = embed[:author_url] || ''
+ @card.provider_name = embed[:provider_name] || ''
+ @card.provider_url = embed[:provider_url] || ''
@card.width = 0
@card.height = 0
case @card.type
when 'link'
- @card.image_remote_url = embed.thumbnail_url if embed.respond_to?(:thumbnail_url)
+ @card.image_remote_url = embed[:thumbnail_url] if embed[:thumbnail_url].present?
when 'photo'
- return false unless embed.respond_to?(:url)
+ return false if embed[:url].blank?
- @card.embed_url = embed.url
- @card.image_remote_url = embed.url
- @card.width = embed.width.presence || 0
- @card.height = embed.height.presence || 0
+ @card.embed_url = embed[:url]
+ @card.image_remote_url = embed[:url]
+ @card.width = embed[:width].presence || 0
+ @card.height = embed[:height].presence || 0
when 'video'
- @card.width = embed.width.presence || 0
- @card.height = embed.height.presence || 0
- @card.html = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED)
- @card.image_remote_url = embed.thumbnail_url if embed.respond_to?(:thumbnail_url)
+ @card.width = embed[:width].presence || 0
+ @card.height = embed[:height].presence || 0
+ @card.html = Formatter.instance.sanitize(embed[:html], Sanitize::Config::MASTODON_OEMBED)
+ @card.image_remote_url = embed[:thumbnail_url] if embed[:thumbnail_url].present?
when 'rich'
# Most providers rely on