diff --git a/.circleci/config.yml b/.circleci/config.yml index 751ca95b1..a9ad92145 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -32,7 +32,7 @@ commands: name: Install system dependencies command: | sudo apt-get update - sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler + sudo apt-get install -y libicu-dev libidn11-dev install-ruby-dependencies: parameters: ruby-version: diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 58f2813d3..f8b4a751d 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -6,16 +6,22 @@ on: - "main" tags: - "*" + pull_request: + paths: + - .github/workflows/build-image.yml + - Dockerfile jobs: build-image: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: docker/setup-qemu-action@v1 - uses: docker/setup-buildx-action@v1 - uses: docker/login-action@v1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + if: github.event_name != 'pull_request' - uses: docker/metadata-action@v3 id: meta with: @@ -25,10 +31,12 @@ jobs: tags: | type=edge,branch=main type=semver,pattern={{ raw }} + type=ref,event=pr - uses: docker/build-push-action@v2 with: context: . - push: true + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} cache-from: type=registry,ref=tootsuite/mastodon:latest cache-to: type=inline diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index 2e8f230f3..9cb98dd12 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -18,7 +18,7 @@ jobs: - name: Install system dependencies run: | sudo apt-get update - sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler + sudo apt-get install -y libicu-dev libidn11-dev - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/.rubocop.yml b/.rubocop.yml index 2af0f59bb..68634e9e3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -32,10 +32,11 @@ Layout/EmptyLineAfterGuardClause: Layout/EmptyLinesAroundAttributeAccessor: Enabled: true +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent + Layout/HashAlignment: Enabled: false - # EnforcedHashRocketStyle: table - # EnforcedColonStyle: table Layout/SpaceAroundMethodCallOperator: Enabled: true diff --git a/Aptfile b/Aptfile index b2cbad714..9235141ad 100644 --- a/Aptfile +++ b/Aptfile @@ -4,10 +4,8 @@ libicu-dev libidn11 libidn11-dev libpq-dev -libprotobuf-dev libxdamage1 libxfixes3 -protobuf-compiler zlib1g-dev libcairo2 libcroco3 diff --git a/Dockerfile b/Dockerfile index c6287b5a7..1b3661561 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,7 +51,7 @@ RUN npm install -g npm@latest && \ gem install bundler && \ apt-get update && \ apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \ - libpq-dev libprotobuf-dev protobuf-compiler shared-mime-info + libpq-dev shared-mime-info COPY Gemfile* package.json yarn.lock /opt/mastodon/ @@ -88,7 +88,7 @@ RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selectio RUN apt-get update && \ apt-get -y --no-install-recommends install \ libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \ - libicu66 libprotobuf17 libidn11 libyaml-0-2 \ + libicu66 libidn11 libyaml-0-2 \ file ca-certificates tzdata libreadline8 gcc tini apt-utils && \ ln -s /opt/mastodon /mastodon && \ gem install bundler && \ diff --git a/Gemfile b/Gemfile index afed1ac94..b5d15da61 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ ruby '>= 2.5.0', '< 3.1.0' gem 'pkg-config', '~> 1.4' gem 'rexml', '~> 3.2' -gem 'puma', '~> 5.5' +gem 'puma', '~> 5.6' gem 'rails', '~> 6.1.4' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' @@ -18,20 +18,18 @@ gem 'makara', '~> 0.5' gem 'pghero', '~> 2.8' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.111', require: false +gem 'aws-sdk-s3', '~> 1.113', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false -gem 'kt-paperclip', '~> 7.0' +gem 'kt-paperclip', '~> 7.1' gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.8' -gem 'bootsnap', '~> 1.10.2', require: false +gem 'bootsnap', '~> 1.10.3', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.7' -gem 'iso-639' gem 'chewy', '~> 7.2' -gem 'cld3', '~> 3.4.4' gem 'devise', '~> 4.8' gem 'devise-two-factor', '~> 4.0' @@ -67,7 +65,7 @@ gem 'oj', '~> 3.13' gem 'ox', '~> 2.14' gem 'parslet' gem 'posix-spawn' -gem 'pundit', '~> 2.1' +gem 'pundit', '~> 2.2' gem 'premailer-rails' gem 'rack-attack', '~> 6.5' gem 'rack-cors', '~> 1.1', require: 'rack/cors' @@ -78,7 +76,7 @@ gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'rqrcode', '~> 2.1' gem 'ruby-progressbar', '~> 1.11' gem 'sanitize', '~> 6.0' -gem 'scenic', '~> 1.5' +gem 'scenic', '~> 1.6' gem 'sidekiq', '~> 6.4' gem 'sidekiq-scheduler', '~> 3.1' gem 'sidekiq-unique-jobs', '~> 7.1' @@ -100,12 +98,12 @@ gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' group :development, :test do - gem 'fabrication', '~> 2.24' + gem 'fabrication', '~> 2.27' gem 'fuubar', '~> 2.5' gem 'i18n-tasks', '~> 0.9', require: false gem 'pry-byebug', '~> 3.9' gem 'pry-rails', '~> 0.3' - gem 'rspec-rails', '~> 5.0' + gem 'rspec-rails', '~> 5.1' end group :production, :test do @@ -126,7 +124,7 @@ end group :development do gem 'active_record_query_trace', '~> 1.8' - gem 'annotate', '~> 3.1' + gem 'annotate', '~> 3.2' gem 'better_errors', '~> 2.9' gem 'binding_of_caller', '~> 1.0' gem 'bullet', '~> 7.0' diff --git a/Gemfile.lock b/Gemfile.lock index dc5b33964..48337d9ff 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,40 +1,40 @@ GEM remote: https://rubygems.org/ specs: - actioncable (6.1.4.4) - actionpack (= 6.1.4.4) - activesupport (= 6.1.4.4) + actioncable (6.1.4.6) + actionpack (= 6.1.4.6) + activesupport (= 6.1.4.6) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.4) - actionpack (= 6.1.4.4) - activejob (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + actionmailbox (6.1.4.6) + actionpack (= 6.1.4.6) + activejob (= 6.1.4.6) + activerecord (= 6.1.4.6) + activestorage (= 6.1.4.6) + activesupport (= 6.1.4.6) mail (>= 2.7.1) - actionmailer (6.1.4.4) - actionpack (= 6.1.4.4) - actionview (= 6.1.4.4) - activejob (= 6.1.4.4) - activesupport (= 6.1.4.4) + actionmailer (6.1.4.6) + actionpack (= 6.1.4.6) + actionview (= 6.1.4.6) + activejob (= 6.1.4.6) + activesupport (= 6.1.4.6) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.4.4) - actionview (= 6.1.4.4) - activesupport (= 6.1.4.4) + actionpack (6.1.4.6) + actionview (= 6.1.4.6) + activesupport (= 6.1.4.6) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.4) - actionpack (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + actiontext (6.1.4.6) + actionpack (= 6.1.4.6) + activerecord (= 6.1.4.6) + activestorage (= 6.1.4.6) + activesupport (= 6.1.4.6) nokogiri (>= 1.8.5) - actionview (6.1.4.4) - activesupport (= 6.1.4.4) + actionview (6.1.4.6) + activesupport (= 6.1.4.6) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -45,22 +45,22 @@ GEM case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) active_record_query_trace (1.8) - activejob (6.1.4.4) - activesupport (= 6.1.4.4) + activejob (6.1.4.6) + activesupport (= 6.1.4.6) globalid (>= 0.3.6) - activemodel (6.1.4.4) - activesupport (= 6.1.4.4) - activerecord (6.1.4.4) - activemodel (= 6.1.4.4) - activesupport (= 6.1.4.4) - activestorage (6.1.4.4) - actionpack (= 6.1.4.4) - activejob (= 6.1.4.4) - activerecord (= 6.1.4.4) - activesupport (= 6.1.4.4) + activemodel (6.1.4.6) + activesupport (= 6.1.4.6) + activerecord (6.1.4.6) + activemodel (= 6.1.4.6) + activesupport (= 6.1.4.6) + activestorage (6.1.4.6) + actionpack (= 6.1.4.6) + activejob (= 6.1.4.6) + activerecord (= 6.1.4.6) + activesupport (= 6.1.4.6) marcel (~> 1.0.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.4) + activesupport (6.1.4.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -71,25 +71,25 @@ GEM airbrussh (1.4.0) sshkit (>= 1.6.1, != 1.7.0) android_key_attestation (0.3.0) - annotate (3.1.1) - activerecord (>= 3.2, < 7.0) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ast (2.4.2) attr_encrypted (3.1.0) encryptor (~> 3.0.0) awrence (1.1.1) aws-eventstream (1.2.0) - aws-partitions (1.549.0) - aws-sdk-core (3.125.5) + aws-partitions (1.558.0) + aws-sdk-core (3.127.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.53.0) - aws-sdk-core (~> 3, >= 3.125.0) + aws-sdk-kms (1.55.0) + aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.111.3) - aws-sdk-core (~> 3, >= 3.125.0) + aws-sdk-s3 (1.113.0) + aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.4.0) @@ -104,9 +104,9 @@ GEM debug_inspector (>= 0.0.1) blurhash (0.1.5) ffi (~> 1.14) - bootsnap (1.10.2) + bootsnap (1.10.3) msgpack (~> 1.2) - brakeman (5.2.0) + brakeman (5.2.1) browser (4.2.0) brpoplpush-redis_script (0.1.2) concurrent-ruby (~> 1.0, >= 1.0.5) @@ -147,13 +147,11 @@ GEM activesupport cbor (0.5.9.6) charlock_holmes (0.7.7) - chewy (7.2.3) + chewy (7.2.4) activesupport (>= 5.2) elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl chunky_png (1.4.0) - cld3 (3.4.4) - ffi (>= 1.1.0, < 1.16.0) climate_control (0.2.0) coderay (1.1.3) color_diff (0.1) @@ -183,7 +181,7 @@ GEM devise_pam_authenticatable2 (9.2.0) devise (>= 4.0.0) rpam2 (~> 4.0) - diff-lcs (1.4.4) + diff-lcs (1.5.0) discard (1.2.1) activerecord (>= 4.2, < 8) docile (1.3.4) @@ -208,31 +206,35 @@ GEM multi_json encryptor (3.0.0) erubi (1.10.0) - et-orbi (1.2.4) + et-orbi (1.2.6) tzinfo excon (0.76.0) - fabrication (2.24.0) + fabrication (2.27.0) faker (2.19.0) i18n (>= 1.6, < 2) - faraday (1.8.0) + faraday (1.9.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.1) + faraday-net_http_persistent (~> 1.0) faraday-patron (~> 1.0) faraday-rack (~> 1.0) - multipart-post (>= 1.2, < 3) + faraday-retry (~> 1.0) ruby2_keywords (>= 0.0.4) faraday-em_http (1.0.0) faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) + faraday-multipart (1.0.3) + multipart-post (>= 1.2, < 3) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) + faraday-retry (1.0.3) fast_blank (1.0.1) fastimage (2.2.6) ffi (1.15.5) @@ -252,7 +254,7 @@ GEM fog-json (>= 1.0) ipaddress (>= 0.8) formatador (0.2.5) - fugit (1.4.5) + fugit (1.5.2) et-orbi (~> 1.1, >= 1.1.8) raabro (~> 1.4) fuubar (2.5.1) @@ -287,7 +289,7 @@ GEM httplog (1.5.0) rack (>= 1.0) rainbow (>= 2.0.0) - i18n (1.8.11) + i18n (1.9.1) concurrent-ruby (~> 1.0) i18n-tasks (0.9.37) activesupport (>= 4.0.2) @@ -301,8 +303,7 @@ GEM terminal-table (>= 1.5.1) idn-ruby (0.1.4) ipaddress (0.8.3) - iso-639 (0.3.5) - jmespath (1.5.0) + jmespath (1.6.0) json (2.5.1) json-canonicalization (0.3.0) json-ld (3.2.0) @@ -329,7 +330,7 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - kt-paperclip (7.0.1) + kt-paperclip (7.1.1) activemodel (>= 4.2.0) activesupport (>= 4.2.0) marcel (~> 1.0.1) @@ -353,14 +354,14 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.13.0) + loofah (2.14.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) mini_mime (>= 0.1.1) makara (0.5.1) activerecord (>= 5.2.0) - marcel (1.0.1) + marcel (1.0.2) mario-redis-lock (1.2.1) redis (>= 3.0.5) matrix (0.4.2) @@ -371,9 +372,9 @@ GEM nokogiri (~> 1.10) mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2021.1115) + mime-types-data (3.2022.0105) mini_mime (1.1.2) - mini_portile2 (2.7.1) + mini_portile2 (2.8.0) minitest (5.15.0) msgpack (1.4.4) multi_json (1.15.0) @@ -383,8 +384,8 @@ GEM net-ssh (>= 2.6.5, < 7.0.0) net-ssh (6.1.0) nio4r (2.5.8) - nokogiri (1.13.1) - mini_portile2 (~> 2.7.0) + nokogiri (1.13.3) + mini_portile2 (~> 2.8.0) racc (~> 1.4) nsa (0.2.8) activesupport (>= 4.2, < 7) @@ -408,14 +409,14 @@ GEM openssl (2.2.0) openssl-signature_algorithm (0.4.0) orm_adapter (0.5.0) - ox (2.14.6) + ox (2.14.9) parallel (1.21.0) parser (3.1.0.0) ast (~> 2.4.1) parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.3.0) + pg (1.3.3) pghero (2.8.2) activerecord (>= 5) pkg-config (1.4.7) @@ -437,9 +438,9 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.6) - puma (5.5.2) + puma (5.6.2) nio4r (~> 2.0) - pundit (2.1.1) + pundit (2.2.0) activesupport (>= 3.0.0) raabro (1.4.0) racc (1.6.0) @@ -452,20 +453,20 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.1.4.4) - actioncable (= 6.1.4.4) - actionmailbox (= 6.1.4.4) - actionmailer (= 6.1.4.4) - actionpack (= 6.1.4.4) - actiontext (= 6.1.4.4) - actionview (= 6.1.4.4) - activejob (= 6.1.4.4) - activemodel (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + rails (6.1.4.6) + actioncable (= 6.1.4.6) + actionmailbox (= 6.1.4.6) + actionmailer (= 6.1.4.6) + actionpack (= 6.1.4.6) + actiontext (= 6.1.4.6) + actionview (= 6.1.4.6) + activejob (= 6.1.4.6) + activemodel (= 6.1.4.6) + activerecord (= 6.1.4.6) + activestorage (= 6.1.4.6) + activesupport (= 6.1.4.6) bundler (>= 1.15.0) - railties (= 6.1.4.4) + railties (= 6.1.4.6) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -481,9 +482,9 @@ GEM railties (>= 6.0.0, < 7) rails-settings-cached (0.6.6) rails (>= 4.2.0) - railties (6.1.4.4) - actionpack (= 6.1.4.4) - activesupport (= 6.1.4.4) + railties (6.1.4.6) + actionpack (= 6.1.4.6) + activesupport (= 6.1.4.6) method_source rake (>= 0.13) thor (~> 1.0) @@ -505,19 +506,19 @@ GEM rexml (3.2.5) rotp (6.2.0) rpam2 (4.0.2) - rqrcode (2.1.0) + rqrcode (2.1.1) chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) + rspec-core (3.11.0) + rspec-support (~> 3.11.0) + rspec-expectations (3.11.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.2) + rspec-support (~> 3.11.0) + rspec-mocks (3.11.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (5.0.2) + rspec-support (~> 3.11.0) + rspec-rails (5.1.0) actionpack (>= 5.2) activesupport (>= 5.2) railties (>= 5.2) @@ -528,10 +529,10 @@ GEM rspec-sidekiq (3.1.0) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) - rspec-support (3.10.3) + rspec-support (3.11.0) rspec_junit_formatter (0.5.1) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.25.0) + rubocop (1.25.1) parallel (~> 1.10) parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) @@ -551,32 +552,32 @@ GEM nokogiri (>= 1.10.5) rexml ruby2_keywords (0.0.5) - rufus-scheduler (3.7.0) + rufus-scheduler (3.8.1) fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) jwt (~> 2.0) sanitize (6.0.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - scenic (1.5.5) + scenic (1.6.0) activerecord (>= 4.0.0) railties (>= 4.0.0) securecompare (1.0.0) semantic_range (3.0.0) - sidekiq (6.4.0) + sidekiq (6.4.1) connection_pool (>= 2.2.2) rack (~> 2.0) redis (>= 4.2.0) sidekiq-bulk (0.2.0) sidekiq - sidekiq-scheduler (3.1.0) + sidekiq-scheduler (3.1.1) e2mmap redis (>= 3, < 5) rufus-scheduler (~> 3.2) sidekiq (>= 3) thwait tilt (>= 1.4.0) - sidekiq-unique-jobs (7.1.12) + sidekiq-unique-jobs (7.1.15) brpoplpush-redis_script (> 0.1.1, <= 2.0.0) concurrent-ruby (~> 1.0, >= 1.0.5) sidekiq (>= 5.0, < 8.0) @@ -672,7 +673,7 @@ GEM xorcist (1.1.2) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.5.3) + zeitwerk (2.5.4) PLATFORMS ruby @@ -681,12 +682,12 @@ DEPENDENCIES active_model_serializers (~> 0.10) active_record_query_trace (~> 1.8) addressable (~> 2.8) - annotate (~> 3.1) - aws-sdk-s3 (~> 1.111) + annotate (~> 3.2) + aws-sdk-s3 (~> 1.113) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) - bootsnap (~> 1.10.2) + bootsnap (~> 1.10.3) brakeman (~> 5.2) browser bullet (~> 7.0) @@ -698,7 +699,6 @@ DEPENDENCIES capybara (~> 3.36) charlock_holmes (~> 0.7.7) chewy (~> 7.2) - cld3 (~> 3.4.4) climate_control (~> 0.2) color_diff (~> 0.1) concurrent-ruby @@ -710,7 +710,7 @@ DEPENDENCIES doorkeeper (~> 5.5) dotenv-rails (~> 2.7) ed25519 (~> 1.3) - fabrication (~> 2.24) + fabrication (~> 2.27) faker (~> 2.19) fast_blank (~> 1.0) fastimage @@ -725,11 +725,10 @@ DEPENDENCIES httplog (~> 1.5.0) i18n-tasks (~> 0.9) idn-ruby - iso-639 json-ld json-ld-preloaded (~> 3.2) kaminari (~> 1.2) - kt-paperclip (~> 7.0) + kt-paperclip (~> 7.1) letter_opener (~> 1.7) letter_opener_web (~> 2.0) link_header (~> 0.0) @@ -757,8 +756,8 @@ DEPENDENCIES private_address_check (~> 0.5) pry-byebug (~> 3.9) pry-rails (~> 0.3) - puma (~> 5.5) - pundit (~> 2.1) + puma (~> 5.6) + pundit (~> 2.2) rack (~> 2.2.3) rack-attack (~> 6.5) rack-cors (~> 1.1) @@ -771,14 +770,14 @@ DEPENDENCIES redis-namespace (~> 1.8) rexml (~> 3.2) rqrcode (~> 2.1) - rspec-rails (~> 5.0) + rspec-rails (~> 5.1) rspec-sidekiq (~> 3.1) rspec_junit_formatter (~> 0.5) rubocop (~> 1.25) rubocop-rails (~> 2.13) ruby-progressbar (~> 1.11) sanitize (~> 6.0) - scenic (~> 1.5) + scenic (~> 1.6) sidekiq (~> 6.4) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 3.1) diff --git a/Vagrantfile b/Vagrantfile index e086ddd98..3e73d9e47 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -33,11 +33,9 @@ sudo apt-get install \ redis-tools \ postgresql \ postgresql-contrib \ - protobuf-compiler \ yarn \ libicu-dev \ libidn11-dev \ - libprotobuf-dev \ libreadline-dev \ libpam0g-dev \ -y diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index fde6c861f..4ff7cfa08 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -63,15 +63,29 @@ class ActivityPub::RepliesController < ActivityPub::BaseController end def next_page - only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT) + if only_other_accounts? + # Only consider remote accounts + return nil if @replies.size < DESCENDANTS_LIMIT - account_status_replies_url( - @account, - @status, - page: true, - min_id: only_other_accounts && !only_other_accounts? ? nil : @replies&.last&.id, - only_other_accounts: only_other_accounts - ) + account_status_replies_url( + @account, + @status, + page: true, + min_id: @replies&.last&.id, + only_other_accounts: true + ) + else + # For now, we're serving only self-replies, but next page might be other accounts + next_only_other_accounts = @replies&.last&.account_id != @account.id || @replies.size < DESCENDANTS_LIMIT + + account_status_replies_url( + @account, + @status, + page: true, + min_id: next_only_other_accounts ? nil : @replies&.last&.id, + only_other_accounts: next_only_other_accounts + ) + end end def page_params diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index e7f56e243..e0ae71b9f 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -28,7 +28,7 @@ module Admin @deletion_request = @account.deletion_request @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account) @moderation_notes = @account.targeted_moderation_notes.latest - @warnings = @account.strikes.custom.latest + @warnings = @account.strikes.includes(:target_account, :account, :appeal).latest @domain_block = DomainBlock.rule_for(@account.domain) end @@ -146,7 +146,7 @@ module Admin end def filter_params - params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS) + params.slice(:page, *AccountFilter::KEYS).permit(:page, *AccountFilter::KEYS) end def form_account_batch_params diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index f0a935411..e376baab2 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -8,6 +8,7 @@ module Admin @pending_users_count = User.pending.count @pending_reports_count = Report.unresolved.count @pending_tags_count = Tag.pending_review.count + @pending_appeals_count = Appeal.pending.count end private diff --git a/app/controllers/admin/disputes/appeals_controller.rb b/app/controllers/admin/disputes/appeals_controller.rb new file mode 100644 index 000000000..32e5e2f6f --- /dev/null +++ b/app/controllers/admin/disputes/appeals_controller.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class Admin::Disputes::AppealsController < Admin::BaseController + before_action :set_appeal, except: :index + + def index + authorize :appeal, :index? + + @appeals = filtered_appeals.page(params[:page]) + end + + def approve + authorize @appeal, :approve? + log_action :approve, @appeal + ApproveAppealService.new.call(@appeal, current_account) + redirect_to disputes_strike_path(@appeal.strike) + end + + def reject + authorize @appeal, :approve? + log_action :reject, @appeal + @appeal.reject!(current_account) + UserMailer.appeal_rejected(@appeal.account.user, @appeal) + redirect_to disputes_strike_path(@appeal.strike) + end + + private + + def filtered_appeals + Admin::AppealFilter.new(filter_params.with_defaults(status: 'pending')).results.includes(strike: :account) + end + + def filter_params + params.slice(:page, *Admin::AppealFilter::KEYS).permit(:page, *Admin::AppealFilter::KEYS) + end + + def set_appeal + @appeal = Appeal.find(params[:id]) + end +end diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index f7bdfb0c5..a4bbbba5b 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -6,7 +6,20 @@ module Admin def index authorize :email_domain_block, :index? + @email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page]) + @form = Form::EmailDomainBlockBatch.new + end + + def batch + @form = Form::EmailDomainBlockBatch.new(form_email_domain_block_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.email_domain_blocks.no_email_domain_block_selected') + rescue Mastodon::NotPermittedError + flash[:alert] = I18n.t('admin.custom_emojis.not_permitted') + ensure + redirect_to admin_email_domain_blocks_path end def new @@ -19,41 +32,27 @@ module Admin @email_domain_block = EmailDomainBlock.new(resource_params) - if @email_domain_block.save - log_action :create, @email_domain_block + if action_from_button == 'save' + EmailDomainBlock.transaction do + @email_domain_block.save! + log_action :create, @email_domain_block - if @email_domain_block.with_dns_records? - hostnames = [] - ips = [] + (@email_domain_block.other_domains || []).uniq.each do |domain| + next if EmailDomainBlock.where(domain: domain).exists? - Resolv::DNS.open do |dns| - dns.timeouts = 5 - - hostnames = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s } - - ([@email_domain_block.domain] + hostnames).uniq.each do |hostname| - ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s }) - ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::AAAA).to_a.map { |e| e.address.to_s }) - end - end - - (hostnames + ips).each do |hostname| - another_email_domain_block = EmailDomainBlock.new(domain: hostname, parent: @email_domain_block) - log_action :create, another_email_domain_block if another_email_domain_block.save + other_email_domain_block = EmailDomainBlock.create!(domain: domain, parent: @email_domain_block) + log_action :create, other_email_domain_block end end redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg') else + set_resolved_records render :new end - end - - def destroy - authorize @email_domain_block, :destroy? - @email_domain_block.destroy! - log_action :destroy, @email_domain_block - redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg') + rescue ActiveRecord::RecordInvalid + set_resolved_records + render :new end private @@ -62,8 +61,27 @@ module Admin @email_domain_block = EmailDomainBlock.find(params[:id]) end + def set_resolved_records + Resolv::DNS.open do |dns| + dns.timeouts = 5 + @resolved_records = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a + end + end + def resource_params - params.require(:email_domain_block).permit(:domain, :with_dns_records) + params.require(:email_domain_block).permit(:domain, other_domains: []) + end + + def form_email_domain_block_batch_params + params.require(:form_email_domain_block_batch).permit(email_domain_block_ids: []) + end + + def action_from_button + if params[:delete] + 'delete' + elsif params[:save] + 'save' + end end end end diff --git a/app/controllers/admin/relationships_controller.rb b/app/controllers/admin/relationships_controller.rb index f8a95cfc8..085ded21c 100644 --- a/app/controllers/admin/relationships_controller.rb +++ b/app/controllers/admin/relationships_controller.rb @@ -9,7 +9,8 @@ module Admin def index authorize :account, :index? - @accounts = RelationshipFilter.new(@account, filter_params).results.page(params[:page]).per(PER_PAGE) + @accounts = RelationshipFilter.new(@account, filter_params).results.includes(:account_stat, user: [:ips, :invite_request]).page(params[:page]).per(PER_PAGE) + @form = Form::AccountBatch.new end private diff --git a/app/controllers/admin/reports/actions_controller.rb b/app/controllers/admin/reports/actions_controller.rb new file mode 100644 index 000000000..05a4fb63d --- /dev/null +++ b/app/controllers/admin/reports/actions_controller.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class Admin::Reports::ActionsController < Admin::BaseController + before_action :set_report + + def create + authorize @report, :show? + + case action_from_button + when 'delete' + status_batch_action = Admin::StatusBatchAction.new( + type: action_from_button, + status_ids: @report.status_ids, + current_account: current_account, + report_id: @report.id, + send_email_notification: !@report.spam? + ) + + status_batch_action.save! + when 'silence', 'suspend' + account_action = Admin::AccountAction.new( + type: action_from_button, + report_id: @report.id, + target_account: @report.target_account, + current_account: current_account, + send_email_notification: !@report.spam? + ) + + account_action.save! + end + + redirect_to admin_reports_path + end + + private + + def set_report + @report = Report.find(params[:report_id]) + end + + def action_from_button + if params[:delete] + 'delete' + elsif params[:silence] + 'silence' + elsif params[:suspend] + 'suspend' + end + end +end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index 8d039b281..817c0caa9 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -29,8 +29,9 @@ module Admin end def after_create_redirect_path - if @status_batch_action.report_id.present? - admin_report_path(@status_batch_action.report_id) + report_id = @status_batch_action&.report_id || params[:report_id] + if report_id.present? + admin_report_path(report_id) else admin_account_statuses_path(params[:account_id], current_params) end @@ -48,6 +49,10 @@ module Admin params.slice(*Admin::StatusFilter::KEYS).permit(*Admin::StatusFilter::KEYS) end + def current_params + params.slice(:media, :page).permit(:media, :page) + end + def action_from_button if params[:report] 'report' diff --git a/app/controllers/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/admin/trends/links/preview_card_providers_controller.rb index 2c26e03f3..40a466cd6 100644 --- a/app/controllers/admin/trends/links/preview_card_providers_controller.rb +++ b/app/controllers/admin/trends/links/preview_card_providers_controller.rb @@ -5,11 +5,11 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll authorize :preview_card_provider, :index? @preview_card_providers = filtered_preview_card_providers.page(params[:page]) - @form = Form::PreviewCardProviderBatch.new + @form = Trends::PreviewCardProviderBatch.new end def batch - @form = Form::PreviewCardProviderBatch.new(form_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button)) + @form = Trends::PreviewCardProviderBatch.new(trends_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button)) @form.save rescue ActionController::ParameterMissing flash[:alert] = I18n.t('admin.accounts.no_account_selected') @@ -20,15 +20,15 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll private def filtered_preview_card_providers - PreviewCardProviderFilter.new(filter_params).results + Trends::PreviewCardProviderFilter.new(filter_params).results end def filter_params - params.slice(:page, *PreviewCardProviderFilter::KEYS).permit(:page, *PreviewCardProviderFilter::KEYS) + params.slice(:page, *Trends::PreviewCardProviderFilter::KEYS).permit(:page, *Trends::PreviewCardProviderFilter::KEYS) end - def form_preview_card_provider_batch_params - params.require(:form_preview_card_provider_batch).permit(:action, preview_card_provider_ids: []) + def trends_preview_card_provider_batch_params + params.require(:trends_preview_card_provider_batch).permit(:action, preview_card_provider_ids: []) end def action_from_button diff --git a/app/controllers/admin/trends/links_controller.rb b/app/controllers/admin/trends/links_controller.rb index 619b37deb..434eec5fe 100644 --- a/app/controllers/admin/trends/links_controller.rb +++ b/app/controllers/admin/trends/links_controller.rb @@ -5,11 +5,11 @@ class Admin::Trends::LinksController < Admin::BaseController authorize :preview_card, :index? @preview_cards = filtered_preview_cards.page(params[:page]) - @form = Form::PreviewCardBatch.new + @form = Trends::PreviewCardBatch.new end def batch - @form = Form::PreviewCardBatch.new(form_preview_card_batch_params.merge(current_account: current_account, action: action_from_button)) + @form = Trends::PreviewCardBatch.new(trends_preview_card_batch_params.merge(current_account: current_account, action: action_from_button)) @form.save rescue ActionController::ParameterMissing flash[:alert] = I18n.t('admin.accounts.no_account_selected') @@ -20,26 +20,26 @@ class Admin::Trends::LinksController < Admin::BaseController private def filtered_preview_cards - PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results + Trends::PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results end def filter_params - params.slice(:page, *PreviewCardFilter::KEYS).permit(:page, *PreviewCardFilter::KEYS) + params.slice(:page, *Trends::PreviewCardFilter::KEYS).permit(:page, *Trends::PreviewCardFilter::KEYS) end - def form_preview_card_batch_params - params.require(:form_preview_card_batch).permit(:action, preview_card_ids: []) + def trends_preview_card_batch_params + params.require(:trends_preview_card_batch).permit(:action, preview_card_ids: []) end def action_from_button if params[:approve] 'approve' - elsif params[:approve_all] - 'approve_all' + elsif params[:approve_providers] + 'approve_providers' elsif params[:reject] 'reject' - elsif params[:reject_all] - 'reject_all' + elsif params[:reject_providers] + 'reject_providers' end end end diff --git a/app/controllers/admin/trends/statuses_controller.rb b/app/controllers/admin/trends/statuses_controller.rb new file mode 100644 index 000000000..766242738 --- /dev/null +++ b/app/controllers/admin/trends/statuses_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class Admin::Trends::StatusesController < Admin::BaseController + def index + authorize :status, :index? + + @statuses = filtered_statuses.page(params[:page]) + @form = Trends::StatusBatch.new + end + + def batch + @form = Trends::StatusBatch.new(trends_status_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.accounts.no_account_selected') + ensure + redirect_to admin_trends_statuses_path(filter_params) + end + + private + + def filtered_statuses + Trends::StatusFilter.new(filter_params.with_defaults(trending: 'all')).results.includes(:account, :media_attachments, :active_mentions) + end + + def filter_params + params.slice(:page, *Trends::StatusFilter::KEYS).permit(:page, *Trends::StatusFilter::KEYS) + end + + def trends_status_batch_params + params.require(:trends_status_batch).permit(:action, status_ids: []) + end + + def action_from_button + if params[:approve] + 'approve' + elsif params[:approve_accounts] + 'approve_accounts' + elsif params[:reject] + 'reject' + elsif params[:reject_accounts] + 'reject_accounts' + end + end +end diff --git a/app/controllers/admin/trends/tags_controller.rb b/app/controllers/admin/trends/tags_controller.rb index 91ff33d40..f4d1ec0d1 100644 --- a/app/controllers/admin/trends/tags_controller.rb +++ b/app/controllers/admin/trends/tags_controller.rb @@ -5,11 +5,11 @@ class Admin::Trends::TagsController < Admin::BaseController authorize :tag, :index? @tags = filtered_tags.page(params[:page]) - @form = Form::TagBatch.new + @form = Trends::TagBatch.new end def batch - @form = Form::TagBatch.new(form_tag_batch_params.merge(current_account: current_account, action: action_from_button)) + @form = Trends::TagBatch.new(trends_tag_batch_params.merge(current_account: current_account, action: action_from_button)) @form.save rescue ActionController::ParameterMissing flash[:alert] = I18n.t('admin.accounts.no_account_selected') @@ -20,15 +20,15 @@ class Admin::Trends::TagsController < Admin::BaseController private def filtered_tags - TagFilter.new(filter_params).results + Trends::TagFilter.new(filter_params).results end def filter_params - params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS) + params.slice(:page, *Trends::TagFilter::KEYS).permit(:page, *Trends::TagFilter::KEYS) end - def form_tag_batch_params - params.require(:form_tag_batch).permit(:action, tag_ids: []) + def trends_tag_batch_params + params.require(:trends_tag_batch).permit(:action, tag_ids: []) end def action_from_button diff --git a/app/controllers/api/v1/admin/trends/links_controller.rb b/app/controllers/api/v1/admin/trends/links_controller.rb new file mode 100644 index 000000000..63b3d9358 --- /dev/null +++ b/app/controllers/api/v1/admin/trends/links_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Api::V1::Admin::Trends::LinksController < Api::BaseController + protect_from_forgery with: :exception + + before_action -> { authorize_if_got_token! :'admin:read' } + before_action :require_staff! + before_action :set_links + + def index + render json: @links, each_serializer: REST::Trends::LinkSerializer + end + + private + + def set_links + @links = Trends.links.query.limit(limit_param(10)) + end +end diff --git a/app/controllers/api/v1/admin/trends/statuses_controller.rb b/app/controllers/api/v1/admin/trends/statuses_controller.rb new file mode 100644 index 000000000..86633cc74 --- /dev/null +++ b/app/controllers/api/v1/admin/trends/statuses_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Api::V1::Admin::Trends::StatusesController < Api::BaseController + protect_from_forgery with: :exception + + before_action -> { authorize_if_got_token! :'admin:read' } + before_action :require_staff! + before_action :set_statuses + + def index + render json: @statuses, each_serializer: REST::StatusSerializer + end + + private + + def set_statuses + @statuses = cache_collection(Trends.statuses.query.limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status) + end +end diff --git a/app/controllers/api/v1/admin/trends/tags_controller.rb b/app/controllers/api/v1/admin/trends/tags_controller.rb index 4815af31e..5cc4c269d 100644 --- a/app/controllers/api/v1/admin/trends/tags_controller.rb +++ b/app/controllers/api/v1/admin/trends/tags_controller.rb @@ -14,6 +14,6 @@ class Api::V1::Admin::Trends::TagsController < Api::BaseController private def set_tags - @tags = Trends.tags.get(false, limit_param(10)) + @tags = Trends.tags.query.limit(limit_param(10)) end end diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index a2a919a3e..72094790f 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -20,7 +20,7 @@ class Api::V1::MediaController < Api::BaseController end def update - @media_attachment.update!(media_attachment_params) + @media_attachment.update!(updateable_media_attachment_params) render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment end @@ -42,6 +42,10 @@ class Api::V1::MediaController < Api::BaseController params.permit(:file, :thumbnail, :description, :focus) end + def updateable_media_attachment_params + params.permit(:thumbnail, :description, :focus) + end + def file_type_error { error: 'File type of uploaded media could not be verified' } end diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index e10083d45..052d70cc8 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -33,6 +33,6 @@ class Api::V1::ReportsController < Api::BaseController end def report_params - params.permit(:account_id, :comment, :forward, status_ids: []) + params.permit(:account_id, :comment, :category, :forward, status_ids: [], rule_ids: []) end end diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb index c2c1fac5d..7fe73a6f5 100644 --- a/app/controllers/api/v1/statuses/histories_controller.rb +++ b/app/controllers/api/v1/statuses/histories_controller.rb @@ -7,7 +7,7 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController before_action :set_status def show - render json: @status.edits, each_serializer: REST::StatusEditSerializer + render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer end private diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index a50143101..dc63241de 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -3,8 +3,8 @@ class Api::V1::StatusesController < Api::BaseController include Authorization - before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy] - before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy] + before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy] + before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy] before_action :require_user!, except: [:show, :context] before_action :set_status, only: [:show, :context] before_action :set_thread, only: [:create] @@ -38,25 +38,45 @@ class Api::V1::StatusesController < Api::BaseController end def create - @status = PostStatusService.new.call(current_user.account, - text: status_params[:status], - thread: @thread, - media_ids: status_params[:media_ids], - sensitive: status_params[:sensitive], - spoiler_text: status_params[:spoiler_text], - visibility: status_params[:visibility], - scheduled_at: status_params[:scheduled_at], - application: doorkeeper_token.application, - poll: status_params[:poll], - idempotency: request.headers['Idempotency-Key'], - with_rate_limit: true, - quote_id: status_params[:quote_id].presence) + @status = PostStatusService.new.call( + current_user.account, + text: status_params[:status], + thread: @thread, + media_ids: status_params[:media_ids], + sensitive: status_params[:sensitive], + spoiler_text: status_params[:spoiler_text], + visibility: status_params[:visibility], + language: status_params[:language], + scheduled_at: status_params[:scheduled_at], + application: doorkeeper_token.application, + poll: status_params[:poll], + idempotency: request.headers['Idempotency-Key'], + with_rate_limit: true, + quote_id: status_params[:quote_id].presence, + ) render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer end + def update + @status = Status.where(account: current_account).find(params[:id]) + authorize @status, :update? + + UpdateStatusService.new.call( + @status, + current_account.id, + text: status_params[:status], + media_ids: status_params[:media_ids], + sensitive: status_params[:sensitive], + spoiler_text: status_params[:spoiler_text], + poll: status_params[:poll] + ) + + render json: @status, serializer: REST::StatusSerializer + end + def destroy - @status = Status.where(account_id: current_user.account).find(params[:id]) + @status = Status.where(account: current_account).find(params[:id]) authorize @status, :destroy? @status.discard @@ -88,6 +108,7 @@ class Api::V1::StatusesController < Api::BaseController :sensitive, :spoiler_text, :visibility, + :language, :scheduled_at, :quote_id, media_ids: [], diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb index 1c3ab1e1c..ad20e7f8b 100644 --- a/app/controllers/api/v1/trends/links_controller.rb +++ b/app/controllers/api/v1/trends/links_controller.rb @@ -12,10 +12,14 @@ class Api::V1::Trends::LinksController < Api::BaseController def set_links @links = begin if Setting.trends - Trends.links.get(true, limit_param(10)) + links_from_trends else [] end end end + + def links_from_trends + Trends.links.query.allowed.in_locale(content_locale).limit(limit_param(10)) + end end diff --git a/app/controllers/api/v1/trends/statuses_controller.rb b/app/controllers/api/v1/trends/statuses_controller.rb new file mode 100644 index 000000000..d4ec97ae5 --- /dev/null +++ b/app/controllers/api/v1/trends/statuses_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class Api::V1::Trends::StatusesController < Api::BaseController + before_action :set_statuses + + def index + render json: @statuses, each_serializer: REST::StatusSerializer + end + + private + + def set_statuses + @statuses = begin + if Setting.trends + cache_collection(statuses_from_trends, Status) + else + [] + end + end + end + + def statuses_from_trends + scope = Trends.statuses.query.allowed.in_locale(content_locale) + scope = scope.filtered_for(current_account) if user_signed_in? + scope.limit(limit_param(DEFAULT_STATUSES_LIMIT)) + end +end diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index 947b53de2..1334b72d2 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -12,7 +12,7 @@ class Api::V1::Trends::TagsController < Api::BaseController def set_tags @tags = begin if Setting.trends - Trends.tags.get(true, limit_param(10)) + Trends.tags.query.allowed.limit(limit_param(10)) else [] end diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index bed57fc54..5167928e9 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -17,16 +17,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController data = { policy: 'all', - - alerts: { - follow: alerts_enabled, - follow_request: alerts_enabled, - favourite: alerts_enabled, - reblog: alerts_enabled, - mention: alerts_enabled, - poll: alerts_enabled, - status: alerts_enabled, - }, + alerts: Notification::TYPES.index_with { alerts_enabled }, } data.deep_merge!(data_params) if params[:data] @@ -61,6 +52,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController end def data_params - @data_params ||= params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status]) + @data_params ||= params.require(:data).permit(:policy, alerts: Notification::TYPES) end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index f37e906fd..3b025838b 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -9,6 +9,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :check_enabled_registrations, only: [:new, :create] before_action :configure_sign_up_params, only: [:create] before_action :set_sessions, only: [:edit, :update] + before_action :set_strikes, only: [:edit, :update] before_action :set_instance_presenter, only: [:new, :create, :update] before_action :set_body_classes, only: [:new, :create, :edit, :update] before_action :require_not_suspended!, only: [:update] @@ -111,8 +112,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_invite - invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil - @invite = invite&.valid_for_use? ? invite : nil + @invite = begin + invite = Invite.find_by(code: invite_code) if invite_code.present? + invite if invite&.valid_for_use? + end end def determine_layout @@ -123,6 +126,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController @sessions = current_user.session_activations end + def set_strikes + @strikes = current_account.strikes.active.latest + end + def require_not_suspended! forbidden if current_account.suspended? end diff --git a/app/controllers/concerns/authorization.rb b/app/controllers/concerns/authorization.rb index 95a37e379..05260cc8b 100644 --- a/app/controllers/concerns/authorization.rb +++ b/app/controllers/concerns/authorization.rb @@ -3,7 +3,7 @@ module Authorization extend ActiveSupport::Concern - include Pundit + include Pundit::Authorization def pundit_user current_account diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb index fe1142f34..ede299d5a 100644 --- a/app/controllers/concerns/localized.rb +++ b/app/controllers/concerns/localized.rb @@ -7,27 +7,28 @@ module Localized around_action :set_locale end - def set_locale - locale = current_user.locale if respond_to?(:user_signed_in?) && user_signed_in? - locale ||= session[:locale] ||= default_locale - locale = default_locale unless I18n.available_locales.include?(locale.to_sym) - - I18n.with_locale(locale) do - yield - end + def set_locale(&block) + I18n.with_locale(requested_locale || I18n.default_locale, &block) end private - def default_locale - if ENV['DEFAULT_LOCALE'].present? - I18n.default_locale - else - request_locale || I18n.default_locale - end + def requested_locale + requested_locale_name = available_locale_or_nil(params[:lang]) + requested_locale_name ||= available_locale_or_nil(current_user.locale) if respond_to?(:user_signed_in?) && user_signed_in? + requested_locale_name ||= http_accept_language if ENV['DEFAULT_LOCALE'].blank? + requested_locale_name end - def request_locale - http_accept_language.language_region_compatible_from(I18n.available_locales) + def http_accept_language + HttpAcceptLanguage::Parser.new(request.headers.fetch('Accept-Language')).language_region_compatible_from(I18n.available_locales) if request.headers.key?('Accept-Language') + end + + def available_locale_or_nil(locale_name) + locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym) + end + + def content_locale + @content_locale ||= I18n.locale.to_s.split(/[_-]/).first end end diff --git a/app/controllers/disputes/appeals_controller.rb b/app/controllers/disputes/appeals_controller.rb new file mode 100644 index 000000000..eefd92b5a --- /dev/null +++ b/app/controllers/disputes/appeals_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Disputes::AppealsController < Disputes::BaseController + before_action :set_strike + + def create + authorize @strike, :appeal? + + @appeal = AppealService.new.call(@strike, appeal_params[:text]) + + redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg') + rescue ActiveRecord::RecordInvalid => e + @appeal = e.record + render template: 'disputes/strikes/show' + end + + private + + def set_strike + @strike = current_account.strikes.find(params[:strike_id]) + end + + def appeal_params + params.require(:appeal).permit(:text) + end +end diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb new file mode 100644 index 000000000..865146b5c --- /dev/null +++ b/app/controllers/disputes/base_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Disputes::BaseController < ApplicationController + include Authorization + + layout 'admin' + + skip_before_action :require_functional! + + before_action :set_body_classes + before_action :authenticate_user! + + private + + def set_body_classes + @body_classes = 'admin' + end +end diff --git a/app/controllers/disputes/strikes_controller.rb b/app/controllers/disputes/strikes_controller.rb new file mode 100644 index 000000000..d41c5c727 --- /dev/null +++ b/app/controllers/disputes/strikes_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Disputes::StrikesController < Disputes::BaseController + before_action :set_strike + + def show + authorize @strike, :show? + + @appeal = @strike.appeal || @strike.build_appeal + end + + private + + def set_strike + @strike = AccountWarning.find(params[:id]) + end +end diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index b3b5476e2..0853897f2 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -3,6 +3,7 @@ class InstanceActorsController < ApplicationController include AccountControllerConcern + skip_before_action :check_account_confirmation skip_around_action :set_locale def show diff --git a/app/helpers/admin/account_moderation_notes_helper.rb b/app/helpers/admin/account_moderation_notes_helper.rb index 40b2a5289..2f08538ca 100644 --- a/app/helpers/admin/account_moderation_notes_helper.rb +++ b/app/helpers/admin/account_moderation_notes_helper.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true module Admin::AccountModerationNotesHelper - def admin_account_link_to(account) + def admin_account_link_to(account, path: nil) return if account.nil? - link_to admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do + link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct 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'), diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index f3aa4be4f..47eeeaac3 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -33,6 +33,8 @@ module Admin::ActionLogsHelper "#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})" when 'Instance' record.domain + when 'Appeal' + link_to record.account.acct, disputes_strike_path(record.strike) end end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 907529b37..140fc73ed 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -5,9 +5,10 @@ module Admin::FilterHelper AccountFilter::KEYS, CustomEmojiFilter::KEYS, ReportFilter::KEYS, - TagFilter::KEYS, - PreviewCardProviderFilter::KEYS, - PreviewCardFilter::KEYS, + Trends::TagFilter::KEYS, + Trends::PreviewCardProviderFilter::KEYS, + Trends::PreviewCardFilter::KEYS, + Trends::StatusFilter::KEYS, InstanceFilter::KEYS, InviteFilter::KEYS, RelationshipFilter::KEYS, diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb new file mode 100644 index 000000000..d16e3dd12 --- /dev/null +++ b/app/helpers/admin/trends/statuses_helper.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Admin::Trends::StatusesHelper + def one_line_preview(status) + text = begin + if status.local? + status.text.split("\n").first + else + Nokogiri::HTML(status.text).css('html > body > *').first&.text + end + end + + return '' if text.blank? + + html = Formatter.instance.send(:encode, text) + html = Formatter.instance.send(:encode_custom_emojis, html, status.emojis, prefers_autoplay?) + + html.html_safe # rubocop:disable Rails/OutputSafety + end +end diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index 730724208..f22cc6d28 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -1,94 +1,247 @@ # frozen_string_literal: true module LanguagesHelper - HUMAN_LOCALES = { - af: 'Afrikaans', - ar: 'العربية', - ast: 'Asturianu', - bg: 'Български', - bn: 'বাংলা', - br: 'Breton', - ca: 'Català', - co: 'Corsu', - cs: 'Čeština', - cy: 'Cymraeg', - da: 'Dansk', - de: 'Deutsch', - el: 'Ελληνικά', - en: 'English', - eo: 'Esperanto', + ISO_639_1 = { + aa: ['Afar', 'Afaraf'].freeze, + ab: ['Abkhaz', 'аҧсуа бызшәа'].freeze, + ae: ['Avestan', 'avesta'].freeze, + af: ['Afrikaans', 'Afrikaans'].freeze, + ak: ['Akan', 'Akan'].freeze, + am: ['Amharic', 'አማርኛ'].freeze, + an: ['Aragonese', 'aragonés'].freeze, + ar: ['Arabic', 'اللغة العربية'].freeze, + as: ['Assamese', 'অসমীয়া'].freeze, + av: ['Avaric', 'авар мацӀ'].freeze, + ay: ['Aymara', 'aymar aru'].freeze, + az: ['Azerbaijani', 'azərbaycan dili'].freeze, + ba: ['Bashkir', 'башҡорт теле'].freeze, + be: ['Belarusian', 'беларуская мова'].freeze, + bg: ['Bulgarian', 'български език'].freeze, + bh: ['Bihari', 'भोजपुरी'].freeze, + bi: ['Bislama', 'Bislama'].freeze, + bm: ['Bambara', 'bamanankan'].freeze, + bn: ['Bengali', 'বাংলা'].freeze, + bo: ['Tibetan', 'བོད་ཡིག'].freeze, + br: ['Breton', 'brezhoneg'].freeze, + bs: ['Bosnian', 'bosanski jezik'].freeze, + ca: ['Catalan', 'Català'].freeze, + ce: ['Chechen', 'нохчийн мотт'].freeze, + ch: ['Chamorro', 'Chamoru'].freeze, + co: ['Corsican', 'corsu'].freeze, + cr: ['Cree', 'ᓀᐦᐃᔭᐍᐏᐣ'].freeze, + cs: ['Czech', 'čeština'].freeze, + cu: ['Old Church Slavonic', 'ѩзыкъ словѣньскъ'].freeze, + cv: ['Chuvash', 'чӑваш чӗлхи'].freeze, + cy: ['Welsh', 'Cymraeg'].freeze, + da: ['Danish', 'dansk'].freeze, + de: ['German', 'Deutsch'].freeze, + dv: ['Divehi', 'Dhivehi'].freeze, + dz: ['Dzongkha', 'རྫོང་ཁ'].freeze, + ee: ['Ewe', 'Eʋegbe'].freeze, + el: ['Greek', 'Ελληνικά'].freeze, + en: ['English', 'English'].freeze, + eo: ['Esperanto', 'Esperanto'].freeze, + es: ['Spanish', 'Español'].freeze, + et: ['Estonian', 'eesti'].freeze, + eu: ['Basque', 'euskara'].freeze, + fa: ['Persian', 'فارسی'].freeze, + ff: ['Fula', 'Fulfulde'].freeze, + fi: ['Finnish', 'suomi'].freeze, + fj: ['Fijian', 'Vakaviti'].freeze, + fo: ['Faroese', 'føroyskt'].freeze, + fr: ['French', 'Français'].freeze, + fy: ['Western Frisian', 'Frysk'].freeze, + ga: ['Irish', 'Gaeilge'].freeze, + gd: ['Scottish Gaelic', 'Gàidhlig'].freeze, + gl: ['Galician', 'galego'].freeze, + gu: ['Gujarati', 'ગુજરાતી'].freeze, + gv: ['Manx', 'Gaelg'].freeze, + ha: ['Hausa', 'هَوُسَ'].freeze, + he: ['Hebrew', 'עברית'].freeze, + hi: ['Hindi', 'हिन्दी'].freeze, + ho: ['Hiri Motu', 'Hiri Motu'].freeze, + hr: ['Croatian', 'Hrvatski'].freeze, + ht: ['Haitian', 'Kreyòl ayisyen'].freeze, + hu: ['Hungarian', 'magyar'].freeze, + hy: ['Armenian', 'Հայերեն'].freeze, + hz: ['Herero', 'Otjiherero'].freeze, + ia: ['Interlingua', 'Interlingua'].freeze, + id: ['Indonesian', 'Bahasa Indonesia'].freeze, + ie: ['Interlingue', 'Interlingue'].freeze, + ig: ['Igbo', 'Asụsụ Igbo'].freeze, + ii: ['Nuosu', 'ꆈꌠ꒿ Nuosuhxop'].freeze, + ik: ['Inupiaq', 'Iñupiaq'].freeze, + io: ['Ido', 'Ido'].freeze, + is: ['Icelandic', 'Íslenska'].freeze, + it: ['Italian', 'Italiano'].freeze, + iu: ['Inuktitut', 'ᐃᓄᒃᑎᑐᑦ'].freeze, + ja: ['Japanese', '日本語'].freeze, + jv: ['Javanese', 'basa Jawa'].freeze, + ka: ['Georgian', 'ქართული'].freeze, + kg: ['Kongo', 'Kikongo'].freeze, + ki: ['Kikuyu', 'Gĩkũyũ'].freeze, + kj: ['Kwanyama', 'Kuanyama'].freeze, + kk: ['Kazakh', 'қазақ тілі'].freeze, + kl: ['Kalaallisut', 'kalaallisut'].freeze, + km: ['Khmer', 'ខេមរភាសា'].freeze, + kn: ['Kannada', 'ಕನ್ನಡ'].freeze, + ko: ['Korean', '한국어'].freeze, + kr: ['Kanuri', 'Kanuri'].freeze, + ks: ['Kashmiri', 'कश्मीरी'].freeze, + ku: ['Kurdish', 'Kurdî'].freeze, + kv: ['Komi', 'коми кыв'].freeze, + kw: ['Cornish', 'Kernewek'].freeze, + ky: ['Kyrgyz', 'Кыргызча'].freeze, + la: ['Latin', 'latine'].freeze, + lb: ['Luxembourgish', 'Lëtzebuergesch'].freeze, + lg: ['Ganda', 'Luganda'].freeze, + li: ['Limburgish', 'Limburgs'].freeze, + ln: ['Lingala', 'Lingála'].freeze, + lo: ['Lao', 'ພາສາ'].freeze, + lt: ['Lithuanian', 'lietuvių kalba'].freeze, + lu: ['Luba-Katanga', 'Tshiluba'].freeze, + lv: ['Latvian', 'latviešu valoda'].freeze, + mg: ['Malagasy', 'fiteny malagasy'].freeze, + mh: ['Marshallese', 'Kajin M̧ajeļ'].freeze, + mi: ['Māori', 'te reo Māori'].freeze, + mk: ['Macedonian', 'македонски јазик'].freeze, + ml: ['Malayalam', 'മലയാളം'].freeze, + mn: ['Mongolian', 'Монгол хэл'].freeze, + mr: ['Marathi', 'मराठी'].freeze, + ms: ['Malay', 'Bahasa Malaysia'].freeze, + mt: ['Maltese', 'Malti'].freeze, + my: ['Burmese', 'ဗမာစာ'].freeze, + na: ['Nauru', 'Ekakairũ Naoero'].freeze, + nb: ['Norwegian Bokmål', 'Norsk bokmål'].freeze, + nd: ['Northern Ndebele', 'isiNdebele'].freeze, + ne: ['Nepali', 'नेपाली'].freeze, + ng: ['Ndonga', 'Owambo'].freeze, + nl: ['Dutch', 'Nederlands'].freeze, + nn: ['Norwegian Nynorsk', 'Norsk nynorsk'].freeze, + no: ['Norwegian', 'Norsk'].freeze, + nr: ['Southern Ndebele', 'isiNdebele'].freeze, + nv: ['Navajo', 'Diné bizaad'].freeze, + ny: ['Chichewa', 'chiCheŵa'].freeze, + oc: ['Occitan', 'occitan'].freeze, + oj: ['Ojibwe', 'ᐊᓂᔑᓈᐯᒧᐎᓐ'].freeze, + om: ['Oromo', 'Afaan Oromoo'].freeze, + or: ['Oriya', 'ଓଡ଼ିଆ'].freeze, + os: ['Ossetian', 'ирон æвзаг'].freeze, + pa: ['Panjabi', 'ਪੰਜਾਬੀ'].freeze, + pi: ['Pāli', 'पाऴि'].freeze, + pl: ['Polish', 'Polski'].freeze, + ps: ['Pashto', 'پښتو'].freeze, + pt: ['Portuguese', 'Português'].freeze, + qu: ['Quechua', 'Runa Simi'].freeze, + rm: ['Romansh', 'rumantsch grischun'].freeze, + rn: ['Kirundi', 'Ikirundi'].freeze, + ro: ['Romanian', 'Română'].freeze, + ru: ['Russian', 'Русский'].freeze, + rw: ['Kinyarwanda', 'Ikinyarwanda'].freeze, + sa: ['Sanskrit', 'संस्कृतम्'].freeze, + sc: ['Sardinian', 'sardu'].freeze, + sd: ['Sindhi', 'सिन्धी'].freeze, + se: ['Northern Sami', 'Davvisámegiella'].freeze, + sg: ['Sango', 'yângâ tî sängö'].freeze, + si: ['Sinhala', 'සිංහල'].freeze, + sk: ['Slovak', 'slovenčina'].freeze, + sl: ['Slovenian', 'slovenščina'].freeze, + sn: ['Shona', 'chiShona'].freeze, + so: ['Somali', 'Soomaaliga'].freeze, + sq: ['Albanian', 'Shqip'].freeze, + sr: ['Serbian', 'српски језик'].freeze, + ss: ['Swati', 'SiSwati'].freeze, + st: ['Southern Sotho', 'Sesotho'].freeze, + su: ['Sundanese', 'Basa Sunda'].freeze, + sv: ['Swedish', 'Svenska'].freeze, + sw: ['Swahili', 'Kiswahili'].freeze, + ta: ['Tamil', 'தமிழ்'].freeze, + te: ['Telugu', 'తెలుగు'].freeze, + tg: ['Tajik', 'тоҷикӣ'].freeze, + th: ['Thai', 'ไทย'].freeze, + ti: ['Tigrinya', 'ትግርኛ'].freeze, + tk: ['Turkmen', 'Türkmen'].freeze, + tl: ['Tagalog', 'Wikang Tagalog'].freeze, + tn: ['Tswana', 'Setswana'].freeze, + to: ['Tonga', 'faka Tonga'].freeze, + tr: ['Turkish', 'Türkçe'].freeze, + ts: ['Tsonga', 'Xitsonga'].freeze, + tt: ['Tatar', 'татар теле'].freeze, + tw: ['Twi', 'Twi'].freeze, + ty: ['Tahitian', 'Reo Tahiti'].freeze, + ug: ['Uyghur', 'ئۇيغۇرچە‎'].freeze, + uk: ['Ukrainian', 'Українська'].freeze, + ur: ['Urdu', 'اردو'].freeze, + uz: ['Uzbek', 'Ўзбек'].freeze, + ve: ['Venda', 'Tshivenḓa'].freeze, + vi: ['Vietnamese', 'Tiếng Việt'].freeze, + vo: ['Volapük', 'Volapük'].freeze, + wa: ['Walloon', 'walon'].freeze, + wo: ['Wolof', 'Wollof'].freeze, + xh: ['Xhosa', 'isiXhosa'].freeze, + yi: ['Yiddish', 'ייִדיש'].freeze, + yo: ['Yoruba', 'Yorùbá'].freeze, + za: ['Zhuang', 'Saɯ cueŋƅ'].freeze, + zh: ['Chinese', '中文'].freeze, + zu: ['Zulu', 'isiZulu'].freeze, + }.freeze + + ISO_639_3 = { + ast: ['Asturian', 'Asturianu'].freeze, + kab: ['Kabyle', 'Taqbaylit'].freeze, + kmr: ['Northern Kurdish', 'Kurmancî'].freeze, + zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze, + }.freeze + + SUPPORTED_LOCALES = {}.merge(ISO_639_1).merge(ISO_639_3).freeze + + # For ISO-639-1 and ISO-639-3 language codes, we have their official + # names, but for some translations, we need the names of the + # regional variants specifically + REGIONAL_LOCALE_NAMES = { 'es-AR': 'Español (Argentina)', 'es-MX': 'Español (México)', - es: 'Español', - et: 'Eesti', - eu: 'Euskara', - fa: 'فارسی', - fi: 'Suomi', - fr: 'Français', - ga: 'Gaeilge', - gd: 'Gàidhlig', - gl: 'Galego', - he: 'עברית', - hi: 'हिन्दी', - hr: 'Hrvatski', - hu: 'Magyar', - hy: 'Հայերեն', - id: 'Bahasa Indonesia', - io: 'Ido', - is: 'Íslenska', - it: 'Italiano', - ja: '日本語', - ka: 'ქართული', - kab: 'Taqbaylit', - kk: 'Қазақша', - kmr: 'Kurmancî', - kn: 'ಕನ್ನಡ', - ko: '한국어', - ku: 'سۆرانی', - lt: 'Lietuvių', - lv: 'Latviešu', - mk: 'Македонски', - ml: 'മലയാളം', - mr: 'मराठी', - ms: 'Bahasa Melayu', - nl: 'Nederlands', - nn: 'Nynorsk', - no: 'Norsk', - oc: 'Occitan', - pl: 'Polski', 'pt-BR': 'Português (Brasil)', 'pt-PT': 'Português (Portugal)', - pt: 'Português', - ro: 'Română', - ru: 'Русский', - sa: 'संस्कृतम्', - sc: 'Sardu', - si: 'සිංහල', - sk: 'Slovenčina', - sl: 'Slovenščina', - sq: 'Shqip', 'sr-Latn': 'Srpski (latinica)', - sr: 'Српски', - sv: 'Svenska', - ta: 'தமிழ்', - te: 'తెలుగు', - th: 'ไทย', - tr: 'Türkçe', - uk: 'Українська', - ur: 'اُردُو', - vi: 'Tiếng Việt', - zgh: 'ⵜⴰⵎⴰⵣⵉⵖⵜ', 'zh-CN': '简体中文', 'zh-HK': '繁體中文(香港)', 'zh-TW': '繁體中文(臺灣)', - zh: '中文', }.freeze - def human_locale(locale) - if locale == 'und' + def native_locale_name(locale) + if locale.blank? || locale == 'und' I18n.t('generic.none') + elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym]) + supported_locale[1] + elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym]) + regional_locale else - HUMAN_LOCALES[locale.to_sym] || locale + locale end end + + def standard_locale_name(locale) + if locale.blank? + I18n.t('generic.none') + elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym]) + supported_locale[0] + else + locale + end + end + + def valid_locale_or_nil(str) + return if str.blank? + + code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP + + return unless valid_locale?(code) + + code + end + + def valid_locale?(locale) + locale.present? && SUPPORTED_LOCALES.key?(locale.to_sym) + end end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 23739d1cd..3d5592867 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -2,7 +2,7 @@ module SettingsHelper def filterable_languages - LanguageDetector.instance.language_names.select(&LanguagesHelper::HUMAN_LOCALES.method(:key?)) + LanguagesHelper::SUPPORTED_LOCALES.keys end def hash_to_object(hash) diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 6316f96d1..e0f04b099 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -72,6 +72,8 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL'; export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION'; export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS'; +export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS'; + const messages = defineMessages({ uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, @@ -85,6 +87,15 @@ export const ensureComposeIsVisible = (getState, routerHistory) => { } }; +export function setComposeToStatus(status, text, spoiler_text) { + return{ + type: COMPOSE_SET_STATUS, + status, + text, + spoiler_text, + }; +}; + export function changeCompose(text) { return { type: COMPOSE_CHANGE, @@ -156,8 +167,9 @@ export function directCompose(account, routerHistory) { export function submitCompose(routerHistory) { return function (dispatch, getState) { - const status = getState().getIn(['compose', 'text'], ''); - const media = getState().getIn(['compose', 'media_attachments']); + const status = getState().getIn(['compose', 'text'], ''); + const media = getState().getIn(['compose', 'media_attachments']); + const statusId = getState().getIn(['compose', 'id'], null); if ((!status || !status.length) && media.size === 0) { return; @@ -165,16 +177,19 @@ export function submitCompose(routerHistory) { dispatch(submitComposeRequest()); - api(getState).post('/api/v1/statuses', { - status, - in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), - media_ids: media.map(item => item.get('id')), - sensitive: getState().getIn(['compose', 'sensitive']), - spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '', - visibility: getState().getIn(['compose', 'privacy']), - poll: getState().getIn(['compose', 'poll'], null), - quote_id: getState().getIn(['compose', 'quote_from'], null), - }, { + api(getState).request({ + url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`, + method: statusId === null ? 'post' : 'put', + data: { + status, + in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), + media_ids: media.map(item => item.get('id')), + sensitive: getState().getIn(['compose', 'sensitive']), + spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '', + visibility: getState().getIn(['compose', 'privacy']), + poll: getState().getIn(['compose', 'poll'], null), + quote_id: getState().getIn(['compose', 'quote_from'], null), + }, headers: { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), }, @@ -196,11 +211,11 @@ export function submitCompose(routerHistory) { } }; - if (response.data.visibility !== 'direct') { + if (statusId === null && response.data.visibility !== 'direct') { insertIfOnline('home'); } - if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { + if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility === 'public') { insertIfOnline('community'); insertIfOnline('public'); insertIfOnline(`account:${response.data.account.id}`); diff --git a/app/javascript/mastodon/actions/history.js b/app/javascript/mastodon/actions/history.js new file mode 100644 index 000000000..c142aaf61 --- /dev/null +++ b/app/javascript/mastodon/actions/history.js @@ -0,0 +1,37 @@ +import api from '../api'; +import { importFetchedAccounts } from './importer'; + +export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST'; +export const HISTORY_FETCH_SUCCESS = 'HISTORY_FETCH_SUCCESS'; +export const HISTORY_FETCH_FAIL = 'HISTORY_FETCH_FAIL'; + +export const fetchHistory = statusId => (dispatch, getState) => { + const loading = getState().getIn(['history', statusId, 'loading']); + + if (loading) { + return; + } + + dispatch(fetchHistoryRequest(statusId)); + + api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => { + dispatch(importFetchedAccounts(data.map(x => x.account))); + dispatch(fetchHistorySuccess(statusId, data)); + }).catch(error => dispatch(fetchHistoryFail(error))); +}; + +export const fetchHistoryRequest = statusId => ({ + type: HISTORY_FETCH_REQUEST, + statusId, +}); + +export const fetchHistorySuccess = (statusId, history) => ({ + type: HISTORY_FETCH_SUCCESS, + statusId, + history, +}); + +export const fetchHistoryFail = error => ({ + type: HISTORY_FETCH_FAIL, + error, +}); diff --git a/app/javascript/mastodon/actions/modal.js b/app/javascript/mastodon/actions/modal.js index 3d0299db5..3e576fab8 100644 --- a/app/javascript/mastodon/actions/modal.js +++ b/app/javascript/mastodon/actions/modal.js @@ -9,9 +9,10 @@ export function openModal(type, props) { }; }; -export function closeModal(type) { +export function closeModal(type, options = { ignoreFocus: false }) { return { type: MODAL_CLOSE, modalType: type, + ignoreFocus: options.ignoreFocus, }; }; diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 39767d090..00e8d74d7 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -34,7 +34,6 @@ export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; - export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; @@ -46,7 +45,7 @@ defineMessages({ }); const fetchRelatedRelationships = (dispatch, notifications) => { - const accountIds = notifications.map(item => item.account.id); + const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id); if (accountIds.length > 0) { dispatch(fetchRelationships(accountIds)); @@ -124,7 +123,18 @@ export function updateNotifications(notification, intlMessages, intlLocale) { const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); const excludeTypesFromFilter = filter => { - const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']); + const allTypes = ImmutableList([ + 'follow', + 'follow_request', + 'favourite', + 'reblog', + 'mention', + 'poll', + 'status', + 'update', + 'admin.sign_up', + ]); + return allTypes.filterNot(item => item === filter).toJS(); }; diff --git a/app/javascript/mastodon/actions/reports.js b/app/javascript/mastodon/actions/reports.js index afa0c3412..fbe5b3791 100644 --- a/app/javascript/mastodon/actions/reports.js +++ b/app/javascript/mastodon/actions/reports.js @@ -1,89 +1,38 @@ import api from '../api'; -import { openModal, closeModal } from './modal'; - -export const REPORT_INIT = 'REPORT_INIT'; -export const REPORT_CANCEL = 'REPORT_CANCEL'; +import { openModal } from './modal'; export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST'; export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS'; export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; -export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE'; -export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE'; -export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE'; +export const initReport = (account, status) => dispatch => + dispatch(openModal('REPORT', { + accountId: account.get('id'), + statusId: status?.get('id'), + })); -export function initReport(account, status) { - return dispatch => { - dispatch({ - type: REPORT_INIT, - account, - status, - }); +export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => { + dispatch(submitReportRequest()); - dispatch(openModal('REPORT')); - }; + api(getState).post('/api/v1/reports', params).then(response => { + dispatch(submitReportSuccess(response.data)); + if (onSuccess) onSuccess(); + }).catch(error => { + dispatch(submitReportFail(error)); + if (onFail) onFail(); + }); }; -export function cancelReport() { - return { - type: REPORT_CANCEL, - }; -}; +export const submitReportRequest = () => ({ + type: REPORT_SUBMIT_REQUEST, +}); -export function toggleStatusReport(statusId, checked) { - return { - type: REPORT_STATUS_TOGGLE, - statusId, - checked, - }; -}; +export const submitReportSuccess = report => ({ + type: REPORT_SUBMIT_SUCCESS, + report, +}); -export function submitReport() { - return (dispatch, getState) => { - dispatch(submitReportRequest()); - - api(getState).post('/api/v1/reports', { - account_id: getState().getIn(['reports', 'new', 'account_id']), - status_ids: getState().getIn(['reports', 'new', 'status_ids']), - comment: getState().getIn(['reports', 'new', 'comment']), - forward: getState().getIn(['reports', 'new', 'forward']), - }).then(response => { - dispatch(closeModal()); - dispatch(submitReportSuccess(response.data)); - }).catch(error => dispatch(submitReportFail(error))); - }; -}; - -export function submitReportRequest() { - return { - type: REPORT_SUBMIT_REQUEST, - }; -}; - -export function submitReportSuccess(report) { - return { - type: REPORT_SUBMIT_SUCCESS, - report, - }; -}; - -export function submitReportFail(error) { - return { - type: REPORT_SUBMIT_FAIL, - error, - }; -}; - -export function changeReportComment(comment) { - return { - type: REPORT_COMMENT_CHANGE, - comment, - }; -}; - -export function changeReportForward(forward) { - return { - type: REPORT_FORWARD_CHANGE, - forward, - }; -}; +export const submitReportFail = error => ({ + type: REPORT_SUBMIT_FAIL, + error, +}); diff --git a/app/javascript/mastodon/actions/rules.js b/app/javascript/mastodon/actions/rules.js new file mode 100644 index 000000000..34e60a121 --- /dev/null +++ b/app/javascript/mastodon/actions/rules.js @@ -0,0 +1,27 @@ +import api from '../api'; + +export const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST'; +export const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS'; +export const RULES_FETCH_FAIL = 'RULES_FETCH_FAIL'; + +export const fetchRules = () => (dispatch, getState) => { + dispatch(fetchRulesRequest()); + + api(getState) + .get('/api/v1/instance').then(({ data }) => dispatch(fetchRulesSuccess(data.rules))) + .catch(err => dispatch(fetchRulesFail(err))); +}; + +const fetchRulesRequest = () => ({ + type: RULES_FETCH_REQUEST, +}); + +const fetchRulesSuccess = rules => ({ + type: RULES_FETCH_SUCCESS, + rules, +}); + +const fetchRulesFail = error => ({ + type: RULES_FETCH_FAIL, + error, +}); diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 7fc65c84b..4f260c738 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -2,7 +2,7 @@ import api from '../api'; import { deleteFromTimelines } from './timelines'; import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer'; -import { ensureComposeIsVisible } from './compose'; +import { ensureComposeIsVisible, setComposeToStatus } from './compose'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; @@ -33,6 +33,10 @@ export const QUOTE_HIDE = 'QUOTE_HIDE'; export const REDRAFT = 'REDRAFT'; +export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST'; +export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS'; +export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL'; + export function fetchStatusRequest(id, skipLoading) { return { type: STATUS_FETCH_REQUEST, @@ -87,6 +91,37 @@ export function redraft(status, raw_text) { }; }; +export const editStatus = (id, routerHistory) => (dispatch, getState) => { + let status = getState().getIn(['statuses', id]); + + if (status.get('poll')) { + status = status.set('poll', getState().getIn(['polls', status.get('poll')])); + } + + dispatch(fetchStatusSourceRequest()); + + api(getState).get(`/api/v1/statuses/${id}/source`).then(response => { + dispatch(fetchStatusSourceSuccess()); + ensureComposeIsVisible(getState, routerHistory); + dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text)); + }).catch(error => { + dispatch(fetchStatusSourceFail(error)); + }); +}; + +export const fetchStatusSourceRequest = () => ({ + type: STATUS_FETCH_SOURCE_REQUEST, +}); + +export const fetchStatusSourceSuccess = () => ({ + type: STATUS_FETCH_SOURCE_SUCCESS, +}); + +export const fetchStatusSourceFail = error => ({ + type: STATUS_FETCH_SOURCE_FAIL, + error, +}); + export function deleteStatus(id, routerHistory, withRedraft = false) { return (dispatch, getState) => { let status = getState().getIn(['statuses', id]); diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js index 853e4f60a..304bbebef 100644 --- a/app/javascript/mastodon/actions/trends.js +++ b/app/javascript/mastodon/actions/trends.js @@ -1,31 +1,94 @@ import api from '../api'; +import { importFetchedStatuses } from './importer'; -export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST'; -export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS'; -export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL'; +export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST'; +export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS'; +export const TRENDS_TAGS_FETCH_FAIL = 'TRENDS_TAGS_FETCH_FAIL'; -export const fetchTrends = () => (dispatch, getState) => { - dispatch(fetchTrendsRequest()); +export const TRENDS_LINKS_FETCH_REQUEST = 'TRENDS_LINKS_FETCH_REQUEST'; +export const TRENDS_LINKS_FETCH_SUCCESS = 'TRENDS_LINKS_FETCH_SUCCESS'; +export const TRENDS_LINKS_FETCH_FAIL = 'TRENDS_LINKS_FETCH_FAIL'; + +export const TRENDS_STATUSES_FETCH_REQUEST = 'TRENDS_STATUSES_FETCH_REQUEST'; +export const TRENDS_STATUSES_FETCH_SUCCESS = 'TRENDS_STATUSES_FETCH_SUCCESS'; +export const TRENDS_STATUSES_FETCH_FAIL = 'TRENDS_STATUSES_FETCH_FAIL'; + +export const fetchTrendingHashtags = () => (dispatch, getState) => { + dispatch(fetchTrendingHashtagsRequest()); api(getState) - .get('/api/v1/trends') - .then(({ data }) => dispatch(fetchTrendsSuccess(data))) - .catch(err => dispatch(fetchTrendsFail(err))); + .get('/api/v1/trends/tags') + .then(({ data }) => dispatch(fetchTrendingHashtagsSuccess(data))) + .catch(err => dispatch(fetchTrendingHashtagsFail(err))); }; -export const fetchTrendsRequest = () => ({ - type: TRENDS_FETCH_REQUEST, +export const fetchTrendingHashtagsRequest = () => ({ + type: TRENDS_TAGS_FETCH_REQUEST, skipLoading: true, }); -export const fetchTrendsSuccess = trends => ({ - type: TRENDS_FETCH_SUCCESS, +export const fetchTrendingHashtagsSuccess = trends => ({ + type: TRENDS_TAGS_FETCH_SUCCESS, trends, skipLoading: true, }); -export const fetchTrendsFail = error => ({ - type: TRENDS_FETCH_FAIL, +export const fetchTrendingHashtagsFail = error => ({ + type: TRENDS_TAGS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, +}); + +export const fetchTrendingLinks = () => (dispatch, getState) => { + dispatch(fetchTrendingLinksRequest()); + + api(getState) + .get('/api/v1/trends/links') + .then(({ data }) => dispatch(fetchTrendingLinksSuccess(data))) + .catch(err => dispatch(fetchTrendingLinksFail(err))); +}; + +export const fetchTrendingLinksRequest = () => ({ + type: TRENDS_LINKS_FETCH_REQUEST, + skipLoading: true, +}); + +export const fetchTrendingLinksSuccess = trends => ({ + type: TRENDS_LINKS_FETCH_SUCCESS, + trends, + skipLoading: true, +}); + +export const fetchTrendingLinksFail = error => ({ + type: TRENDS_LINKS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, +}); + +export const fetchTrendingStatuses = () => (dispatch, getState) => { + dispatch(fetchTrendingStatusesRequest()); + + api(getState).get('/api/v1/trends/statuses').then(({ data }) => { + dispatch(importFetchedStatuses(data)); + dispatch(fetchTrendingStatusesSuccess(data)); + }).catch(err => dispatch(fetchTrendingStatusesFail(err))); +}; + +export const fetchTrendingStatusesRequest = () => ({ + type: TRENDS_STATUSES_FETCH_REQUEST, + skipLoading: true, +}); + +export const fetchTrendingStatusesSuccess = statuses => ({ + type: TRENDS_STATUSES_FETCH_SUCCESS, + statuses, + skipLoading: true, +}); + +export const fetchTrendingStatusesFail = error => ({ + type: TRENDS_STATUSES_FETCH_FAIL, error, skipLoading: true, skipAlert: true, diff --git a/app/javascript/mastodon/components/check.js b/app/javascript/mastodon/components/check.js new file mode 100644 index 000000000..ee2ef1595 --- /dev/null +++ b/app/javascript/mastodon/components/check.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const Check = () => ( + + + +); + +export default Check; diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 7d0588901..4b4ad8355 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -6,6 +6,8 @@ import Overlay from 'react-overlays/lib/Overlay'; import Motion from '../features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import { supportsPassiveEvents } from 'detect-passive-events'; +import classNames from 'classnames'; +import { CircularProgress } from 'mastodon/components/loading_indicator'; const listenerOptions = supportsPassiveEvents ? { passive: true } : false; let id = 0; @@ -17,13 +19,18 @@ class DropdownMenu extends React.PureComponent { }; static propTypes = { - items: PropTypes.array.isRequired, + items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired, + loading: PropTypes.bool, + scrollable: PropTypes.bool, onClose: PropTypes.func.isRequired, style: PropTypes.object, placement: PropTypes.string, arrowOffsetLeft: PropTypes.string, arrowOffsetTop: PropTypes.string, openedViaKeyboard: PropTypes.bool, + renderItem: PropTypes.func, + renderHeader: PropTypes.func, + onItemClick: PropTypes.func.isRequired, }; static defaultProps = { @@ -45,9 +52,11 @@ class DropdownMenu extends React.PureComponent { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('keydown', this.handleKeyDown, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + if (this.focusedItem && this.props.openedViaKeyboard) { this.focusedItem.focus({ preventScroll: true }); } + this.setState({ mounted: true }); } @@ -66,7 +75,7 @@ class DropdownMenu extends React.PureComponent { } handleKeyDown = e => { - const items = Array.from(this.node.getElementsByTagName('a')); + const items = Array.from(this.node.querySelectorAll('a, button')); const index = items.indexOf(document.activeElement); let element = null; @@ -109,21 +118,11 @@ class DropdownMenu extends React.PureComponent { } handleClick = e => { - const i = Number(e.currentTarget.getAttribute('data-index')); - const { action, to } = this.props.items[i]; - - this.props.onClose(); - - if (typeof action === 'function') { - e.preventDefault(); - action(e); - } else if (to) { - e.preventDefault(); - this.context.router.history.push(to); - } + const { onItemClick } = this.props; + onItemClick(e); } - renderItem (option, i) { + renderItem = (option, i) => { if (option === null) { return
  • ; } @@ -140,9 +139,11 @@ class DropdownMenu extends React.PureComponent { } render () { - const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props; + const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props; const { mounted } = this.state; + let renderItem = this.props.renderItem || this.renderItem; + return ( {({ opacity, scaleX, scaleY }) => ( @@ -152,9 +153,23 @@ class DropdownMenu extends React.PureComponent {
    -
      - {items.map((option, i) => this.renderItem(option, i))} -
    +
    + {loading && ( + + )} + + {!loading && renderHeader && ( +
    + {renderHeader(items)} +
    + )} + + {!loading && ( +
      + {items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))} +
    + )} +
    )} @@ -170,11 +185,14 @@ export default class Dropdown extends React.PureComponent { }; static propTypes = { - icon: PropTypes.string.isRequired, - items: PropTypes.array.isRequired, - size: PropTypes.number.isRequired, + children: PropTypes.node, + icon: PropTypes.string, + items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired, + loading: PropTypes.bool, + size: PropTypes.number, title: PropTypes.string, disabled: PropTypes.bool, + scrollable: PropTypes.bool, status: ImmutablePropTypes.map, isUserTouching: PropTypes.func, onOpen: PropTypes.func.isRequired, @@ -182,6 +200,9 @@ export default class Dropdown extends React.PureComponent { dropdownPlacement: PropTypes.string, openDropdownId: PropTypes.number, openedViaKeyboard: PropTypes.bool, + renderItem: PropTypes.func, + renderHeader: PropTypes.func, + onItemClick: PropTypes.func, }; static defaultProps = { @@ -237,17 +258,21 @@ export default class Dropdown extends React.PureComponent { } handleItemClick = e => { + const { onItemClick } = this.props; const i = Number(e.currentTarget.getAttribute('data-index')); - const { action, to } = this.props.items[i]; + const item = this.props.items[i]; this.handleClose(); - if (typeof action === 'function') { + if (typeof onItemClick === 'function') { e.preventDefault(); - action(); - } else if (to) { + onItemClick(item, i); + } else if (item && typeof item.action === 'function') { e.preventDefault(); - this.context.router.history.push(to); + item.action(); + } else if (item && item.to) { + e.preventDefault(); + this.context.router.history.push(item.to); } } @@ -265,29 +290,67 @@ export default class Dropdown extends React.PureComponent { } } + close = () => { + this.handleClose(); + } + render () { - const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props; + const { + icon, + items, + size, + title, + disabled, + loading, + scrollable, + dropdownPlacement, + openDropdownId, + openedViaKeyboard, + children, + renderItem, + renderHeader, + } = this.props; + const open = this.state.id === openDropdownId; + const button = children ? React.cloneElement(React.Children.only(children), { + ref: this.setTargetRef, + onClick: this.handleClick, + onMouseDown: this.handleMouseDown, + onKeyDown: this.handleButtonKeyDown, + onKeyPress: this.handleKeyPress, + }) : ( + + ); + return ( -
    - + + {button} - + -
    + ); } diff --git a/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js b/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js new file mode 100644 index 000000000..e30c18372 --- /dev/null +++ b/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux'; +import { openDropdownMenu, closeDropdownMenu } from 'mastodon/actions/dropdown_menu'; +import { fetchHistory } from 'mastodon/actions/history'; +import DropdownMenu from 'mastodon/components/dropdown_menu'; + +const mapStateToProps = (state, { statusId }) => ({ + dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), + openDropdownId: state.getIn(['dropdown_menu', 'openId']), + openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), + items: state.getIn(['history', statusId, 'items']), + loading: state.getIn(['history', statusId, 'loading']), +}); + +const mapDispatchToProps = (dispatch, { statusId }) => ({ + + onOpen (id, onItemClick, dropdownPlacement, keyboard) { + dispatch(fetchHistory(statusId)); + dispatch(openDropdownMenu(id, dropdownPlacement, keyboard)); + }, + + onClose (id) { + dispatch(closeDropdownMenu(id)); + }, + +}); + +export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu); diff --git a/app/javascript/mastodon/components/edited_timestamp/index.js b/app/javascript/mastodon/components/edited_timestamp/index.js new file mode 100644 index 000000000..bebf93886 --- /dev/null +++ b/app/javascript/mastodon/components/edited_timestamp/index.js @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage, injectIntl } from 'react-intl'; +import Icon from 'mastodon/components/icon'; +import DropdownMenu from './containers/dropdown_menu_container'; +import { connect } from 'react-redux'; +import { openModal } from 'mastodon/actions/modal'; +import RelativeTimestamp from 'mastodon/components/relative_timestamp'; +import InlineAccount from 'mastodon/components/inline_account'; + +const mapDispatchToProps = (dispatch, { statusId }) => ({ + + onItemClick (index) { + dispatch(openModal('COMPARE_HISTORY', { index, statusId })); + }, + +}); + +export default @connect(null, mapDispatchToProps) +@injectIntl +class EditedTimestamp extends React.PureComponent { + + static propTypes = { + statusId: PropTypes.string.isRequired, + timestamp: PropTypes.string.isRequired, + intl: PropTypes.object.isRequired, + onItemClick: PropTypes.func.isRequired, + }; + + handleItemClick = (item, i) => { + const { onItemClick } = this.props; + onItemClick(i); + }; + + renderHeader = items => { + return ( + + ); + } + + renderItem = (item, index, { onClick, onKeyPress }) => { + const formattedDate = ; + const formattedName = ; + + const label = item.get('original') ? ( + + ) : ( + + ); + + return ( +
  • + +
  • + ); + } + + render () { + const { timestamp, intl, statusId } = this.props; + + return ( + + + + ); + } + +} diff --git a/app/javascript/mastodon/components/hashtag.js b/app/javascript/mastodon/components/hashtag.js index a793a32f5..7f442d189 100644 --- a/app/javascript/mastodon/components/hashtag.js +++ b/app/javascript/mastodon/components/hashtag.js @@ -38,7 +38,7 @@ class SilentErrorBoundary extends React.Component { * * @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element} */ -const accountsCountRenderer = (displayNumber, pluralReady) => ( +export const accountsCountRenderer = (displayNumber, pluralReady) => ( +