diff --git a/.browserslistrc b/.browserslistrc index 0376af4bccd..6367e4d3581 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,6 +1,7 @@ [production] defaults > 0.2% +firefox >= 78 ios >= 15.6 not dead not OperaMini all diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c6dcc4d46a3..3aa0bbf7da4 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -11,5 +11,8 @@ RUN apt-get update && \ export DEBIAN_FRONTEND=noninteractive && \ apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libvips42 libpam-dev +# Disable download prompt for Corepack +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 + # Move welcome message to where VS Code expects it COPY .devcontainer/welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt diff --git a/.devcontainer/codespaces/devcontainer.json b/.devcontainer/codespaces/devcontainer.json index 8acffec8259..d2358657f6d 100644 --- a/.devcontainer/codespaces/devcontainer.json +++ b/.devcontainer/codespaces/devcontainer.json @@ -39,7 +39,7 @@ }, "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", - "postCreateCommand": "COREPACK_ENABLE_DOWNLOAD_PROMPT=0 bin/setup", + "postCreateCommand": "bin/setup", "waitFor": "postCreateCommand", "customizations": { diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml index 1e2e1ba7de2..f87082013c6 100644 --- a/.devcontainer/compose.yaml +++ b/.devcontainer/compose.yaml @@ -69,7 +69,7 @@ services: hard: -1 libretranslate: - image: libretranslate/libretranslate:v1.5.7 + image: libretranslate/libretranslate:v1.6.0 restart: unless-stopped volumes: - lt-data:/home/libretranslate/.local diff --git a/.eslintrc.js b/.eslintrc.js index d1182628263..b6e4253e61d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -316,7 +316,7 @@ module.exports = defineConfig({ ], parserOptions: { - project: true, + projectService: true, tsconfigRootDir: __dirname, }, diff --git a/.github/codecov.yml b/.github/codecov.yml index 701ba3af8f7..21af6d0d457 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -9,3 +9,5 @@ coverage: default: # GitHub status check is not blocking informational: true +github_checks: + annotations: false diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 968c77cac27..8a106762838 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -7,6 +7,7 @@ ':prConcurrentLimitNone', // Remove limit for open PRs at any time. ':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour. ], + rebaseWhen: 'conflicted', minimumReleaseAge: '3', // Wait 3 days after the package has been published before upgrading it // packageRules order is important, they are applied from top to bottom and are merged, // meaning the most important ones must be at the bottom, for example grouping rules diff --git a/.github/workflows/crowdin-download.yml b/.github/workflows/crowdin-download.yml index 0faa7e4939f..f1817b3e9a1 100644 --- a/.github/workflows/crowdin-download.yml +++ b/.github/workflows/crowdin-download.yml @@ -52,7 +52,7 @@ jobs: # Create or update the pull request - name: Create Pull Request - uses: peter-evans/create-pull-request@v6.0.5 + uses: peter-evans/create-pull-request@v7.0.1 with: commit-message: 'New Crowdin translations' title: 'New Crowdin Translations (automated)' diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index fcfeed5fbad..3da53c1ae8e 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -42,11 +42,24 @@ jobs: with: onlyProduction: 'true' + - name: Cache assets from compilation + uses: actions/cache@v4 + with: + path: | + public/assets + public/packs + public/packs-test + tmp/cache/webpacker + key: ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }} + restore-keys: | + ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }} + ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }} + ${{ matrix.mode }}-assets-main + ${{ matrix.mode }}-assets + - name: Precompile assets - # Previously had set this, but it's not supported - # export NODE_OPTIONS=--openssl-legacy-provider run: |- - ./bin/rails assets:precompile + bin/rails assets:precompile - name: Archive asset artifacts run: | @@ -137,6 +150,19 @@ jobs: bin/rails db:setup bin/flatware fan bin/rails db:test:prepare + - name: Cache RSpec persistence file + uses: actions/cache@v4 + with: + path: | + tmp/rspec/examples.txt + key: rspec-persistence-${{ github.head_ref || github.ref_name }}-${{ github.sha }} + restore-keys: | + rspec-persistence-${{ github.head_ref || github.ref_name }}-${{ github.sha }}-${{ matrix.ruby-version }} + rspec-persistence-${{ github.head_ref || github.ref_name }}-${{ github.sha }} + rspec-persistence-${{ github.head_ref || github.ref_name }} + rspec-persistence-main + rspec-persistence + - run: bin/flatware rspec -r ./spec/flatware_helper.rb - name: Upload coverage reports to Codecov diff --git a/.gitignore b/.gitignore index a70f30f9528..a74317bd7d8 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,6 @@ docker-compose.override.yml # Ignore dotenv .local files .env*.local + +# Ignore local-only rspec configuration +.rspec-local diff --git a/.profile b/.profile deleted file mode 100644 index f4826ea3033..00000000000 --- a/.profile +++ /dev/null @@ -1 +0,0 @@ -LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio:/app/.apt/usr/lib/x86_64-linux-gnu/openblas-pthread diff --git a/.rspec b/.rspec index 9a8e706d091..83e16f80447 100644 --- a/.rspec +++ b/.rspec @@ -1,3 +1,2 @@ --color --require spec_helper ---format Fuubar diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 09acb795bf2..a6e51d6aee1 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.65.0. +# using RuboCop version 1.66.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -35,7 +35,6 @@ Rails/OutputSafety: # Configuration parameters: AllowedVars. Style/FetchEnvVar: Exclude: - - 'app/lib/redis_configuration.rb' - 'app/lib/translation_service.rb' - 'config/environments/production.rb' - 'config/initializers/2_limited_federation_mode.rb' @@ -44,7 +43,6 @@ Style/FetchEnvVar: - 'config/initializers/devise.rb' - 'config/initializers/paperclip.rb' - 'config/initializers/vapid.rb' - - 'lib/mastodon/redis_config.rb' - 'lib/tasks/repo.rake' # This cop supports safe autocorrection (--autocorrect). @@ -93,7 +91,6 @@ Style/OptionalBooleanParameter: - 'app/services/fetch_resource_service.rb' - 'app/workers/domain_block_worker.rb' - 'app/workers/unfollow_follow_worker.rb' - - 'lib/mastodon/redis_config.rb' # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. diff --git a/.ruby-version b/.ruby-version index a0891f563f3..fa7adc7ac72 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.4 +3.3.5 diff --git a/Aptfile b/Aptfile index 5e033f13650..06c91d4c7b0 100644 --- a/Aptfile +++ b/Aptfile @@ -1,5 +1,5 @@ -ffmpeg -libopenblas0-pthread -libpq-dev -libxdamage1 -libxfixes3 +libidn12 +# for idn-ruby on heroku-24 stack + +# use https://github.com/heroku/heroku-buildpack-activestorage-preview +# in place for ffmpeg and its dependent packages to reduce slag size diff --git a/CHANGELOG.md b/CHANGELOG.md index 7388e5b461b..02ac2898ddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -165,7 +165,7 @@ The following changelog entries focus on changes visible to users, administrator - **Change colors throughout the web UI** (#29522, #29584, #29653, #29779, #29803, #29809, #29808, #29828, #31034, #31168, #31266, #31348, #31349, #31361, and #31510 by @ClearlyClaire, @Gargron, @renchap, and @vmstan) - **Change onboarding prompt to follow suggestions carousel in web UI** (#28878 and #29272 by @Gargron) - **Change email templates** (#28416, #28755, #28814, #29064, #28883, #29470, #29607, #29761, #29760, and #29879 by @ClearlyClaire, @Gargron, @hteumeuleu, and @mjankowski)\ - All emails to end-users have been completely redesigned with a fresh new look, providing more information while making them easier to reand and keeping maximum compatibility across mail clients. + All emails to end-users have been completely redesigned with a fresh new look, providing more information while making them easier to read and keeping maximum compatibility across mail clients. - **Change follow recommendations algorithm** (#28314, #28433, #29017, #29108, #29306, #29550, #29619, and #31474 by @ClearlyClaire, @Gargron, @kernal053, @mjankowski, and @wheatear-dev)\ This replaces the “past interactions” recommendation algorithm with a “friends of friends” algorithm that suggests accounts followed by people you follow, and a “similar profiles” algorithm that suggests accounts with a profile similar to your most recent follows.\ In addition, the implementation has been significantly reworked, and all follow recommendations are now dismissable.\ diff --git a/Dockerfile b/Dockerfile index cd555f7027d..c7c02d9b467 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ARG BUILDPLATFORM=${BUILDPLATFORM} # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"] # renovate: datasource=docker depName=docker.io/ruby -ARG RUBY_VERSION="3.3.4" +ARG RUBY_VERSION="3.3.5" # # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # renovate: datasource=node-version depName=node ARG NODE_MAJOR_VERSION="20" diff --git a/Gemfile b/Gemfile index 9a6db1ec098..4c808fa4800 100644 --- a/Gemfile +++ b/Gemfile @@ -99,10 +99,10 @@ gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' -gem 'opentelemetry-api', '~> 1.3.0' +gem 'opentelemetry-api', '~> 1.4.0' group :opentelemetry do - gem 'opentelemetry-exporter-otlp', '~> 0.28.0', require: false + gem 'opentelemetry-exporter-otlp', '~> 0.29.0', require: false gem 'opentelemetry-instrumentation-active_job', '~> 0.7.1', require: false gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.20.1', require: false gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.21.2', require: false @@ -126,9 +126,6 @@ group :test do # Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab gem 'rspec-github', '~> 2.4', require: false - # RSpec progress bar formatter - gem 'fuubar', '~> 2.5' - # RSpec helpers for email specs gem 'email_spec' @@ -149,11 +146,13 @@ group :test do gem 'rails-controller-testing', '~> 1.0' # Validate schemas in specs - gem 'json-schema', '~> 4.0' + gem 'json-schema', '~> 5.0' # Test harness fo rack components gem 'rack-test', '~> 2.1' + gem 'shoulda-matchers' + # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false gem 'simplecov', '~> 0.22', require: false gem 'simplecov-lcov', '~> 0.8', require: false @@ -210,7 +209,7 @@ group :development, :test do gem 'test-prof' # RSpec runner for rails - gem 'rspec-rails', '~> 6.0' + gem 'rspec-rails', '~> 7.0' end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index 62f6f091b8c..206178a530f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,35 +10,35 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.1.3.4) - actionpack (= 7.1.3.4) - activesupport (= 7.1.3.4) + actioncable (7.1.4) + actionpack (= 7.1.4) + activesupport (= 7.1.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.3.4) - actionpack (= 7.1.3.4) - activejob (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) + actionmailbox (7.1.4) + actionpack (= 7.1.4) + activejob (= 7.1.4) + activerecord (= 7.1.4) + activestorage (= 7.1.4) + activesupport (= 7.1.4) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.1.3.4) - actionpack (= 7.1.3.4) - actionview (= 7.1.3.4) - activejob (= 7.1.3.4) - activesupport (= 7.1.3.4) + actionmailer (7.1.4) + actionpack (= 7.1.4) + actionview (= 7.1.4) + activejob (= 7.1.4) + activesupport (= 7.1.4) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.2) - actionpack (7.1.3.4) - actionview (= 7.1.3.4) - activesupport (= 7.1.3.4) + actionpack (7.1.4) + actionview (= 7.1.4) + activesupport (= 7.1.4) nokogiri (>= 1.8.5) racc rack (>= 2.2.4) @@ -46,15 +46,15 @@ GEM rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.3.4) - actionpack (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) + actiontext (7.1.4) + actionpack (= 7.1.4) + activerecord (= 7.1.4) + activestorage (= 7.1.4) + activesupport (= 7.1.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.3.4) - activesupport (= 7.1.3.4) + actionview (7.1.4) + activesupport (= 7.1.4) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -64,22 +64,22 @@ GEM activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.1.3.4) - activesupport (= 7.1.3.4) + activejob (7.1.4) + activesupport (= 7.1.4) globalid (>= 0.3.6) - activemodel (7.1.3.4) - activesupport (= 7.1.3.4) - activerecord (7.1.3.4) - activemodel (= 7.1.3.4) - activesupport (= 7.1.3.4) + activemodel (7.1.4) + activesupport (= 7.1.4) + activerecord (7.1.4) + activemodel (= 7.1.4) + activesupport (= 7.1.4) timeout (>= 0.4.0) - activestorage (7.1.3.4) - actionpack (= 7.1.3.4) - activejob (= 7.1.3.4) - activerecord (= 7.1.3.4) - activesupport (= 7.1.3.4) + activestorage (7.1.4) + actionpack (= 7.1.4) + activejob (= 7.1.4) + activerecord (= 7.1.4) + activesupport (= 7.1.4) marcel (~> 1.0) - activesupport (7.1.3.4) + activesupport (7.1.4) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -100,17 +100,17 @@ GEM attr_required (1.0.2) awrence (1.2.1) aws-eventstream (1.3.0) - aws-partitions (1.966.0) - aws-sdk-core (3.201.5) + aws-partitions (1.974.0) + aws-sdk-core (3.205.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.88.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-kms (1.91.0) + aws-sdk-core (~> 3, >= 3.205.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.159.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-s3 (1.162.0) + aws-sdk-core (~> 3, >= 3.205.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.9.1) @@ -137,14 +137,14 @@ GEM blurhash (0.1.7) bootsnap (1.18.4) msgpack (~> 1.2) - brakeman (6.1.2) + brakeman (6.2.1) racc browser (5.3.1) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, < 6) builder (3.3.0) - bundler-audit (0.9.1) + bundler-audit (0.9.2) bundler (>= 1.2.0, < 3) thor (~> 1.0) capybara (3.40.0) @@ -164,20 +164,22 @@ GEM activesupport (>= 5.2) elasticsearch (>= 7.14.0, < 8) elasticsearch-dsl + childprocess (5.1.0) + logger (~> 1.5) chunky_png (1.4.0) climate_control (1.2.0) cocoon (1.2.15) color_diff (0.1) concurrent-ruby (1.3.4) connection_pool (2.4.1) - cose (1.3.0) + cose (1.3.1) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) crack (1.0.0) bigdecimal rexml crass (1.0.6) - css_parser (1.17.1) + css_parser (1.19.0) addressable csv (3.3.0) database_cleaner-active_record (2.2.0) @@ -206,20 +208,21 @@ GEM diff-lcs (1.5.1) discard (1.3.0) activerecord (>= 4.2, < 8) - docile (1.4.0) + docile (1.4.1) domain_name (0.6.20240107) doorkeeper (5.7.1) railties (>= 5) dotenv (3.1.2) drb (2.2.1) ed25519 (1.3.0) - elasticsearch (7.17.10) - elasticsearch-api (= 7.17.10) - elasticsearch-transport (= 7.17.10) - elasticsearch-api (7.17.10) + elasticsearch (7.17.11) + elasticsearch-api (= 7.17.11) + elasticsearch-transport (= 7.17.11) + elasticsearch-api (7.17.11) multi_json elasticsearch-dsl (0.1.10) - elasticsearch-transport (7.17.10) + elasticsearch-transport (7.17.11) + base64 faraday (>= 1, < 3) multi_json email_spec (2.3.0) @@ -251,7 +254,7 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) @@ -264,10 +267,11 @@ GEM ffi-compiler (1.3.2) ffi (>= 1.15.5) rake - flatware (2.3.2) + flatware (2.3.3) + drb thor (< 2.0) - flatware-rspec (2.3.2) - flatware (= 2.3.2) + flatware-rspec (2.3.3) + flatware (= 2.3.3) rspec (>= 3.6) fog-core (2.5.0) builder @@ -284,14 +288,11 @@ GEM fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) - fuubar (2.5.1) - rspec-core (~> 3.0) - ruby-progressbar (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) google-protobuf (3.25.4) - googleapis-common-protos-types (1.14.0) - google-protobuf (~> 3.18) + googleapis-common-protos-types (1.15.0) + google-protobuf (>= 3.18, < 5.a) haml (6.3.0) temple (>= 0.8.2) thor @@ -307,11 +308,12 @@ GEM rainbow rubocop (>= 1.0) sysexits (~> 1.1) - hashdiff (1.1.0) + hashdiff (1.1.1) hashie (5.0.0) hcaptcha (7.1.0) json - highline (3.0.1) + highline (3.1.1) + reline hiredis (0.6.3) hkdf (0.3.0) htmlentities (4.3.4) @@ -342,7 +344,7 @@ GEM rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) idn-ruby (0.1.5) - inline_svg (1.9.0) + inline_svg (1.10.0) activesupport (>= 3.0) nokogiri (>= 1.6) io-console (0.7.2) @@ -368,8 +370,8 @@ GEM json-ld-preloaded (3.3.0) json-ld (~> 3.3) rdf (~> 3.3) - json-schema (4.3.1) - addressable (>= 2.8) + json-schema (5.0.0) + addressable (~> 2.8) jsonapi-renderer (0.2.2) jwt (2.7.1) kaminari (1.2.2) @@ -391,8 +393,9 @@ GEM mime-types terrapin (>= 0.6.0, < 2.0) language_server-protocol (3.17.0.3) - launchy (2.5.2) + launchy (3.0.1) addressable (~> 2.8) + childprocess (~> 5.0) letter_opener (1.10.0) launchy (>= 2.2, < 4) letter_opener_web (3.0.0) @@ -429,19 +432,19 @@ GEM memory_profiler (1.0.2) mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2024.0702) + mime-types-data (3.2024.0820) mini_mime (1.1.5) mini_portile2 (2.8.7) - minitest (5.24.1) + minitest (5.25.1) msgpack (1.7.2) multi_json (1.15.0) - multipart-post (2.4.0) + multipart-post (2.4.1) mutex_m (0.2.0) net-http (0.4.1) uri net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.4.14) + net-imap (0.4.15) date net-protocol net-ldap (0.19.0) @@ -455,7 +458,7 @@ GEM nokogiri (1.16.7) mini_portile2 (~> 2.8.2) racc (~> 1.4) - oj (3.16.5) + oj (3.16.6) bigdecimal (>= 3.0) ostruct (>= 0.2) omniauth (2.1.2) @@ -466,7 +469,7 @@ GEM addressable (~> 2.8) nokogiri (~> 1.12) omniauth (~> 2.1) - omniauth-rails_csrf_protection (1.0.1) + omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) omniauth-saml (2.1.0) @@ -489,10 +492,10 @@ GEM openssl (3.2.0) openssl-signature_algorithm (1.3.0) openssl (> 2.0) - opentelemetry-api (1.3.0) - opentelemetry-common (0.20.1) + opentelemetry-api (1.4.0) + opentelemetry-common (0.21.0) opentelemetry-api (~> 1.0) - opentelemetry-exporter-otlp (0.28.1) + opentelemetry-exporter-otlp (0.29.0) google-protobuf (>= 3.18) googleapis-common-protos-types (~> 1.3) opentelemetry-api (~> 1.1) @@ -580,25 +583,25 @@ GEM orm_adapter (0.5.0) ostruct (0.6.0) ox (2.14.18) - parallel (1.25.1) - parser (3.3.4.0) + parallel (1.26.3) + parser (3.3.5.0) ast (~> 2.4.1) racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.5.7) + pg (1.5.8) pghero (3.6.0) activerecord (>= 6.1) - premailer (1.23.0) + premailer (1.27.0) addressable - css_parser (>= 1.12.0) + css_parser (>= 1.19.0) htmlentities (>= 4.0.0) premailer-rails (1.12.0) actionmailer (>= 3) net-smtp premailer (~> 1.7, >= 1.7.9) - propshaft (0.9.1) + propshaft (1.0.0) actionpack (>= 7.0.0) activesupport (>= 7.0.0) rack @@ -608,7 +611,7 @@ GEM public_suffix (6.0.1) puma (6.4.2) nio4r (~> 2.0) - pundit (2.3.2) + pundit (2.4.0) activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) @@ -635,20 +638,20 @@ GEM rackup (1.0.0) rack (< 3) webrick - rails (7.1.3.4) - actioncable (= 7.1.3.4) - actionmailbox (= 7.1.3.4) - actionmailer (= 7.1.3.4) - actionpack (= 7.1.3.4) - actiontext (= 7.1.3.4) - actionview (= 7.1.3.4) - activejob (= 7.1.3.4) - activemodel (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) + rails (7.1.4) + actioncable (= 7.1.4) + actionmailbox (= 7.1.4) + actionmailer (= 7.1.4) + actionpack (= 7.1.4) + actiontext (= 7.1.4) + actionview (= 7.1.4) + activejob (= 7.1.4) + activemodel (= 7.1.4) + activerecord (= 7.1.4) + activestorage (= 7.1.4) + activesupport (= 7.1.4) bundler (>= 1.15.0) - railties (= 7.1.3.4) + railties (= 7.1.4) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -663,9 +666,9 @@ GEM rails-i18n (7.0.9) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.1.3.4) - actionpack (= 7.1.3.4) - activesupport (= 7.1.3.4) + railties (7.1.4) + actionpack (= 7.1.4) + activesupport (= 7.1.4) irb rackup (>= 1.0.0) rake (>= 12.2) @@ -688,17 +691,16 @@ GEM redlock (1.3.2) redis (>= 3.0.0, < 6.0) regexp_parser (2.9.2) - reline (0.5.9) + reline (0.5.10) io-console (~> 0.5) request_store (1.6.0) rack (>= 1.4) responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.3.4) - strscan + rexml (3.3.7) rotp (6.3.0) - rouge (4.2.1) + rouge (4.3.0) rpam2 (4.0.2) rqrcode (2.2.0) chunky_png (~> 1.0) @@ -708,9 +710,9 @@ GEM rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.0) + rspec-core (3.13.1) rspec-support (~> 3.13.0) - rspec-expectations (3.13.1) + rspec-expectations (3.13.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-github (2.4.0) @@ -718,10 +720,10 @@ GEM rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (6.1.4) - actionpack (>= 6.1) - activesupport (>= 6.1) - railties (>= 6.1) + rspec-rails (7.0.1) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) rspec-core (~> 3.13) rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) @@ -732,18 +734,17 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 8) rspec-support (3.13.1) - rubocop (1.65.1) + rubocop (1.66.1) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.4, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.3) + rubocop-ast (1.32.3) parser (>= 3.3.1.0) rubocop-capybara (2.21.0) rubocop (~> 1.41) @@ -780,13 +781,15 @@ GEM scenic (1.8.0) activerecord (>= 4.0.0) railties (>= 4.0.0) - selenium-webdriver (4.23.0) + selenium-webdriver (4.24.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) semantic_range (3.0.0) + shoulda-matchers (6.4.0) + activesupport (>= 5.2.0) sidekiq (6.5.12) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) @@ -821,7 +824,6 @@ GEM stringio (3.1.1) strong_migrations (2.0.0) activerecord (>= 6.1) - strscan (3.1.0) swd (1.3.0) activesupport (>= 3) attr_required (>= 0.0.5) @@ -832,11 +834,11 @@ GEM unicode-display_width (>= 1.1.1, < 3) terrapin (1.0.1) climate_control - test-prof (1.4.0) - thor (1.3.1) - tilt (2.3.0) + test-prof (1.4.2) + thor (1.3.2) + tilt (2.4.0) timeout (0.4.1) - tpm-key_attestation (0.12.0) + tpm-key_attestation (0.12.1) bindata (~> 2.4) openssl (> 2.0) openssl-signature_algorithm (~> 1.0) @@ -855,13 +857,13 @@ GEM unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2024.1) + tzinfo-data (1.2024.2) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext unf_ext (0.0.9.1) unicode-display_width (2.5.0) - uri (0.13.0) + uri (0.13.1) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) @@ -900,7 +902,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.17) + zeitwerk (2.6.18) PLATFORMS ruby @@ -943,7 +945,6 @@ DEPENDENCIES flatware-rspec fog-core (<= 2.5.0) fog-openstack (~> 1.0) - fuubar (~> 2.5) haml-rails (~> 2.0) haml_lint hcaptcha (~> 7.1) @@ -959,7 +960,7 @@ DEPENDENCIES irb (~> 1.8) json-ld json-ld-preloaded (~> 3.2) - json-schema (~> 4.0) + json-schema (~> 5.0) kaminari (~> 1.2) kt-paperclip (~> 7.2) letter_opener (~> 1.8) @@ -980,8 +981,8 @@ DEPENDENCIES omniauth-rails_csrf_protection (~> 1.0) omniauth-saml (~> 2.0) omniauth_openid_connect (~> 0.6.1) - opentelemetry-api (~> 1.3.0) - opentelemetry-exporter-otlp (~> 0.28.0) + opentelemetry-api (~> 1.4.0) + opentelemetry-exporter-otlp (~> 0.29.0) opentelemetry-instrumentation-active_job (~> 0.7.1) opentelemetry-instrumentation-active_model_serializers (~> 0.20.1) opentelemetry-instrumentation-concurrent_ruby (~> 0.21.2) @@ -1018,7 +1019,7 @@ DEPENDENCIES redis-namespace (~> 1.10) rqrcode (~> 2.2) rspec-github (~> 2.4) - rspec-rails (~> 6.0) + rspec-rails (~> 7.0) rspec-sidekiq (~> 5.0) rubocop rubocop-capybara @@ -1033,6 +1034,7 @@ DEPENDENCIES sanitize (~> 6.0) scenic (~> 1.7) selenium-webdriver + shoulda-matchers sidekiq (~> 6.5) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 5.0) @@ -1056,7 +1058,7 @@ DEPENDENCIES xorcist (~> 1.1) RUBY VERSION - ruby 3.3.2p78 + ruby 3.3.4p94 BUNDLED WITH - 2.5.11 + 2.5.18 diff --git a/Procfile b/Procfile index d15c835b867..f033fd36c6b 100644 --- a/Procfile +++ b/Procfile @@ -11,4 +11,4 @@ worker: bundle exec sidekiq # # and let the main app use the separate app: # -# heroku config:set STREAMING_API_BASE_URL=wss://.herokuapp.com -a +# heroku config:set STREAMING_API_BASE_URL=wss://.herokuapp.com -a diff --git a/app.json b/app.json index 4f05a64f516..5e5a3dc1e7b 100644 --- a/app.json +++ b/app.json @@ -90,9 +90,15 @@ } }, "buildpacks": [ + { + "url": "https://github.com/heroku/heroku-buildpack-activestorage-preview" + }, { "url": "https://github.com/heroku/heroku-buildpack-apt" }, + { + "url": "heroku/nodejs" + }, { "url": "heroku/ruby" } @@ -100,5 +106,6 @@ "scripts": { "postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed" }, - "addons": ["heroku-postgresql", "heroku-redis"] + "addons": ["heroku-postgresql", "heroku-redis"], + "stack": "heroku-24" } diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb index 8b6c1a4454e..a3c4adf59a7 100644 --- a/app/controllers/admin/account_moderation_notes_controller.rb +++ b/app/controllers/admin/account_moderation_notes_controller.rb @@ -13,7 +13,7 @@ module Admin redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg') else @account = @account_moderation_note.target_account - @moderation_notes = @account.targeted_moderation_notes.latest + @moderation_notes = @account.targeted_moderation_notes.chronological.includes(:account) @warnings = @account.strikes.custom.latest render 'admin/accounts/show' diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 9beb8fde6b2..7b169ba26a3 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -33,7 +33,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 + @moderation_notes = @account.targeted_moderation_notes.chronological.includes(:account) @warnings = @account.strikes.includes(:target_account, :account, :appeal).latest @domain_block = DomainBlock.rule_for(@account.domain) end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 4b5afbe157c..48685db17a2 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -7,17 +7,12 @@ module Admin layout 'admin' - before_action :set_body_classes before_action :set_cache_headers after_action :verify_authorized private - def set_body_classes - @body_classes = 'admin' - end - def set_cache_headers response.cache_control.replace(private: true, no_store: true) end diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 3a6df662ea2..5b0867dcfba 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -7,12 +7,12 @@ module Admin def index authorize :dashboard, :index? + @pending_appeals_count = Appeal.pending.async_count + @pending_reports_count = Report.unresolved.async_count + @pending_tags_count = Tag.pending_review.async_count + @pending_users_count = User.pending.async_count @system_checks = Admin::SystemCheck.perform(current_user) @time_period = (29.days.ago.to_date...Time.now.utc.to_date) - @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 end end diff --git a/app/controllers/admin/report_notes_controller.rb b/app/controllers/admin/report_notes_controller.rb index b5f04a1caa0..6b16c29fc7d 100644 --- a/app/controllers/admin/report_notes_controller.rb +++ b/app/controllers/admin/report_notes_controller.rb @@ -21,7 +21,7 @@ module Admin redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg') else - @report_notes = @report.notes.includes(:account).order(id: :desc) + @report_notes = @report.notes.chronological.includes(:account) @action_logs = @report.history.includes(:target) @form = Admin::StatusBatchAction.new @statuses = @report.statuses.with_includes diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 00d200d7c88..aa877f1448c 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -13,7 +13,7 @@ module Admin authorize @report, :show? @report_note = @report.notes.new - @report_notes = @report.notes.includes(:account).order(id: :desc) + @report_notes = @report.notes.chronological.includes(:account) @action_logs = @report.history.includes(:target) @form = Admin::StatusBatchAction.new @statuses = @report.statuses.with_includes diff --git a/app/controllers/api/v2_alpha/notifications/accounts_controller.rb b/app/controllers/api/v2_alpha/notifications/accounts_controller.rb new file mode 100644 index 00000000000..9933b63373e --- /dev/null +++ b/app/controllers/api/v2_alpha/notifications/accounts_controller.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class Api::V2Alpha::Notifications::AccountsController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:notifications' } + before_action :require_user! + before_action :set_notifications! + after_action :insert_pagination_headers, only: :index + + def index + @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer + end + + private + + def load_accounts + @paginated_notifications.map(&:from_account) + end + + def set_notifications! + @paginated_notifications = begin + current_account + .notifications + .without_suspended + .where(group_key: params[:notification_group_key]) + .includes(from_account: [:account_stat, :user]) + .paginate_by_max_id( + limit_param(DEFAULT_ACCOUNTS_LIMIT), + params[:max_id], + params[:since_id] + ) + end + end + + def next_path + api_v2_alpha_notification_accounts_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + api_v2_alpha_notification_accounts_url pagination_params(min_id: pagination_since_id) unless @paginated_notifications.empty? + end + + def pagination_collection + @paginated_notifications + end + + def records_continue? + @paginated_notifications.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) + end +end diff --git a/app/controllers/api/v2_alpha/notifications_controller.rb b/app/controllers/api/v2_alpha/notifications_controller.rb index d0205ad6af8..e8aa0b9e498 100644 --- a/app/controllers/api/v2_alpha/notifications_controller.rb +++ b/app/controllers/api/v2_alpha/notifications_controller.rb @@ -13,7 +13,6 @@ class Api::V2Alpha::NotificationsController < Api::BaseController def index with_read_replica do @notifications = load_notifications - @group_metadata = load_group_metadata @grouped_notifications = load_grouped_notifications @relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id) @presenter = GroupedNotificationsPresenter.new(@grouped_notifications, expand_accounts: expand_accounts_param) @@ -34,7 +33,7 @@ class Api::V2Alpha::NotificationsController < Api::BaseController 'app.notification_grouping.expand_accounts_param' => expand_accounts_param ) - render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata, expand_accounts: expand_accounts_param + render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, expand_accounts: expand_accounts_param end end @@ -42,13 +41,13 @@ class Api::V2Alpha::NotificationsController < Api::BaseController limit = limit_param(DEFAULT_NOTIFICATIONS_COUNT_LIMIT, MAX_NOTIFICATIONS_COUNT_LIMIT) with_read_replica do - render json: { count: browserable_account_notifications.paginate_groups_by_min_id(limit, min_id: notification_marker&.last_read_id).count } + render json: { count: browserable_account_notifications.paginate_groups_by_min_id(limit, min_id: notification_marker&.last_read_id, grouped_types: params[:grouped_types]).count } end end def show - @notification = current_account.notifications.without_suspended.find_by!(group_key: params[:id]) - presenter = GroupedNotificationsPresenter.new([NotificationGroup.from_notification(@notification)]) + @notification = current_account.notifications.without_suspended.find_by!(group_key: params[:group_key]) + presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification])) render json: presenter, serializer: REST::DedupNotificationGroupSerializer end @@ -58,7 +57,7 @@ class Api::V2Alpha::NotificationsController < Api::BaseController end def dismiss - current_account.notifications.where(group_key: params[:id]).destroy_all + current_account.notifications.where(group_key: params[:group_key]).destroy_all render_empty end @@ -68,7 +67,7 @@ class Api::V2Alpha::NotificationsController < Api::BaseController MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_notifications') do notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id( limit_param(DEFAULT_NOTIFICATIONS_LIMIT), - params_slice(:max_id, :since_id, :min_id) + params.slice(:max_id, :since_id, :min_id, :grouped_types).permit(:max_id, :since_id, :min_id, grouped_types: []) ) Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses| @@ -77,22 +76,11 @@ class Api::V2Alpha::NotificationsController < Api::BaseController end end - def load_group_metadata - return {} if @notifications.empty? - - MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_group_metadata') do - browserable_account_notifications - .where(group_key: @notifications.filter_map(&:group_key)) - .where(id: (@notifications.last.id)..(@notifications.first.id)) - .group(:group_key) - .pluck(:group_key, 'min(notifications.id) as min_id', 'max(notifications.id) as max_id', 'max(notifications.created_at) as latest_notification_at') - .to_h { |group_key, min_id, max_id, latest_notification_at| [group_key, { min_id: min_id, max_id: max_id, latest_notification_at: latest_notification_at }] } - end - end - def load_grouped_notifications + return [] if @notifications.empty? + MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_grouped_notifications') do - @notifications.map { |notification| NotificationGroup.from_notification(notification, max_id: @group_metadata.dig(notification.group_key, :max_id)) } + NotificationGroup.from_notifications(@notifications, pagination_range: (@notifications.last.id)..(@notifications.first.id), grouped_types: params[:grouped_types]) end end @@ -125,11 +113,11 @@ class Api::V2Alpha::NotificationsController < Api::BaseController end def browserable_params - params.permit(:include_filtered, types: [], exclude_types: []) + params.slice(:include_filtered, :types, :exclude_types, :grouped_types).permit(:include_filtered, types: [], exclude_types: [], grouped_types: []) end def pagination_params(core_params) - params.slice(:limit, :types, :exclude_types, :include_filtered).permit(:limit, :include_filtered, types: [], exclude_types: []).merge(core_params) + params.slice(:limit, :include_filtered, :types, :exclude_types, :grouped_types).permit(:limit, :include_filtered, types: [], exclude_types: [], grouped_types: []).merge(core_params) end def expand_accounts_param diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index c12960934e3..4d94c801586 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -11,7 +11,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController 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_body_classes, only: [:new, :create, :edit, :update] before_action :require_not_suspended!, only: [:update] before_action :set_cache_headers, only: [:edit, :update] before_action :set_rules, only: :new @@ -104,10 +103,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController private - def set_body_classes - @body_classes = 'admin' if %w(edit update).include?(action_name) - end - def set_invite @invite = begin invite = Invite.find_by(code: invite_code) if invite_code.present? diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index d63bcc85c95..b75f3e35819 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -20,7 +20,7 @@ module AccountControllerConcern webfinger_account_link, actor_url_link, ] - ) + ).to_s end def webfinger_account_link diff --git a/app/controllers/concerns/api/pagination.rb b/app/controllers/concerns/api/pagination.rb index 7f06dc02023..b0b4ae4603f 100644 --- a/app/controllers/concerns/api/pagination.rb +++ b/app/controllers/concerns/api/pagination.rb @@ -19,7 +19,7 @@ module Api::Pagination links = [] links << [next_path, [%w(rel next)]] if next_path links << [prev_path, [%w(rel prev)]] if prev_path - response.headers['Link'] = LinkHeader.new(links) unless links.empty? + response.headers['Link'] = LinkHeader.new(links).to_s unless links.empty? end def require_valid_pagination_options! diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index b8c909877b6..e1f599dcb0c 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -8,6 +8,16 @@ module WebAppControllerConcern before_action :redirect_unauthenticated_to_permalinks! before_action :set_app_body_class + + content_security_policy do |p| + policy = ContentSecurityPolicy.new + + if policy.sso_host.present? + p.form_action policy.sso_host + else + p.form_action :none + end + end end def skip_csrf_meta_tags? diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb index 1054f3db805..dd24a1b7408 100644 --- a/app/controllers/disputes/base_controller.rb +++ b/app/controllers/disputes/base_controller.rb @@ -7,16 +7,11 @@ class Disputes::BaseController < ApplicationController skip_before_action :require_functional! - before_action :set_body_classes before_action :authenticate_user! before_action :set_cache_headers private - def set_body_classes - @body_classes = 'admin' - end - def set_cache_headers response.cache_control.replace(private: true, no_store: true) end diff --git a/app/controllers/filters/statuses_controller.rb b/app/controllers/filters/statuses_controller.rb index 94993f938b5..7ada13f680d 100644 --- a/app/controllers/filters/statuses_controller.rb +++ b/app/controllers/filters/statuses_controller.rb @@ -6,7 +6,6 @@ class Filters::StatusesController < ApplicationController before_action :authenticate_user! before_action :set_filter before_action :set_status_filters - before_action :set_body_classes before_action :set_cache_headers PER_PAGE = 20 @@ -42,10 +41,6 @@ class Filters::StatusesController < ApplicationController 'remove' if params[:remove] end - def set_body_classes - @body_classes = 'admin' - end - def set_cache_headers response.cache_control.replace(private: true, no_store: true) end diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index bd9964426b8..8c4e867e93a 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -5,7 +5,6 @@ class FiltersController < ApplicationController before_action :authenticate_user! before_action :set_filter, only: [:edit, :update, :destroy] - before_action :set_body_classes before_action :set_cache_headers def index @@ -52,10 +51,6 @@ class FiltersController < ApplicationController params.require(:custom_filter).permit(:title, :expires_in, :filter_action, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) end - def set_body_classes - @body_classes = 'admin' - end - def set_cache_headers response.cache_control.replace(private: true, no_store: true) end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 9bc5164d599..070852695ee 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -6,7 +6,6 @@ class InvitesController < ApplicationController layout 'admin' before_action :authenticate_user! - before_action :set_body_classes before_action :set_cache_headers def index @@ -47,10 +46,6 @@ class InvitesController < ApplicationController params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment) end - def set_body_classes - @body_classes = 'admin' - end - def set_cache_headers response.cache_control.replace(private: true, no_store: true) end diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 53eee40012a..9d10468e693 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -19,9 +19,7 @@ class MediaController < ApplicationController redirect_to @media_attachment.file.url(:original) end - def player - @body_classes = 'player' - end + def player; end private diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 7bb22453ca0..267409a9ce4 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -6,7 +6,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :store_current_location before_action :authenticate_resource_owner! before_action :require_not_suspended!, only: :destroy - before_action :set_body_classes before_action :set_cache_headers before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json } @@ -23,10 +22,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio private - def set_body_classes - @body_classes = 'admin' - end - def store_current_location store_location_for(:user, request.url) end diff --git a/app/controllers/redirect/base_controller.rb b/app/controllers/redirect/base_controller.rb index 90894ec1ed8..34558a41261 100644 --- a/app/controllers/redirect/base_controller.rb +++ b/app/controllers/redirect/base_controller.rb @@ -4,7 +4,6 @@ class Redirect::BaseController < ApplicationController vary_by 'Accept-Language' before_action :set_resource - before_action :set_app_body_class def show @redirect_path = ActivityPub::TagManager.instance.url_for(@resource) @@ -14,10 +13,6 @@ class Redirect::BaseController < ApplicationController private - def set_app_body_class - @body_classes = 'app-body' - end - def set_resource raise NotImplementedError end diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index dd794f3199e..d351afcfb72 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -6,7 +6,6 @@ class RelationshipsController < ApplicationController before_action :authenticate_user! before_action :set_accounts, only: :show before_action :set_relationships, only: :show - before_action :set_body_classes before_action :set_cache_headers helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? @@ -68,10 +67,6 @@ class RelationshipsController < ApplicationController end end - def set_body_classes - @body_classes = 'admin' - end - def set_cache_headers response.cache_control.replace(private: true, no_store: true) end diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index f15140aa2be..188334ac234 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -4,15 +4,10 @@ class Settings::BaseController < ApplicationController layout 'admin' before_action :authenticate_user! - before_action :set_body_classes before_action :set_cache_headers private - def set_body_classes - @body_classes = 'admin' - end - def set_cache_headers response.cache_control.replace(private: true, no_store: true) end diff --git a/app/controllers/settings/verifications_controller.rb b/app/controllers/settings/verifications_controller.rb index fc4f23bb186..4e0663253cc 100644 --- a/app/controllers/settings/verifications_controller.rb +++ b/app/controllers/settings/verifications_controller.rb @@ -2,14 +2,30 @@ class Settings::VerificationsController < Settings::BaseController before_action :set_account + before_action :set_verified_links - def show - @verified_links = @account.fields.select(&:verified?) + def show; end + + def update + if UpdateAccountService.new.call(@account, account_params) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg') + else + render :show + end end private + def account_params + params.require(:account).permit(:attribution_domains_as_text) + end + def set_account @account = current_account end + + def set_verified_links + @verified_links = @account.fields.select(&:verified?) + end end diff --git a/app/controllers/severed_relationships_controller.rb b/app/controllers/severed_relationships_controller.rb index 168e85e3fe4..965753a26f8 100644 --- a/app/controllers/severed_relationships_controller.rb +++ b/app/controllers/severed_relationships_controller.rb @@ -4,7 +4,6 @@ class SeveredRelationshipsController < ApplicationController layout 'admin' before_action :authenticate_user! - before_action :set_body_classes before_action :set_cache_headers before_action :set_event, only: [:following, :followers] @@ -51,10 +50,6 @@ class SeveredRelationshipsController < ApplicationController account.local? ? account.local_username_and_domain : account.acct end - def set_body_classes - @body_classes = 'admin' - end - def set_cache_headers response.cache_control.replace(private: true, no_store: true) end diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb index 6546b849780..1aa0ce5a0d4 100644 --- a/app/controllers/shares_controller.rb +++ b/app/controllers/shares_controller.rb @@ -4,13 +4,6 @@ class SharesController < ApplicationController layout 'modal' before_action :authenticate_user! - before_action :set_body_classes def show; end - - private - - def set_body_classes - @body_classes = 'modal-layout compose-standalone' - end end diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index 4a3fc10ca4f..e517bf3ae88 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -5,7 +5,6 @@ class StatusesCleanupController < ApplicationController before_action :authenticate_user! before_action :set_policy - before_action :set_body_classes before_action :set_cache_headers def show; end @@ -34,10 +33,6 @@ class StatusesCleanupController < ApplicationController params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :min_favs, :min_reblogs) end - def set_body_classes - @body_classes = 'admin' - end - def set_cache_headers response.cache_control.replace(private: true, no_store: true) end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index db7eddd78b3..341b0e64729 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -11,7 +11,6 @@ class StatusesController < ApplicationController before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status before_action :redirect_to_original, only: :show - before_action :set_body_classes, only: :embed after_action :set_link_headers @@ -51,12 +50,10 @@ class StatusesController < ApplicationController private - def set_body_classes - @body_classes = 'with-modals' - end - def set_link_headers - response.headers['Link'] = LinkHeader.new([[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]) + response.headers['Link'] = LinkHeader.new( + [[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]] + ).to_s end def set_status diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index 158a0815e12..d804566c935 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -19,14 +19,6 @@ module AccountsHelper end end - def account_action_button(account) - return if account.memorial? || account.moved? - - link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do - safe_join([logo_as_symbol, t('accounts.follow')]) - end - end - def account_formatted_stat(value) number_to_human(value, precision: 3, strip_insignificant_zeros: true) end diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb index 79fee44dc4b..c7a59660cf7 100644 --- a/app/helpers/admin/trends/statuses_helper.rb +++ b/app/helpers/admin/trends/statuses_helper.rb @@ -5,7 +5,7 @@ module Admin::Trends::StatusesHelper text = if status.local? status.text.split("\n").first else - Nokogiri::HTML(status.text).css('html > body > *').first&.text + Nokogiri::HTML5(status.text).css('html > body > *').first&.text end return '' if text.blank? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2799ceecb59..de00f76d362 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -86,7 +86,7 @@ module ApplicationHelper def html_title safe_join( [content_for(:page_title).to_s.chomp, title] - .select(&:present?), + .compact_blank, ' - ' ) end @@ -106,11 +106,16 @@ module ApplicationHelper end def material_symbol(icon, attributes = {}) - inline_svg_tag( - "400-24px/#{icon}.svg", - class: ['icon', "material-#{icon}"].concat(attributes[:class].to_s.split), - role: :img, - data: attributes[:data] + safe_join( + [ + inline_svg_tag( + "400-24px/#{icon}.svg", + class: ['icon', "material-#{icon}"].concat(attributes[:class].to_s.split), + role: :img, + data: attributes[:data] + ), + ' ', + ] ) end @@ -154,6 +159,7 @@ module ApplicationHelper def body_classes output = body_class_string.split + output << content_for(:body_classes) output << "theme-#{current_theme.parameterize}" output << 'system-font' if current_account&.user&.setting_system_font_ui output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion') diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index cbefe0fe538..a0c1781d240 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -41,6 +41,7 @@ module ContextHelper 'cipherText' => 'toot:cipherText', }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, + attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } }, }.freeze def full_context diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 932a3420db9..ba096427cfc 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -200,14 +200,6 @@ module JsonLdHelper nil end - def merge_context(context, new_context) - if context.is_a?(Array) - context << new_context - else - [context, new_context] - end - end - def response_successful?(response) (200...300).cover?(response.code) end diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index 9e1c0a7db1d..b6c09b73147 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -238,9 +238,7 @@ module LanguagesHelper # Helper for self.sorted_locale_keys private_class_method def self.locale_name_for_sorting(locale) - if locale.blank? || locale == 'und' - '000' - elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym]) + if (supported_locale = SUPPORTED_LOCALES[locale.to_sym]) ASCIIFolding.new.fold(supported_locale[1]).downcase elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym]) ASCIIFolding.new.fold(regional_locale).downcase diff --git a/app/helpers/media_component_helper.rb b/app/helpers/media_component_helper.rb index fa8f34fb4d3..60ccdd08359 100644 --- a/app/helpers/media_component_helper.rb +++ b/app/helpers/media_component_helper.rb @@ -57,26 +57,6 @@ module MediaComponentHelper end end - def render_card_component(status, **options) - component_params = { - sensitive: sensitive_viewer?(status, current_account), - card: serialize_status_card(status).as_json, - }.merge(**options) - - react_component :card, component_params - end - - def render_poll_component(status, **options) - component_params = { - disabled: true, - poll: serialize_status_poll(status).as_json, - }.merge(**options) - - react_component :poll, component_params do - render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? } - end - end - private def serialize_media_attachment(attachment) @@ -86,22 +66,6 @@ module MediaComponentHelper ) end - def serialize_status_card(status) - ActiveModelSerializers::SerializableResource.new( - status.preview_card, - serializer: REST::PreviewCardSerializer - ) - end - - def serialize_status_poll(status) - ActiveModelSerializers::SerializableResource.new( - status.preloadable_poll, - serializer: REST::PollSerializer, - scope: current_user, - scope_name: :current_user - ) - end - def sensitive_viewer?(status, account) if !account.nil? && account.id == status.account_id status.sensitive diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index d956e4fcd8f..bba6d64a478 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -4,6 +4,13 @@ module StatusesHelper EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_ACTION = 'embed' + VISIBLITY_ICONS = { + public: 'globe', + unlisted: 'lock_open', + private: 'lock', + direct: 'alternate_email', + }.freeze + def nothing_here(extra_classes = '') content_tag(:div, class: "nothing-here #{extra_classes}") do t('accounts.nothing_here') @@ -57,17 +64,8 @@ module StatusesHelper embedded_view? ? '_blank' : nil end - def fa_visibility_icon(status) - case status.visibility - when 'public' - material_symbol 'globe' - when 'unlisted' - material_symbol 'lock_open' - when 'private' - material_symbol 'lock' - when 'direct' - material_symbol 'alternate_email' - end + def visibility_icon(status) + VISIBLITY_ICONS[status.visibility.to_sym] end def embedded_view? diff --git a/app/javascript/entrypoints/embed.tsx b/app/javascript/entrypoints/embed.tsx new file mode 100644 index 00000000000..f8c824d287a --- /dev/null +++ b/app/javascript/entrypoints/embed.tsx @@ -0,0 +1,74 @@ +import './public-path'; +import { createRoot } from 'react-dom/client'; + +import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal'; + +import { start } from '../mastodon/common'; +import { Status } from '../mastodon/features/standalone/status'; +import { loadPolyfills } from '../mastodon/polyfills'; +import ready from '../mastodon/ready'; + +start(); + +function loaded() { + const mountNode = document.getElementById('mastodon-status'); + + if (mountNode) { + const attr = mountNode.getAttribute('data-props'); + + if (!attr) return; + + const props = JSON.parse(attr) as { id: string; locale: string }; + const root = createRoot(mountNode); + + root.render(); + } +} + +function main() { + ready(loaded).catch((error: unknown) => { + console.error(error); + }); +} + +loadPolyfills() + .then(main) + .catch((error: unknown) => { + console.error(error); + }); + +interface SetHeightMessage { + type: 'setHeight'; + id: string; + height: number; +} + +function isSetHeightMessage(data: unknown): data is SetHeightMessage { + if ( + data && + typeof data === 'object' && + 'type' in data && + data.type === 'setHeight' + ) + return true; + else return false; +} + +window.addEventListener('message', (e) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases + if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return; + + const data = e.data; + + // We use a timeout to allow for the React page to render before calculating the height + afterInitialRender(() => { + window.parent.postMessage( + { + type: 'setHeight', + id: data.id, + height: document.getElementsByTagName('html')[0]?.scrollHeight, + }, + '*', + ); + }); +}); diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx index b06675c2ee2..d33e00d5da8 100644 --- a/app/javascript/entrypoints/public.tsx +++ b/app/javascript/entrypoints/public.tsx @@ -37,43 +37,6 @@ const messages = defineMessages({ }, }); -interface SetHeightMessage { - type: 'setHeight'; - id: string; - height: number; -} - -function isSetHeightMessage(data: unknown): data is SetHeightMessage { - if ( - data && - typeof data === 'object' && - 'type' in data && - data.type === 'setHeight' - ) - return true; - else return false; -} - -window.addEventListener('message', (e) => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases - if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return; - - const data = e.data; - - ready(() => { - window.parent.postMessage( - { - type: 'setHeight', - id: data.id, - height: document.getElementsByTagName('html')[0]?.scrollHeight, - }, - '*', - ); - }).catch((e: unknown) => { - console.error('Error in setHeightMessage postMessage', e); - }); -}); - function loaded() { const { messages: localeData } = getLocale(); diff --git a/app/javascript/hooks/useRenderSignal.ts b/app/javascript/hooks/useRenderSignal.ts new file mode 100644 index 00000000000..740df4a35a3 --- /dev/null +++ b/app/javascript/hooks/useRenderSignal.ts @@ -0,0 +1,32 @@ +// This hook allows a component to signal that it's done rendering in a way that +// can be used by e.g. our embed code to determine correct iframe height + +let renderSignalReceived = false; + +type Callback = () => void; + +let onInitialRender: Callback; + +export const afterInitialRender = (callback: Callback) => { + if (renderSignalReceived) { + callback(); + } else { + onInitialRender = callback; + } +}; + +export const useRenderSignal = () => { + return () => { + if (renderSignalReceived) { + return; + } + + renderSignalReceived = true; + + if (typeof onInitialRender !== 'undefined') { + window.requestAnimationFrame(() => { + onInitialRender(); + }); + } + }; +}; diff --git a/app/javascript/mastodon/actions/account_notes.ts b/app/javascript/mastodon/actions/account_notes.ts index c2ebaf54a43..9e7d199dc9a 100644 --- a/app/javascript/mastodon/actions/account_notes.ts +++ b/app/javascript/mastodon/actions/account_notes.ts @@ -6,5 +6,4 @@ export const submitAccountNote = createDataLoadingThunk( ({ accountId, note }: { accountId: string; note: string }) => apiSubmitAccountNote(accountId, note), (relationship) => ({ relationship }), - { skipLoading: true }, ); diff --git a/app/javascript/mastodon/actions/languages.js b/app/javascript/mastodon/actions/languages.js deleted file mode 100644 index ad186ba0ccd..00000000000 --- a/app/javascript/mastodon/actions/languages.js +++ /dev/null @@ -1,12 +0,0 @@ -import { saveSettings } from './settings'; - -export const LANGUAGE_USE = 'LANGUAGE_USE'; - -export const useLanguage = language => dispatch => { - dispatch({ - type: LANGUAGE_USE, - language, - }); - - dispatch(saveSettings()); -}; diff --git a/app/javascript/mastodon/actions/markers.ts b/app/javascript/mastodon/actions/markers.ts index 77d91d9b9c3..6254e3f083f 100644 --- a/app/javascript/mastodon/actions/markers.ts +++ b/app/javascript/mastodon/actions/markers.ts @@ -2,6 +2,7 @@ import { debounce } from 'lodash'; import type { MarkerJSON } from 'mastodon/api_types/markers'; import { getAccessToken } from 'mastodon/initial_state'; +import { selectUseGroupedNotifications } from 'mastodon/selectors/settings'; import type { AppDispatch, RootState } from 'mastodon/store'; import { createAppAsyncThunk } from 'mastodon/store/typed_functions'; @@ -64,7 +65,7 @@ export const synchronouslySubmitMarkers = createAppAsyncThunk( client.setRequestHeader('Content-Type', 'application/json'); client.setRequestHeader('Authorization', `Bearer ${accessToken}`); client.send(JSON.stringify(params)); - } catch (e) { + } catch { // Do not make the BeforeUnload handler error out } }, @@ -75,13 +76,8 @@ interface MarkerParam { } function getLastNotificationId(state: RootState): string | undefined { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - const enableBeta = state.settings.getIn( - ['notifications', 'groupingBeta'], - false, - ) as boolean; // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return enableBeta + return selectUseGroupedNotifications(state) ? state.notificationGroups.lastReadId : // @ts-expect-error state.notifications is not yet typed // eslint-disable-next-line @typescript-eslint/no-unsafe-call diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 51f83f1d241..2ee46500ab8 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -18,7 +18,7 @@ import { selectSettingsNotificationsQuickFilterActive, selectSettingsNotificationsShows, } from 'mastodon/selectors/settings'; -import type { AppDispatch } from 'mastodon/store'; +import type { AppDispatch, RootState } from 'mastodon/store'; import { createAppAsyncThunk, createDataLoadingThunk, @@ -32,6 +32,14 @@ function excludeAllTypesExcept(filter: string) { return allNotificationTypes.filter((item) => item !== filter); } +function getExcludedTypes(state: RootState) { + const activeFilter = selectSettingsNotificationsQuickFilterActive(state); + + return activeFilter === 'all' + ? selectSettingsNotificationsExcludedTypes(state) + : excludeAllTypesExcept(activeFilter); +} + function dispatchAssociatedRecords( dispatch: AppDispatch, notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[], @@ -62,17 +70,8 @@ function dispatchAssociatedRecords( export const fetchNotifications = createDataLoadingThunk( 'notificationGroups/fetch', - async (_params, { getState }) => { - const activeFilter = - selectSettingsNotificationsQuickFilterActive(getState()); - - return apiFetchNotifications({ - exclude_types: - activeFilter === 'all' - ? selectSettingsNotificationsExcludedTypes(getState()) - : excludeAllTypesExcept(activeFilter), - }); - }, + async (_params, { getState }) => + apiFetchNotifications({ exclude_types: getExcludedTypes(getState()) }), ({ notifications, accounts, statuses }, { dispatch }) => { dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedStatuses(statuses)); @@ -92,9 +91,11 @@ export const fetchNotifications = createDataLoadingThunk( export const fetchNotificationsGap = createDataLoadingThunk( 'notificationGroups/fetchGap', - async (params: { gap: NotificationGap }) => - apiFetchNotifications({ max_id: params.gap.maxId }), - + async (params: { gap: NotificationGap }, { getState }) => + apiFetchNotifications({ + max_id: params.gap.maxId, + exclude_types: getExcludedTypes(getState()), + }), ({ notifications, accounts, statuses }, { dispatch }) => { dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedStatuses(statuses)); @@ -109,6 +110,7 @@ export const pollRecentNotifications = createDataLoadingThunk( async (_params, { getState }) => { return apiFetchNotifications({ max_id: undefined, + exclude_types: getExcludedTypes(getState()), // In slow mode, we don't want to include notifications that duplicate the already-displayed ones since_id: usePendingItems ? getState().notificationGroups.groups.find( @@ -183,7 +185,6 @@ export const setNotificationsFilter = createAppAsyncThunk( path: ['notifications', 'quickFilter', 'active'], value: filterType, }); - // dispatch(expandNotifications({ forceLoad: true })); void dispatch(fetchNotifications()); dispatch(saveSettings()); }, diff --git a/app/javascript/mastodon/actions/notifications_migration.tsx b/app/javascript/mastodon/actions/notifications_migration.tsx index c245dc7565a..0d4da765ec3 100644 --- a/app/javascript/mastodon/actions/notifications_migration.tsx +++ b/app/javascript/mastodon/actions/notifications_migration.tsx @@ -1,3 +1,4 @@ +import { selectUseGroupedNotifications } from 'mastodon/selectors/settings'; import { createAppAsyncThunk } from 'mastodon/store'; import { fetchNotifications } from './notification_groups'; @@ -6,13 +7,8 @@ import { expandNotifications } from './notifications'; export const initializeNotifications = createAppAsyncThunk( 'notifications/initialize', (_, { dispatch, getState }) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - const enableBeta = getState().settings.getIn( - ['notifications', 'groupingBeta'], - false, - ) as boolean; - - if (enableBeta) void dispatch(fetchNotifications()); + if (selectUseGroupedNotifications(getState())) + void dispatch(fetchNotifications()); else void dispatch(expandNotifications({})); }, ); diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 86f90d8aba5..7ea7adc5d60 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -49,11 +49,13 @@ export function fetchStatusRequest(id, skipLoading) { }; } -export function fetchStatus(id, forceFetch = false) { +export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) { return (dispatch, getState) => { const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null; - dispatch(fetchContext(id)); + if (alsoFetchContext) { + dispatch(fetchContext(id)); + } if (skipLoading) { return; diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 04f5e6b88c5..03013110c38 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -1,5 +1,7 @@ // @ts-check +import { selectUseGroupedNotifications } from 'mastodon/selectors/settings'; + import { getLocale } from '../locales'; import { connectStream } from '../stream'; @@ -103,7 +105,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti const notificationJSON = JSON.parse(data.payload); dispatch(updateNotifications(notificationJSON, messages, locale)); // TODO: remove this once the groups feature replaces the previous one - if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) { + if(selectUseGroupedNotifications(getState())) { dispatch(processNewNotificationForGroups(notificationJSON)); } break; @@ -112,7 +114,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti const state = getState(); if (state.notifications.top || !state.notifications.mounted) dispatch(expandNotifications({ forceLoad: true, maxId: undefined })); - if(state.settings.getIn(['notifications', 'groupingBeta'], false)) { + if (selectUseGroupedNotifications(state)) { dispatch(refreshStaleNotificationGroups()); } break; @@ -145,11 +147,11 @@ async function refreshHomeTimelineAndNotification(dispatch, getState) { await dispatch(expandHomeTimeline({ maxId: undefined })); // TODO: remove this once the groups feature replaces the previous one - if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) { + if(selectUseGroupedNotifications(getState())) { // TODO: polling for merged notifications try { await dispatch(pollRecentGroupNotifications()); - } catch (error) { + } catch { // TODO } } else { diff --git a/app/javascript/mastodon/common.js b/app/javascript/mastodon/common.js index 28857de5346..c61e02250c2 100644 --- a/app/javascript/mastodon/common.js +++ b/app/javascript/mastodon/common.js @@ -5,7 +5,7 @@ export function start() { try { Rails.start(); - } catch (e) { + } catch { // If called twice } } diff --git a/app/javascript/mastodon/components/copy_paste_text.tsx b/app/javascript/mastodon/components/copy_paste_text.tsx new file mode 100644 index 00000000000..f888acd0f7c --- /dev/null +++ b/app/javascript/mastodon/components/copy_paste_text.tsx @@ -0,0 +1,90 @@ +import { useRef, useState, useCallback } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react'; +import { useTimeout } from 'mastodon/../hooks/useTimeout'; +import { Icon } from 'mastodon/components/icon'; + +export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => { + const inputRef = useRef(null); + const [copied, setCopied] = useState(false); + const [focused, setFocused] = useState(false); + const [setAnimationTimeout] = useTimeout(); + + const handleInputClick = useCallback(() => { + setCopied(false); + + if (inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + inputRef.current.setSelectionRange(0, value.length); + } + }, [setCopied, value]); + + const handleButtonClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + void navigator.clipboard.writeText(value); + inputRef.current?.blur(); + setCopied(true); + setAnimationTimeout(() => { + setCopied(false); + }, 700); + }, + [setCopied, setAnimationTimeout, value], + ); + + const handleKeyUp = useCallback( + (e: React.KeyboardEvent) => { + if (e.key !== ' ') return; + void navigator.clipboard.writeText(value); + setCopied(true); + setAnimationTimeout(() => { + setCopied(false); + }, 700); + }, + [setCopied, setAnimationTimeout, value], + ); + + const handleFocus = useCallback(() => { + setFocused(true); + }, [setFocused]); + + const handleBlur = useCallback(() => { + setFocused(false); + }, [setFocused]); + + return ( +
+