diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..70d03f6b9 --- /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 0c158b06e..eddd38d90 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 7da76f8ef..726351c5e 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 068b4874d..c3f4a62f2 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 09ee34f89..2e2cf1f3d 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 1efaf619b..50f5d0b11 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 535bd11d4..522f68c98 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 a4ae9507d..d00b3d222 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 7b5168b31..b5c084e14 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 062d490a7..a3c4008e6 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 d64325944..b7133ca8e 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 e98241323..01880565c 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 f2fe74b17..987290a14 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 abd85ea27..145549bcd 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 3237a15b9..2e9cf14e0 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 44e9c0bb8..8cb54a148 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 5d11a8139..a76be26e5 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 b17c52264..fdfadef08 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 bab4615a1..95863ab1f 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 dfb8fcb8b..e9056166c 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 a2f5917f9..f78e5fbc3 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 3992432db..c6f12ecd4 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 b4125e84e..28f27fbc6 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 eee9c6928..fe3e831d5 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 60b215f02..82fe4519a 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 8fbb17785..997813a04 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 34904194f..a4f5cf50c 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 c5c6f73b3..982d34718 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 51588e78c..3c8db7092 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 fd6858d05..f8a7f91d2 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 e5f7c9399..402d558c4 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 e58625582..d605dbc8a 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 c98d4564e..0c971ceb0 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 dc8fc02ba..84665a7e8 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 fc34c8cdc..bb9b75505 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 815e1905b..8cb81c1a6 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 24c8a5b54..947348f70 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 25ef6db65..971475114 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 6a44808e0..f2e3699d5 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 69c2ae8d8..f442e0675 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 000000000..a7e1c408f --- /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 ad6f3b712..d8e69fd3c 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 e51163971..37587c14c 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 61ea0588d..41d7db9da 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 000000000..49cdf5630 --- /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 cfe93007d..99aba00c3 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 1677c3c6c..07d4d9aa5 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 98c1c43d2..a4af97dda 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 fca42374d..652ca31d1 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 e3e87f1d0..0ffbb14f3 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 b41c98394..c41cc3ea1 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 956accc67..a0c186184 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 33e079201..a0442bad4 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 412ffd3a0..2fd922544 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 9730bf934..ed45ee11e 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 5146d7ca2..a7ca62015 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 abd18742a..dbb4562de 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 92367dc95..2a2734673 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 c18ddbd01..adc1d19a7 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 282a72acb..0ee6d0722 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 7170aefb8..d4836e9fe 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 08aea797d..6d6db7c82 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 c604476c7..7f8690f91 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 826785aad..ce816dc41 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 bb3cc1794..8eeebaf73 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 58274fd2d..e5e826c96 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 e4d07edd1..b1ea0d179 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 60c781e9d..aa978675f 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 8fa6992f1..4efe88a7e 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 000000000..a56720fee --- /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 3b91c0d2c..82b44fe30 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 cdf6f46a3..056fbfe8f 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 261e5795e..1a7b58789 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 000000000..0d4f101c7 --- /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 000000000..0d4f101c7 --- /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 000000000..0d4f101c7 --- /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 aba0bde83..a3a4de0af 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 b5ebd20fc..7719e08a6 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 28d634600..84ff25e03 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 da9b8c420..84d4fc698 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 ad897bcc9..dd675d78f 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 000000000..1b3260faa --- /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 000000000..8226e2c54 --- /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 000000000..6442eda38 --- /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 000000000..5b43aecbe --- /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 000000000..eee9ecc3e --- /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 000000000..f6cadf029 --- /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 0a09a38d2..c9c0e3081 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 f9af6f288..c2d0de4b9 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 348f72078..a6cc8b62b 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 83ac7a8d0..4980ab5f1 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 f0fde6666..a982585c3 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 8df2902d2..9d5ab66a4 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 3620a6f54..cf9547586 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 3a3b4c326..f97890187 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 651c06ced..86614b89b 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 c39163ba8..281cbaf83 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 c12d84f1c..fa876e603 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 dc4e72a2e..cbefe35b4 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 9b00f0f52..5b97a6208 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 c8a358195..8840a450c 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 45c0e91cb..edee2691f 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 0134b4015..aa5907f03 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 000000000..2aa37389c --- /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 e88e98eae..01346bfe5 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 700fd61c4..3a2dcac68 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 4124f1660..050c651ee 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 6235127b2..a24a0093c 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 055b4649c..7c66f2066 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 3bec7211b..000000000 --- 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 dca93a6e9..00f94dacf 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 000000000..63ddba2e8 --- /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 41d4381e5..b6c80b801 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 db2171102..c1ce1e99e 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 bc00b4f32..e352000c3 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 3ac9b1ac1..22e312bb2 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 81f278e07..1d1db1b7a 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 5a7e6a14d..c2651313b 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 d6ecabd3b..df4a6bbac 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 3830ba9b0..20fc74ba6 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 90ce88463..6f8489b89 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 51451d260..d7524cdfd 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 3b8c507c3..7f1ef5191 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 fffc095ee..8e817be00 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 08c1ce945..4dfaea889 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 272eb81af..52c1a33e0 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 1ec21d1a0..b99ed01f0 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 aea8919af..93658793b 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 a104810d1..10490375b 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 fa1884b86..c998a67eb 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 8e6fe537a..2ca42ff70 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 cde26ceed..d559a8f62 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 fdb4c6b80..55e970b0d 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 4ba5432d2..2250e588e 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 a2ec7e84a..c9c94fca1 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 da46cf032..87b498224 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 3b16944ce..62abc876e 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 f864bf8e1..8ab886b18 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 ebb3818c7..639120f7d 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 0b0f01aa8..4f6ec8e8e 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 0c82f06ce..a792b352b 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 5b90c7bce..efe385b2d 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 6d9dec80a..54b416577 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 d364f03df..34d25c83d 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 df93590ce..033d09fd5 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 641128adf..14d683767 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 952661169..44238ca6b 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 d3a98d8bd..afc76bded 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 36fe487dc..dd383eb81 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 ea1173160..79b81828d 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 9fa9405d7..8b1b02412 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 803eb8a33..24beb77b2 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 5aee92d27..1736106f7 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 0a5129d17..99588d26c 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 307876856..96cdee8c7 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 870d8b71f..56857cba8 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 000000000..bde360a41 --- /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 000000000..7680a8da5 --- /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 3860a9cbd..7edbd9b47 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 40714e980..6a137b520 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 5024853ca..867e70876 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 41837d462..505baccd4 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 503c175d8..930fbad1f 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 da32f9615..f67ebb443 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 0f77556dc..510b80c82 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 44df3ed13..bc2d1547a 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 0444baf74..550e75f33 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 d5920a417..77d4aa538 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