diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml index 5da1ec3a24..ced5ecfe88 100644 --- a/.devcontainer/compose.yaml +++ b/.devcontainer/compose.yaml @@ -9,6 +9,7 @@ services: environment: RAILS_ENV: development NODE_ENV: development + VITE_RUBY_HOST: 0.0.0.0 BIND: 0.0.0.0 BOOTSNAP_CACHE_DIR: /tmp REDIS_HOST: redis @@ -22,11 +23,12 @@ services: ES_PORT: '9200' LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000 LOCAL_DOMAIN: ${LOCAL_DOMAIN:-localhost:3000} + VITE_DEV_SERVER_PUBLIC: ${VITE_DEV_SERVER_PUBLIC:-localhost:3036} # Overrides default command so things don't shut down after the process ends. command: sleep infinity ports: - '3000:3000' - - '3035:3035' + - '3036:3036' - '4000:4000' networks: - external_network diff --git a/.env.production.sample b/.env.production.sample index a32c6519e1..f687053d50 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -71,7 +71,6 @@ DB_PORT=5432 # Generate each with the `RAILS_ENV=production bundle exec rails secret` task (`docker-compose run --rm web bundle exec rails secret` if you use docker compose) # ------- SECRET_KEY_BASE= -OTP_SECRET= # Encryption secrets # ------------------ diff --git a/.github/renovate.json5 b/.github/renovate.json5 index e638b9c548..7b1b25a19b 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -25,26 +25,12 @@ 'tesseract.js', // Requires code changes 'react-hotkeys', // Requires code changes - // Requires Webpacker upgrade or replacement - '@svgr/webpack', - '@types/webpack', - 'babel-loader', - 'compression-webpack-plugin', - 'css-loader', - 'imports-loader', - 'mini-css-extract-plugin', - 'postcss-loader', - 'sass-loader', - 'terser-webpack-plugin', - 'webpack', - 'webpack-assets-manifest', - 'webpack-bundle-analyzer', - 'webpack-dev-server', - 'webpack-cli', - // react-router: Requires manual upgrade 'history', 'react-router-dom', + + // react-spring: Requires manual upgrade when upgrading react + '@react-spring/web', ], matchUpdateTypes: ['major'], dependencyDashboardApproval: true, diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index 1f7f8f93a8..3efa35bc03 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -49,7 +49,7 @@ jobs: public/assets public/packs public/packs-test - tmp/cache/webpacker + tmp/cache/vite 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 }} @@ -63,7 +63,7 @@ jobs: - name: Archive asset artifacts run: | - tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* + tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tmp/cache/vite/last-build*.json - uses: actions/upload-artifact@v4 if: matrix.mode == 'test' @@ -143,7 +143,7 @@ jobs: uses: ./.github/actions/setup-ruby with: ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick libpam-dev + additional-system-dependencies: ffmpeg libpam-dev - name: Load database schema run: | @@ -173,8 +173,8 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - test-libvips: - name: Libvips tests + test-imagemagick: + name: ImageMagick tests runs-on: ubuntu-latest needs: @@ -220,7 +220,7 @@ jobs: CAS_ENABLED: true BUNDLE_WITH: 'pam_authentication test' GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }} - MASTODON_USE_LIBVIPS: true + MASTODON_USE_LIBVIPS: false strategy: fail-fast: false @@ -245,7 +245,7 @@ jobs: uses: ./.github/actions/setup-ruby with: ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg libpam-dev + additional-system-dependencies: ffmpeg imagemagick libpam-dev - name: Load database schema run: './bin/rails db:create db:schema:load db:seed' @@ -324,7 +324,7 @@ jobs: uses: ./.github/actions/setup-ruby with: ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick + additional-system-dependencies: ffmpeg - name: Set up Javascript environment uses: ./.github/actions/setup-javascript @@ -443,7 +443,7 @@ jobs: uses: ./.github/actions/setup-ruby with: ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick + additional-system-dependencies: ffmpeg - name: Set up Javascript environment uses: ./.github/actions/setup-javascript diff --git a/.gitignore b/.gitignore index a74317bd7d..b4fb2c946b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,10 +21,11 @@ /public/system /public/assets /public/packs +/public/packs-dev /public/packs-test .env .env.production -/node_modules/ +node_modules/ /build/ # Ignore Vagrant files diff --git a/.nvmrc b/.nvmrc index 5d621bb2fe..f2a2bc6165 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.15 +22.16 diff --git a/.prettierignore b/.prettierignore index e6859a850d..32a7077e94 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,10 +18,6 @@ !/log/.keep /tmp /coverage -/public/system -/public/assets -/public/packs -/public/packs-test .env .env.production .env.development @@ -60,6 +56,7 @@ docker-compose.override.yml /public/packs /public/packs-test /public/system +/public/vite* # Ignore emoji map file /app/javascript/mastodon/features/emoji/emoji_map.json diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 07ee0167b3..e9a182a8f4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -70,13 +70,6 @@ Style/OptionalBooleanParameter: - 'app/workers/domain_block_worker.rb' - 'app/workers/unfollow_follow_worker.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: short, verbose -Style/PreferredHashMethods: - Exclude: - - 'config/initializers/paperclip.rb' - # This cop supports safe autocorrection (--autocorrect). Style/RedundantConstantBase: Exclude: diff --git a/.ruby-version b/.ruby-version index 6cb9d3dd0d..f9892605c7 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.3 +3.4.4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae125f64f0..89fd3c7995 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,6 +62,11 @@ Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues]. Please use the search function to make sure there are not duplicate bug reports or feature requests. +## Security Issues + +If you believe you have identified a security issue in Mastodon or our own apps, +check [SECURITY]. + ## Translations Translations are community contributed via [Crowdin]. They are periodically @@ -124,3 +129,4 @@ and API docs. Improvements are made via PRs to the [documentation repository]. [GitHub Issues]: https://github.com/mastodon/mastodon/issues [keepachangelog]: https://keepachangelog.com/en/1.0.0/ [Mastodon documentation]: https://docs.joinmastodon.org +[SECURITY]: SECURITY.md diff --git a/Dockerfile b/Dockerfile index 7e9393efea..8d746ef308 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ARG BASE_REGISTRY="docker.io" # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"] # renovate: datasource=docker depName=docker.io/ruby -ARG RUBY_VERSION="3.4.3" +ARG RUBY_VERSION="3.4.4" # # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # renovate: datasource=node-version depName=node ARG NODE_MAJOR_VERSION="22" diff --git a/Gemfile b/Gemfile index 819ed6bf73..6a5f0c5343 100644 --- a/Gemfile +++ b/Gemfile @@ -62,7 +62,7 @@ gem 'inline_svg' gem 'irb', '~> 1.8' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' -gem 'linzer', '~> 0.6.1' +gem 'linzer', '~> 0.7.2' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mime-types', '~> 3.7.0', require: 'mime/types/columnar' gem 'mutex_m' @@ -78,7 +78,6 @@ gem 'rack-cors', '~> 2.0', require: 'rack/cors' gem 'rails-i18n', '~> 8.0' gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] -gem 'redis-namespace', '~> 1.10' gem 'rqrcode', '~> 3.0' gem 'ruby-progressbar', '~> 1.13' gem 'sanitize', '~> 7.0' @@ -95,7 +94,6 @@ gem 'tty-prompt', '~> 0.23', require: false gem 'twitter-text', '~> 3.1.0' gem 'tzinfo-data', '~> 1.2023' gem 'webauthn', '~> 3.0' -gem 'webpacker', '~> 5.4' gem 'webpush', github: 'mastodon/webpush', ref: '9631ac63045cfabddacc69fc06e919b4c13eb913' gem 'json-ld' @@ -230,3 +228,5 @@ gem 'rubyzip', '~> 2.3' gem 'hcaptcha', '~> 7.1' gem 'mail', '~> 2.8' + +gem 'vite_rails', '~> 3.0.19' diff --git a/Gemfile.lock b/Gemfile.lock index b013f3d062..7ae8d412b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -94,7 +94,7 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.3.2) - aws-partitions (1.1087.0) + aws-partitions (1.1103.0) aws-sdk-core (3.215.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -109,7 +109,7 @@ GEM aws-sigv4 (~> 1.5) aws-sigv4 (1.11.0) aws-eventstream (~> 1, >= 1.0.2) - azure-blob (0.5.7) + azure-blob (0.5.8) rexml base64 (0.2.0) bcp47_spec (0.2.1) @@ -124,7 +124,7 @@ GEM binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) blurhash (0.1.8) - bootsnap (1.18.5) + bootsnap (1.18.6) msgpack (~> 1.2) brakeman (7.0.2) racc @@ -148,6 +148,7 @@ GEM case_transform (0.2) activesupport cbor (0.5.9.8) + cgi (0.4.2) charlock_holmes (0.7.9) chewy (7.6.0) activesupport (>= 5.2) @@ -194,7 +195,7 @@ GEM devise_pam_authenticatable2 (9.2.0) devise (>= 4.0.0) rpam2 (~> 4.0) - diff-lcs (1.6.1) + diff-lcs (1.6.2) discard (1.4.0) activerecord (>= 4.2, < 9.0) docile (1.4.1) @@ -203,6 +204,7 @@ GEM railties (>= 5) dotenv (3.1.8) drb (2.2.1) + dry-cli (1.2.0) elasticsearch (7.17.11) elasticsearch-api (= 7.17.11) elasticsearch-transport (= 7.17.11) @@ -227,7 +229,7 @@ GEM fabrication (2.31.0) faker (3.5.1) i18n (>= 1.8.11, < 2) - faraday (2.13.0) + faraday (2.13.1) faraday-net_http (>= 2.0, < 3.5) json logger @@ -261,15 +263,16 @@ GEM fog-core (~> 2.1) fog-json (>= 1.0) formatador (1.1.0) + forwardable (1.3.3) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) - google-protobuf (4.30.2) + google-protobuf (4.31.0) bigdecimal rake (>= 13) - googleapis-common-protos-types (1.19.0) + googleapis-common-protos-types (1.20.0) google-protobuf (>= 3.18, < 5.a) haml (6.3.0) temple (>= 0.8.2) @@ -337,7 +340,7 @@ GEM azure-blob (~> 0.5.2) hashie (~> 5.0) jmespath (1.6.2) - json (2.10.2) + json (2.12.0) json-canonicalization (1.0.0) json-jwt (1.16.7) activesupport (>= 4.2) @@ -381,7 +384,7 @@ GEM marcel (~> 1.0.1) mime-types terrapin (>= 0.6.0, < 2.0) - language_server-protocol (3.17.0.4) + language_server-protocol (3.17.0.5) launchy (3.1.1) addressable (~> 2.8) childprocess (~> 5.0) @@ -395,7 +398,11 @@ GEM rexml link_header (0.0.8) lint_roller (1.1.0) - linzer (0.6.5) + linzer (0.7.2) + cgi (~> 0.4.2) + forwardable (~> 1.3, >= 1.3.3) + logger (~> 1.7, >= 1.7.0) + net-http (~> 0.6.0) openssl (~> 3.0, >= 3.0.0) rack (>= 2.2, < 4.0) starry (~> 0.2) @@ -410,7 +417,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.24.0) + loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -426,9 +433,9 @@ GEM mime-types (3.7.0) logger mime-types-data (~> 3.2025, >= 3.2025.0507) - mime-types-data (3.2025.0507) + mime-types-data (3.2025.0514) mini_mime (1.1.5) - mini_portile2 (2.8.8) + mini_portile2 (2.8.9) minitest (5.25.5) msgpack (1.8.0) multi_json (1.15.0) @@ -503,7 +510,7 @@ GEM opentelemetry-api (~> 1.0) opentelemetry-instrumentation-active_support (~> 0.7) opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-action_pack (0.12.0) + opentelemetry-instrumentation-action_pack (0.12.1) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.23.0) opentelemetry-instrumentation-rack (~> 0.21) @@ -617,7 +624,7 @@ GEM activesupport (>= 7.0.0) rack railties (>= 7.0.0) - psych (5.2.3) + psych (5.2.6) date stringio public_suffix (6.0.2) @@ -627,7 +634,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.14) + rack (2.2.16) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-cors (2.0.2) @@ -695,8 +702,6 @@ GEM psych (>= 4.0.0) redcarpet (3.6.1) redis (4.8.1) - redis-namespace (1.11.0) - redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) regexp_parser (2.10.0) @@ -709,7 +714,7 @@ GEM railties (>= 5.2) rexml (3.4.1) rotp (6.3.0) - rouge (4.5.1) + rouge (4.5.2) rpam2 (4.0.2) rqrcode (3.1.0) chunky_png (~> 1.0) @@ -726,7 +731,7 @@ GEM rspec-support (~> 3.13.0) rspec-github (3.0.0) rspec-core (~> 3.0) - rspec-mocks (3.13.3) + rspec-mocks (3.13.4) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-rails (8.0.0) @@ -743,7 +748,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.3) - rubocop (1.75.5) + rubocop (1.75.7) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -767,12 +772,12 @@ GEM lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0) - rubocop-rails (2.31.0) + rubocop-rails (2.32.0) activesupport (>= 4.2.0) lint_roller (~> 1.1) rack (>= 1.1) rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) rubocop-rspec (3.6.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) @@ -780,7 +785,8 @@ GEM lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-rspec (~> 3.5) - ruby-prof (1.7.1) + ruby-prof (1.7.2) + base64 ruby-progressbar (1.13.0) ruby-saml (1.18.0) nokogiri (>= 1.13.10) @@ -806,7 +812,6 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - semantic_range (3.1.0) shoulda-matchers (6.5.0) activesupport (>= 5.2.0) sidekiq (6.5.12) @@ -892,6 +897,15 @@ GEM validate_url (1.0.15) activemodel (>= 3.0.0) public_suffix + vite_rails (3.0.19) + railties (>= 5.1, < 9) + vite_ruby (~> 3.0, >= 3.2.2) + vite_ruby (3.9.2) + dry-cli (>= 0.7, < 2) + logger (~> 1.6) + mutex_m + rack-proxy (~> 0.6, >= 0.6.1) + zeitwerk (~> 2.2) warden (1.2.9) rack (>= 2.0.9) webauthn (3.4.0) @@ -910,11 +924,6 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webpacker (5.4.4) - activesupport (>= 5.2) - rack-proxy (>= 0.6.1) - railties (>= 5.2) - semantic_range (>= 2.3.0) webrick (1.9.1) websocket (1.2.11) websocket-driver (0.7.7) @@ -991,7 +1000,7 @@ DEPENDENCIES letter_opener (~> 1.8) letter_opener_web (~> 3.0) link_header (~> 0.0) - linzer (~> 0.6.1) + linzer (~> 0.7.2) lograge (~> 0.12) mail (~> 2.8) mario-redis-lock (~> 1.2) @@ -1042,7 +1051,6 @@ DEPENDENCIES rdf-normalize (~> 0.5) redcarpet (~> 3.6) redis (~> 4.5) - redis-namespace (~> 1.10) rqrcode (~> 3.0) rspec-github (~> 3.0) rspec-rails (~> 8.0) @@ -1078,9 +1086,9 @@ DEPENDENCIES tty-prompt (~> 0.23) twitter-text (~> 3.1.0) tzinfo-data (~> 1.2023) + vite_rails (~> 3.0.19) webauthn (~> 3.0) webmock (~> 3.18) - webpacker (~> 5.4) webpush! xorcist (~> 1.1) @@ -1088,4 +1096,4 @@ RUBY VERSION ruby 3.4.1p0 BUNDLED WITH - 2.6.8 + 2.6.9 diff --git a/Procfile.dev b/Procfile.dev index f81333b04c..99de0616c5 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,4 +1,4 @@ web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq stream: env PORT=4000 yarn workspace @mastodon/streaming start -webpack: bin/webpack-dev-server +vite: yarn dev diff --git a/README.md b/README.md index f1c6cd4322..ad264822e2 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,10 @@ Mastodon is a **free, open-source social network server** based on ActivityPub w ### Requirements -- **PostgreSQL** 12+ -- **Redis** 4+ +- **PostgreSQL** 13+ +- **Redis** 6.2+ - **Ruby** 3.2+ -- **Node.js** 18+ +- **Node.js** 20+ The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, and **Scalingo**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation. @@ -93,12 +93,12 @@ accepted into Mastodon, you can request to be paid through our [OpenCollective]. ## License -Copyright (c) 2016-2024 Eugen Rochko (+ [`mastodon authors`](AUTHORS.md)) +Copyright (c) 2016-2025 Eugen Rochko (+ [`mastodon authors`](AUTHORS.md)) Licensed under GNU Affero General Public License as stated in the [LICENSE](LICENSE): ``` -Copyright (c) 2016-2024 Eugen Rochko & other Mastodon contributors +Copyright (c) 2016-2025 Eugen Rochko & other Mastodon contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free diff --git a/app.json b/app.json index 5e5a3dc1e7..fe8415be3c 100644 --- a/app.json +++ b/app.json @@ -17,10 +17,6 @@ "description": "The secret key base", "generator": "secret" }, - "OTP_SECRET": { - "description": "One-time password secret", - "generator": "secret" - }, "SINGLE_USER_MODE": { "description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)", "value": "false", diff --git a/app/controllers/admin/rules_controller.rb b/app/controllers/admin/rules_controller.rb index 837eb91489..58d52e6291 100644 --- a/app/controllers/admin/rules_controller.rb +++ b/app/controllers/admin/rules_controller.rb @@ -7,7 +7,7 @@ module Admin def index authorize :rule, :index? - @rules = Rule.ordered + @rules = Rule.ordered.includes(:translations) end def new @@ -27,7 +27,6 @@ module Admin if @rule.save redirect_to admin_rules_path else - @rules = Rule.ordered render :new end end @@ -50,6 +49,22 @@ module Admin redirect_to admin_rules_path end + def move_up + authorize @rule, :update? + + @rule.move!(-1) + + redirect_to admin_rules_path + end + + def move_down + authorize @rule, :update? + + @rule.move!(+1) + + redirect_to admin_rules_path + end + private def set_rule @@ -58,7 +73,7 @@ module Admin def resource_params params - .expect(rule: [:text, :hint, :priority]) + .expect(rule: [:text, :hint, :priority, translations_attributes: [[:id, :language, :text, :hint, :_destroy]]]) end end end diff --git a/app/controllers/api/fasp/base_controller.rb b/app/controllers/api/fasp/base_controller.rb index 690f7e419a..f786ea1767 100644 --- a/app/controllers/api/fasp/base_controller.rb +++ b/app/controllers/api/fasp/base_controller.rb @@ -42,37 +42,22 @@ class Api::Fasp::BaseController < ApplicationController end def validate_signature! - signature_input = request.headers['signature-input']&.encode('UTF-8') - raise Error, 'signature-input is missing' if signature_input.blank? + raise Error, 'signature-input is missing' if request.headers['signature-input'].blank? + + provider = nil + + Linzer.verify!(request.rack_request, no_older_than: 5.minutes) do |keyid| + provider = Fasp::Provider.find(keyid) + Linzer.new_ed25519_public_key(provider.provider_public_key_pem, keyid) + end - keyid = signature_input.match(KEYID_PATTERN)[1] - provider = Fasp::Provider.find(keyid) - linzer_request = Linzer.new_request( - request.method, - request.original_url, - {}, - { - 'content-digest' => request.headers['content-digest'], - 'signature-input' => signature_input, - 'signature' => request.headers['signature'], - } - ) - message = Linzer::Message.new(linzer_request) - key = Linzer.new_ed25519_public_key(provider.provider_public_key_pem, keyid) - signature = Linzer::Signature.build(message.headers) - Linzer.verify(key, message, signature) @current_provider = provider end def sign_response response.headers['content-digest'] = "sha-256=:#{OpenSSL::Digest.base64digest('sha256', response.body || '')}:" - - linzer_response = Linzer.new_response(response.body, response.status, { 'content-digest' => response.headers['content-digest'] }) - message = Linzer::Message.new(linzer_response) key = Linzer.new_ed25519_key(current_provider.server_private_key_pem) - signature = Linzer.sign(key, message, %w(@status content-digest)) - - response.headers.merge!(signature.to_h) + Linzer.sign!(response, key:, components: %w(@status content-digest)) end def check_fasp_enabled diff --git a/app/controllers/api/v1/instances/rules_controller.rb b/app/controllers/api/v1/instances/rules_controller.rb index 3930eec0dd..2b6e534875 100644 --- a/app/controllers/api/v1/instances/rules_controller.rb +++ b/app/controllers/api/v1/instances/rules_controller.rb @@ -18,6 +18,6 @@ class Api::V1::Instances::RulesController < Api::V1::Instances::BaseController private def set_rules - @rules = Rule.ordered + @rules = Rule.ordered.includes(:translations) end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 0b6f5b3af4..973724cf7c 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -126,7 +126,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_rules - @rules = Rule.ordered + @rules = Rule.ordered.includes(:translations) end def require_rules_acceptance! diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 250573fc7d..2808066aaf 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -177,9 +177,7 @@ class Auth::SessionsController < Devise::SessionsController ) # Only send a notification email every hour at most - return if redis.get("2fa_failure_notification:#{user.id}").present? - - redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour) + return if redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour, get: true).present? UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! end diff --git a/app/controllers/concerns/theming_concern.rb b/app/controllers/concerns/theming_concern.rb index 38b31e932f..892868cb57 100644 --- a/app/controllers/concerns/theming_concern.rb +++ b/app/controllers/concerns/theming_concern.rb @@ -12,7 +12,7 @@ module ThemingConcern def current_skin @current_skin ||= begin skins = Themes.instance.skins_for(current_flavour) - [current_user&.setting_skin, Setting.skin, 'system', 'default'].find { |skin| skins.include?(skin) } + [current_user&.setting_skin, Setting.skin, 'system', 'application'].find { |skin| skins.include?(skin) } end end diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index 5e395786f0..9e303fba2d 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -8,6 +8,7 @@ module WebAppControllerConcern before_action :redirect_unauthenticated_to_permalinks! before_action :set_referer_header + before_action :redirect_to_tos_interstitial! content_security_policy do |p| policy = ContentSecurityPolicy.new @@ -45,6 +46,13 @@ module WebAppControllerConcern protected + def redirect_to_tos_interstitial! + return unless current_user&.require_tos_interstitial? + + @terms_of_service = TermsOfService.published.first + render 'terms_of_service_interstitial/show', layout: 'auth' + end + def set_referer_header response.set_header('Referrer-Policy', Setting.allow_referrer_origin ? 'strict-origin-when-cross-origin' : 'same-origin') end diff --git a/app/controllers/terms_of_service_controller.rb b/app/controllers/terms_of_service_controller.rb index 672fb07915..35b9d45fe8 100644 --- a/app/controllers/terms_of_service_controller.rb +++ b/app/controllers/terms_of_service_controller.rb @@ -4,8 +4,19 @@ class TermsOfServiceController < ApplicationController include WebAppControllerConcern skip_before_action :require_functional! + skip_before_action :redirect_to_tos_interstitial! + + before_action :clear_redirect_interstitial! def show expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end + + private + + def clear_redirect_interstitial! + return unless user_signed_in? + + current_user.update(require_tos_interstitial: false) + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 32beed43ab..90f8fd000c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -248,22 +248,8 @@ module ApplicationHelper end # glitch-soc addition to handle the multiple flavors - def preload_locale_pack - supported_locales = Themes.instance.flavour(current_flavour)['locales'] - preload_pack_asset "locales/#{current_flavour}/#{I18n.locale}-json.js" if supported_locales.include?(I18n.locale.to_s) - end - - def flavoured_javascript_pack_tag(pack_name, **) - javascript_pack_tag("flavours/#{current_flavour}/#{pack_name}", **) - end - - def flavoured_stylesheet_pack_tag(pack_name, **) - stylesheet_pack_tag("flavours/#{current_flavour}/#{pack_name}", **) - end - - def preload_signed_in_js_packs - preload_files = Themes.instance.flavour(current_flavour)&.fetch('signed_in_preload', nil) || [] - safe_join(preload_files.map { |entry| preload_pack_asset entry }) + def flavoured_vite_typescript_tag(pack_name, **) + vite_typescript_tag("#{Themes.instance.flavour(current_flavour)['pack_directory'].delete_prefix('app/javascript/')}/#{pack_name}", **) end private diff --git a/app/helpers/routing_helper.rb b/app/helpers/routing_helper.rb index 22efc5f092..1ff8b992c3 100644 --- a/app/helpers/routing_helper.rb +++ b/app/helpers/routing_helper.rb @@ -4,7 +4,7 @@ module RoutingHelper extend ActiveSupport::Concern include ActionView::Helpers::AssetTagHelper - include Webpacker::Helper + include ViteRails::TagHelpers included do include Rails.application.routes.url_helpers @@ -25,7 +25,7 @@ module RoutingHelper end def frontend_asset_path(source, **) - asset_pack_path("media/#{source}", **) + vite_asset_path(source, **) end def frontend_asset_url(source, **) diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb index 0a4ddbc7c4..e9014f6d84 100644 --- a/app/helpers/theme_helper.rb +++ b/app/helpers/theme_helper.rb @@ -6,11 +6,13 @@ module ThemeHelper if theme == 'system' ''.html_safe.tap do |tags| - tags << stylesheet_pack_tag("skins/#{flavour}/mastodon-light", media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') - tags << stylesheet_pack_tag("skins/#{flavour}/default", media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous') + tags << vite_stylesheet_tag("skins/#{flavour}/mastodon-light.scss", media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') + tags << vite_stylesheet_tag("skins/#{flavour}/application.scss", media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous') end + elsif theme == 'default' + vite_stylesheet_tag "skins/#{flavour}/application.scss", media: 'all', crossorigin: 'anonymous' else - stylesheet_pack_tag "skins/#{flavour}/#{theme}", media: 'all', crossorigin: 'anonymous' + vite_stylesheet_tag "skins/#{flavour}/#{theme}.scss", media: 'all', crossorigin: 'anonymous' end end diff --git a/app/javascript/entrypoints/admin.tsx b/app/javascript/entrypoints/admin.tsx index 225cb16330..a60778f0c0 100644 --- a/app/javascript/entrypoints/admin.tsx +++ b/app/javascript/entrypoints/admin.tsx @@ -1,4 +1,3 @@ -import './public-path'; import { createRoot } from 'react-dom/client'; import Rails from '@rails/ujs'; @@ -273,7 +272,7 @@ async function mountReactComponent(element: Element) { ); const { default: Component } = (await import( - `@/mastodon/components/admin/${componentName}` + `@/mastodon/components/admin/${componentName}.jsx` )) as { default: React.ComponentType }; const root = createRoot(element); diff --git a/app/javascript/entrypoints/application.ts b/app/javascript/entrypoints/application.ts index 1087b1c4cb..6aeb716f76 100644 --- a/app/javascript/entrypoints/application.ts +++ b/app/javascript/entrypoints/application.ts @@ -1,11 +1,6 @@ -import './public-path'; +import { loadLocale } from 'mastodon/locales'; import main from 'mastodon/main'; - -import { start } from '../mastodon/common'; -import { loadLocale } from '../mastodon/locales'; -import { loadPolyfills } from '../mastodon/polyfills'; - -start(); +import { loadPolyfills } from 'mastodon/polyfills'; loadPolyfills() .then(loadLocale) diff --git a/app/javascript/entrypoints/common.js b/app/javascript/entrypoints/common.js deleted file mode 100644 index fce857be92..0000000000 --- a/app/javascript/entrypoints/common.js +++ /dev/null @@ -1,4 +0,0 @@ -/* This file is a hack to have something more reliable than the upstream `common` tag - that is implicitly generated as the common chunk through webpack's `splitChunks` config */ - -import './public-path'; diff --git a/app/javascript/entrypoints/common.ts b/app/javascript/entrypoints/common.ts new file mode 100644 index 0000000000..fd9839e83c --- /dev/null +++ b/app/javascript/entrypoints/common.ts @@ -0,0 +1,3 @@ +import { start } from 'mastodon/common'; + +start(); diff --git a/app/javascript/entrypoints/embed.tsx b/app/javascript/entrypoints/embed.tsx index 6c091e4d07..067396b1f1 100644 --- a/app/javascript/entrypoints/embed.tsx +++ b/app/javascript/entrypoints/embed.tsx @@ -1,15 +1,11 @@ -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'); diff --git a/app/javascript/entrypoints/error.ts b/app/javascript/entrypoints/error.ts index db68484f3a..af9d484de1 100644 --- a/app/javascript/entrypoints/error.ts +++ b/app/javascript/entrypoints/error.ts @@ -1,4 +1,3 @@ -import './public-path'; import ready from '../mastodon/ready'; ready(() => { diff --git a/app/javascript/entrypoints/inert.ts b/app/javascript/entrypoints/inert.ts deleted file mode 100644 index 7c04a97faf..0000000000 --- a/app/javascript/entrypoints/inert.ts +++ /dev/null @@ -1,4 +0,0 @@ -/* Placeholder file to have `inert.scss` compiled by Webpack - This is used by the `wicg-inert` polyfill */ - -import '../styles/inert.scss'; diff --git a/app/javascript/entrypoints/mailer.ts b/app/javascript/entrypoints/mailer.ts deleted file mode 100644 index a2ad5e73ac..0000000000 --- a/app/javascript/entrypoints/mailer.ts +++ /dev/null @@ -1,3 +0,0 @@ -import '../styles/mailer.scss'; - -require.context('../icons'); diff --git a/app/javascript/entrypoints/public-path.ts b/app/javascript/entrypoints/public-path.ts deleted file mode 100644 index ac4b9355b9..0000000000 --- a/app/javascript/entrypoints/public-path.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Dynamically set webpack's loading path depending on a meta header, in order -// to share the same assets regardless of instance configuration. -// See https://webpack.js.org/guides/public-path/#on-the-fly - -function removeOuterSlashes(string: string) { - return string.replace(/^\/*/, '').replace(/\/*$/, ''); -} - -function formatPublicPath(host = '', path = '') { - let formattedHost = removeOuterSlashes(host); - if (formattedHost && !/^http/i.test(formattedHost)) { - formattedHost = `//${formattedHost}`; - } - const formattedPath = removeOuterSlashes(path); - return `${formattedHost}/${formattedPath}/`; -} - -const cdnHost = document.querySelector('meta[name=cdn-host]'); - -__webpack_public_path__ = formatPublicPath( - cdnHost ? cdnHost.content : '', - process.env.PUBLIC_OUTPUT_PATH, -); diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx index 9374d6b2d1..0970fc585e 100644 --- a/app/javascript/entrypoints/public.tsx +++ b/app/javascript/entrypoints/public.tsx @@ -1,7 +1,5 @@ import { createRoot } from 'react-dom/client'; -import './public-path'; - import { IntlMessageFormat } from 'intl-messageformat'; import type { MessageDescriptor, PrimitiveType } from 'react-intl'; import { defineMessages } from 'react-intl'; @@ -10,7 +8,6 @@ import Rails from '@rails/ujs'; import axios from 'axios'; import { throttle } from 'lodash'; -import { start } from '../mastodon/common'; import { timeAgoString } from '../mastodon/components/relative_timestamp'; import emojify from '../mastodon/features/emoji/emoji'; import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions'; @@ -20,8 +17,6 @@ import ready from '../mastodon/ready'; import 'cocoon-js-vanilla'; -start(); - const messages = defineMessages({ usernameTaken: { id: 'username.taken', @@ -153,9 +148,7 @@ function loaded() { const reactComponents = document.querySelectorAll('[data-component]'); if (reactComponents.length > 0) { - import( - /* webpackChunkName: "containers/media_container" */ '../mastodon/containers/media_container' - ) + import('../mastodon/containers/media_container') .then(({ default: MediaContainer }) => { reactComponents.forEach((component) => { Array.from(component.children).forEach((child) => { diff --git a/app/javascript/entrypoints/remote_interaction_helper.ts b/app/javascript/entrypoints/remote_interaction_helper.ts index 419571c896..f50203747d 100644 --- a/app/javascript/entrypoints/remote_interaction_helper.ts +++ b/app/javascript/entrypoints/remote_interaction_helper.ts @@ -8,8 +8,6 @@ and performs no other task. */ -import './public-path'; - import axios from 'axios'; interface JRDLink { diff --git a/app/javascript/entrypoints/share.tsx b/app/javascript/entrypoints/share.tsx index 7926250851..eb2d8eb673 100644 --- a/app/javascript/entrypoints/share.tsx +++ b/app/javascript/entrypoints/share.tsx @@ -1,13 +1,9 @@ -import './public-path'; import { createRoot } from 'react-dom/client'; -import { start } from '../mastodon/common'; import ComposeContainer from '../mastodon/containers/compose_container'; import { loadPolyfills } from '../mastodon/polyfills'; import ready from '../mastodon/ready'; -start(); - function loaded() { const mountNode = document.getElementById('mastodon-compose'); diff --git a/app/javascript/entrypoints/sign_up.ts b/app/javascript/entrypoints/sign_up.ts index 87100be56d..09884476ec 100644 --- a/app/javascript/entrypoints/sign_up.ts +++ b/app/javascript/entrypoints/sign_up.ts @@ -1,4 +1,3 @@ -import './public-path'; import axios from 'axios'; import ready from '../mastodon/ready'; diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js index c1db557115..f1aabe2747 100644 --- a/app/javascript/flavours/glitch/actions/importer/index.js +++ b/app/javascript/flavours/glitch/actions/importer/index.js @@ -69,6 +69,10 @@ export function importFetchedStatuses(statuses) { processStatus(status.reblog); } + if (status.quote?.quoted_status) { + processStatus(status.quote.quoted_status); + } + if (status.poll?.id) { pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id])); } diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 69c755d06f..ece72dd1e4 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -23,12 +23,20 @@ export function normalizeFilterResult(result) { export function normalizeStatus(status, normalOldStatus, settings) { const normalStatus = { ...status }; + normalStatus.account = status.account.id; if (status.reblog && status.reblog.id) { normalStatus.reblog = status.reblog.id; } + if (status.quote?.quoted_status ?? status.quote?.quoted_status_id) { + normalStatus.quote = { + ...status.quote, + quoted_status: status.quote.quoted_status?.id ?? status.quote?.quoted_status_id, + }; + } + if (status.poll && status.poll.id) { normalStatus.poll = status.poll.id; } diff --git a/app/javascript/flavours/glitch/common.js b/app/javascript/flavours/glitch/common.ts similarity index 68% rename from app/javascript/flavours/glitch/common.js rename to app/javascript/flavours/glitch/common.ts index 71f45dbf3b..e621a24e39 100644 --- a/app/javascript/flavours/glitch/common.js +++ b/app/javascript/flavours/glitch/common.ts @@ -1,8 +1,6 @@ import Rails from '@rails/ujs'; export function start() { - require.context('@/images/', true, /\.(jpg|png|svg)$/); - try { Rails.start(); } catch { diff --git a/app/javascript/flavours/glitch/components/account.tsx b/app/javascript/flavours/glitch/components/account.tsx index 66d50c327b..5633863afb 100644 --- a/app/javascript/flavours/glitch/components/account.tsx +++ b/app/javascript/flavours/glitch/components/account.tsx @@ -10,9 +10,12 @@ import { unblockAccount, muteAccount, unmuteAccount, + followAccountSuccess, } from 'flavours/glitch/actions/accounts'; +import { showAlertForError } from 'flavours/glitch/actions/alerts'; import { openModal } from 'flavours/glitch/actions/modal'; import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import { apiFollowAccount } from 'flavours/glitch/api/accounts'; import { Avatar } from 'flavours/glitch/components/avatar'; import { Button } from 'flavours/glitch/components/button'; import { FollowersCounter } from 'flavours/glitch/components/counters'; @@ -23,6 +26,8 @@ import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp import { ShortNumber } from 'flavours/glitch/components/short_number'; import { Skeleton } from 'flavours/glitch/components/skeleton'; import { VerifiedBadge } from 'flavours/glitch/components/verified_badge'; +import { useIdentity } from 'flavours/glitch/identity_context'; +import { me } from 'flavours/glitch/initial_state'; import type { MenuItem } from 'flavours/glitch/models/dropdown_menu'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; @@ -67,10 +72,12 @@ export const Account: React.FC<{ withBio?: boolean; }> = ({ id, size = 46, hidden, minimal, defaultAction, withBio }) => { const intl = useIntl(); + const { signedIn } = useIdentity(); const account = useAppSelector((state) => state.accounts.get(id)); const relationship = useAppSelector((state) => state.relationships.get(id)); const dispatch = useAppDispatch(); const accountUrl = account?.url; + const isRemote = account?.acct !== account?.username; const handleBlock = useCallback(() => { if (relationship?.blocking) { @@ -113,37 +120,74 @@ export const Account: React.FC<{ }, ]; } else if (defaultAction !== 'block') { - const handleAddToLists = () => { - dispatch( - openModal({ - modalType: 'LIST_ADDER', - modalProps: { - accountId: id, - }, - }), - ); - }; + arr = []; - arr = [ - { + if (isRemote && accountUrl) { + arr.push({ + text: intl.formatMessage(messages.openOriginalPage), + href: accountUrl, + }); + } + + if (signedIn) { + const handleAddToLists = () => { + const openAddToListModal = () => { + dispatch( + openModal({ + modalType: 'LIST_ADDER', + modalProps: { + accountId: id, + }, + }), + ); + }; + if (relationship?.following || relationship?.requested || id === me) { + openAddToListModal(); + } else { + dispatch( + openModal({ + modalType: 'CONFIRM_FOLLOW_TO_LIST', + modalProps: { + accountId: id, + onConfirm: () => { + apiFollowAccount(id) + .then((relationship) => { + dispatch( + followAccountSuccess({ + relationship, + alreadyFollowing: false, + }), + ); + openAddToListModal(); + }) + .catch((err: unknown) => { + dispatch(showAlertForError(err)); + }); + }, + }, + }), + ); + } + }; + + arr.push({ text: intl.formatMessage(messages.addToLists), action: handleAddToLists, - }, - ]; - - if (accountUrl) { - arr.unshift( - { - text: intl.formatMessage(messages.openOriginalPage), - href: accountUrl, - }, - null, - ); + }); } } return arr; - }, [dispatch, intl, id, accountUrl, relationship, defaultAction]); + }, [ + dispatch, + intl, + id, + accountUrl, + relationship, + defaultAction, + isRemote, + signedIn, + ]); if (hidden) { return ( diff --git a/app/javascript/flavours/glitch/components/avatar_group.tsx b/app/javascript/flavours/glitch/components/avatar_group.tsx index 2420728542..a629568693 100644 --- a/app/javascript/flavours/glitch/components/avatar_group.tsx +++ b/app/javascript/flavours/glitch/components/avatar_group.tsx @@ -7,10 +7,16 @@ import classNames from 'classnames'; export const AvatarGroup: React.FC<{ compact?: boolean; + avatarHeight?: number; children: React.ReactNode; -}> = ({ children, compact = false }) => ( +}> = ({ children, compact = false, avatarHeight }) => (
{children}
diff --git a/app/javascript/flavours/glitch/components/counters.tsx b/app/javascript/flavours/glitch/components/counters.tsx index 151b25a3f7..8b14d2a822 100644 --- a/app/javascript/flavours/glitch/components/counters.tsx +++ b/app/javascript/flavours/glitch/components/counters.tsx @@ -43,3 +43,17 @@ export const FollowersCounter = ( }} /> ); + +export const FollowersYouKnowCounter = ( + displayNumber: React.ReactNode, + pluralReady: number, +) => ( + {displayNumber}, + }} + /> +); diff --git a/app/javascript/flavours/glitch/components/hover_card_account.tsx b/app/javascript/flavours/glitch/components/hover_card_account.tsx index caec1a84b0..58c0b17acb 100644 --- a/app/javascript/flavours/glitch/components/hover_card_account.tsx +++ b/app/javascript/flavours/glitch/components/hover_card_account.tsx @@ -8,12 +8,17 @@ import { fetchAccount } from 'flavours/glitch/actions/accounts'; import { AccountBio } from 'flavours/glitch/components/account_bio'; import { AccountFields } from 'flavours/glitch/components/account_fields'; import { Avatar } from 'flavours/glitch/components/avatar'; -import { FollowersCounter } from 'flavours/glitch/components/counters'; +import { AvatarGroup } from 'flavours/glitch/components/avatar_group'; +import { + FollowersCounter, + FollowersYouKnowCounter, +} from 'flavours/glitch/components/counters'; import { DisplayName } from 'flavours/glitch/components/display_name'; import { FollowButton } from 'flavours/glitch/components/follow_button'; import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import { Permalink } from 'flavours/glitch/components/permalink'; import { ShortNumber } from 'flavours/glitch/components/short_number'; +import { useFetchFamiliarFollowers } from 'flavours/glitch/features/account_timeline/hooks/familiar_followers'; import { domain } from 'flavours/glitch/initial_state'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; @@ -38,6 +43,8 @@ export const HoverCardAccount = forwardRef< } }, [dispatch, accountId, account]); + const { familiarFollowers } = useFetchFamiliarFollowers({ accountId }); + return (
-
+
+ {familiarFollowers.length > 0 && ( + <> + · +
+ + + {familiarFollowers.slice(0, 3).map((account) => ( + + ))} + +
+ + )}
diff --git a/app/javascript/flavours/glitch/components/more_from_author.jsx b/app/javascript/flavours/glitch/components/more_from_author.jsx deleted file mode 100644 index f2377b450b..0000000000 --- a/app/javascript/flavours/glitch/components/more_from_author.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import PropTypes from 'prop-types'; - -import { FormattedMessage } from 'react-intl'; - -import { IconLogo } from 'flavours/glitch/components/logo'; -import { AuthorLink } from 'flavours/glitch/features/explore/components/author_link'; - -export const MoreFromAuthor = ({ accountId }) => ( -
- - }} /> -
-); - -MoreFromAuthor.propTypes = { - accountId: PropTypes.string.isRequired, -}; diff --git a/app/javascript/flavours/glitch/components/more_from_author.tsx b/app/javascript/flavours/glitch/components/more_from_author.tsx new file mode 100644 index 0000000000..957d97c141 --- /dev/null +++ b/app/javascript/flavours/glitch/components/more_from_author.tsx @@ -0,0 +1,21 @@ +import { FormattedMessage } from 'react-intl'; + +import { IconLogo } from 'flavours/glitch/components/logo'; +import { AuthorLink } from 'flavours/glitch/features/explore/components/author_link'; + +export const MoreFromAuthor: React.FC<{ accountId: string }> = ({ + accountId, +}) => ( + }} + > + {(chunks) => ( +
+ + {chunks} +
+ )} +
+); diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index fb62be9b87..b1c6fbb4ce 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -82,6 +82,7 @@ class Status extends ImmutablePureComponent { id: PropTypes.string, status: ImmutablePropTypes.map, account: ImmutablePropTypes.record, + children: PropTypes.node, previousId: PropTypes.string, nextInReplyToId: PropTypes.string, rootId: PropTypes.string, @@ -111,6 +112,7 @@ class Status extends ImmutablePureComponent { withDismiss: PropTypes.bool, onMoveUp: PropTypes.func, onMoveDown: PropTypes.func, + isQuotedPost: PropTypes.bool, getScrollPosition: PropTypes.func, updateScrollBottom: PropTypes.func, expanded: PropTypes.bool, @@ -440,7 +442,7 @@ class Status extends ImmutablePureComponent { } render () { - const { intl, hidden, featured, unfocusable, unread, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46 } = this.props; + const { intl, hidden, featured, unfocusable, unread, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46, children } = this.props; const { status, @@ -452,6 +454,7 @@ class Status extends ImmutablePureComponent { onOpenMedia, notification, history, + isQuotedPost, ...other } = this.props; let attachments = null; @@ -618,7 +621,7 @@ class Status extends ImmutablePureComponent { ); mediaIcons.push('video-camera'); } - } else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) { + } else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted && !status.get('quote')) { media.push( {(connectReply || connectUp || connectToRoot) &&
} @@ -722,6 +735,8 @@ class Status extends ImmutablePureComponent { {...statusContentProps} /> + {children} + {media} {hashtagBar} @@ -730,13 +745,15 @@ class Status extends ImmutablePureComponent { {/* This is a glitch-soc addition to have a placeholder */} {!expanded && } - + {!isQuotedPost && + + }
diff --git a/app/javascript/flavours/glitch/components/status_action_bar.jsx b/app/javascript/flavours/glitch/components/status_action_bar.jsx index dc62d2309d..3232498204 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.jsx +++ b/app/javascript/flavours/glitch/components/status_action_bar.jsx @@ -18,8 +18,8 @@ import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react'; import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; -import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg'; -import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg'; +import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react'; +import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react'; import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; diff --git a/app/javascript/flavours/glitch/components/status_list.jsx b/app/javascript/flavours/glitch/components/status_list.jsx index 09061f9dc0..95ff8f45e5 100644 --- a/app/javascript/flavours/glitch/components/status_list.jsx +++ b/app/javascript/flavours/glitch/components/status_list.jsx @@ -9,7 +9,7 @@ import { TIMELINE_GAP, TIMELINE_SUGGESTIONS } from 'flavours/glitch/actions/time import { RegenerationIndicator } from 'flavours/glitch/components/regeneration_indicator'; import { InlineFollowSuggestions } from 'flavours/glitch/features/home_timeline/components/inline_follow_suggestions'; -import StatusContainer from '../containers/status_container'; +import { StatusQuoteManager } from '../components/status_quoted'; import { LoadGap } from './load_gap'; import ScrollableList from './scrollable_list'; @@ -114,7 +114,7 @@ export default class StatusList extends ImmutablePureComponent { ); default: return ( - ( - = ({ isError, children }) => { + return ( +
+ + {children} +
+ ); +}; + +const QuoteLink: React.FC<{ + status: Status; +}> = ({ status }) => { + const accountId = status.get('account') as string; + const account = useAppSelector((state) => + accountId ? state.accounts.get(accountId) : undefined, + ); + + const quoteAuthorName = account?.display_name_html; + + if (!quoteAuthorName) { + return null; + } + + const quoteAuthorElement = ( + + ); + const quoteUrl = `/@${account.get('acct')}/${status.get('id') as string}`; + + return ( + + + + + + ); +}; + +type QuoteMap = ImmutableMap<'state' | 'quoted_status', string | null>; + +export const QuotedStatus: React.FC<{ + quote: QuoteMap; + contextType?: string; + variant?: 'full' | 'link'; + nestingLevel?: number; +}> = ({ quote, contextType, nestingLevel = 1, variant = 'full' }) => { + const quotedStatusId = quote.get('quoted_status'); + const state = quote.get('state'); + const status = useAppSelector((state) => + quotedStatusId ? state.statuses.get(quotedStatusId) : undefined, + ); + let quoteError: React.ReactNode = null; + + if (state === 'deleted') { + quoteError = ( + + ); + } else if (state === 'unauthorized') { + quoteError = ( + + ); + } else if (state === 'pending') { + quoteError = ( + + ); + } else if (state === 'rejected' || state === 'revoked') { + quoteError = ( + + ); + } else if (!status || !quotedStatusId) { + quoteError = ( + + ); + } + + if (quoteError) { + return {quoteError}; + } + + if (variant === 'link' && status) { + return ; + } + + const childQuote = status?.get('quote') as QuoteMap | undefined; + const canRenderChildQuote = + childQuote && nestingLevel <= MAX_QUOTE_POSTS_NESTING_LEVEL; + + return ( + + {/* @ts-expect-error Status is not yet typed */} + + {canRenderChildQuote && ( + + )} + + + ); +}; + +interface StatusQuoteManagerProps { + id: string; + contextType?: string; + [key: string]: unknown; +} + +/** + * This wrapper component takes a status ID and, if the associated status + * is a quote post, it renders the quote into `StatusContainer` as a child. + * It passes all other props through to `StatusContainer`. + */ + +export const StatusQuoteManager = (props: StatusQuoteManagerProps) => { + const status = useAppSelector((state) => { + const status = state.statuses.get(props.id); + const reblogId = status?.get('reblog') as string | undefined; + return reblogId ? state.statuses.get(reblogId) : status; + }); + const quote = status?.get('quote') as QuoteMap | undefined; + + if (quote) { + return ( + /* @ts-expect-error Status is not yet typed */ + + + + ); + } + + /* @ts-expect-error Status is not yet typed */ + return ; +}; diff --git a/app/javascript/flavours/glitch/entrypoints/admin.tsx b/app/javascript/flavours/glitch/entrypoints/admin.tsx index 209799ca26..2b97ef08f4 100644 --- a/app/javascript/flavours/glitch/entrypoints/admin.tsx +++ b/app/javascript/flavours/glitch/entrypoints/admin.tsx @@ -1,4 +1,3 @@ -import '@/entrypoints/public-path'; import { createRoot } from 'react-dom/client'; import Rails from '@rails/ujs'; @@ -273,7 +272,7 @@ async function mountReactComponent(element: Element) { ); const { default: Component } = (await import( - `@/flavours/glitch/components/admin/${componentName}` + `@/flavours/glitch/components/admin/${componentName}.jsx` )) as { default: React.ComponentType }; const root = createRoot(element); diff --git a/app/javascript/flavours/glitch/entrypoints/application.ts b/app/javascript/flavours/glitch/entrypoints/application.ts index 3659d8212b..2b1c3e2b46 100644 --- a/app/javascript/flavours/glitch/entrypoints/application.ts +++ b/app/javascript/flavours/glitch/entrypoints/application.ts @@ -1,12 +1,7 @@ -import '@/entrypoints/public-path'; - -import { start } from 'flavours/glitch/common'; import { loadLocale } from 'flavours/glitch/locales'; import main from 'flavours/glitch/main'; import { loadPolyfills } from 'flavours/glitch/polyfills'; -start(); - loadPolyfills() .then(loadLocale) .then(main) diff --git a/app/javascript/flavours/glitch/entrypoints/common.js b/app/javascript/flavours/glitch/entrypoints/common.js deleted file mode 100644 index 774139c35c..0000000000 --- a/app/javascript/flavours/glitch/entrypoints/common.js +++ /dev/null @@ -1,7 +0,0 @@ -/* This file is a hack to have something more reliable than the upstream `common` tag - that is implicitly generated as the common chunk through webpack's `splitChunks` config */ - -import '@/entrypoints/public-path'; - -// This is a hack to ensures that webpack compiles our images. -require.context('../images', true); diff --git a/app/javascript/flavours/glitch/entrypoints/common.ts b/app/javascript/flavours/glitch/entrypoints/common.ts new file mode 100644 index 0000000000..b1ae25887a --- /dev/null +++ b/app/javascript/flavours/glitch/entrypoints/common.ts @@ -0,0 +1,3 @@ +import { start } from 'flavours/glitch/common'; + +start(); diff --git a/app/javascript/flavours/glitch/entrypoints/embed.tsx b/app/javascript/flavours/glitch/entrypoints/embed.tsx index 2249aa9e69..243cefea7f 100644 --- a/app/javascript/flavours/glitch/entrypoints/embed.tsx +++ b/app/javascript/flavours/glitch/entrypoints/embed.tsx @@ -1,15 +1,10 @@ import { createRoot } from 'react-dom/client'; -import '@/entrypoints/public-path'; - -import { start } from 'flavours/glitch/common'; import { Status } from 'flavours/glitch/features/standalone/status'; import { afterInitialRender } from 'flavours/glitch/hooks/useRenderSignal'; import { loadPolyfills } from 'flavours/glitch/polyfills'; import ready from 'flavours/glitch/ready'; -start(); - function loaded() { const mountNode = document.getElementById('mastodon-status'); diff --git a/app/javascript/flavours/glitch/entrypoints/error.ts b/app/javascript/flavours/glitch/entrypoints/error.ts index 9e067d4caa..2077064f2c 100644 --- a/app/javascript/flavours/glitch/entrypoints/error.ts +++ b/app/javascript/flavours/glitch/entrypoints/error.ts @@ -1,4 +1,3 @@ -import '@/entrypoints/public-path'; import ready from 'flavours/glitch/ready'; ready(() => { diff --git a/app/javascript/flavours/glitch/entrypoints/inert.ts b/app/javascript/flavours/glitch/entrypoints/inert.ts deleted file mode 100644 index a5d7e548be..0000000000 --- a/app/javascript/flavours/glitch/entrypoints/inert.ts +++ /dev/null @@ -1,4 +0,0 @@ -/* Placeholder file to have `inert.scss` compiled by Webpack - This is used by the `wicg-inert` polyfill */ - -import '@/styles/inert.scss'; diff --git a/app/javascript/flavours/glitch/entrypoints/mailer.ts b/app/javascript/flavours/glitch/entrypoints/mailer.ts deleted file mode 100644 index 28cbb906f5..0000000000 --- a/app/javascript/flavours/glitch/entrypoints/mailer.ts +++ /dev/null @@ -1,3 +0,0 @@ -import '@/styles/mailer.scss'; - -require.context('@/icons'); diff --git a/app/javascript/flavours/glitch/entrypoints/public.tsx b/app/javascript/flavours/glitch/entrypoints/public.tsx index 279a16ef57..90c4ef8c87 100644 --- a/app/javascript/flavours/glitch/entrypoints/public.tsx +++ b/app/javascript/flavours/glitch/entrypoints/public.tsx @@ -1,7 +1,5 @@ import { createRoot } from 'react-dom/client'; -import '@/entrypoints/public-path'; - import { IntlMessageFormat } from 'intl-messageformat'; import type { MessageDescriptor, PrimitiveType } from 'react-intl'; import { defineMessages } from 'react-intl'; @@ -10,7 +8,6 @@ import Rails from '@rails/ujs'; import axios from 'axios'; import { throttle } from 'lodash'; -import { start } from 'flavours/glitch/common'; import { timeAgoString } from 'flavours/glitch/components/relative_timestamp'; import emojify from 'flavours/glitch/features/emoji/emoji'; import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions'; @@ -20,8 +17,6 @@ import ready from 'flavours/glitch/ready'; import 'cocoon-js-vanilla'; -start(); - const messages = defineMessages({ usernameTaken: { id: 'username.taken', @@ -153,9 +148,7 @@ function loaded() { const reactComponents = document.querySelectorAll('[data-component]'); if (reactComponents.length > 0) { - import( - /* webpackChunkName: "containers/media_container" */ 'flavours/glitch/containers/media_container' - ) + import('flavours/glitch/containers/media_container') .then(({ default: MediaContainer }) => { reactComponents.forEach((component) => { Array.from(component.children).forEach((child) => { diff --git a/app/javascript/flavours/glitch/entrypoints/remote_interaction_helper.ts b/app/javascript/flavours/glitch/entrypoints/remote_interaction_helper.ts index 3bfc1fc139..f50203747d 100644 --- a/app/javascript/flavours/glitch/entrypoints/remote_interaction_helper.ts +++ b/app/javascript/flavours/glitch/entrypoints/remote_interaction_helper.ts @@ -8,8 +8,6 @@ and performs no other task. */ -import '@/entrypoints/public-path'; - import axios from 'axios'; interface JRDLink { diff --git a/app/javascript/flavours/glitch/entrypoints/share.tsx b/app/javascript/flavours/glitch/entrypoints/share.tsx index 0eda442506..1ed58604e1 100644 --- a/app/javascript/flavours/glitch/entrypoints/share.tsx +++ b/app/javascript/flavours/glitch/entrypoints/share.tsx @@ -1,13 +1,9 @@ -import '@/entrypoints/public-path'; import { createRoot } from 'react-dom/client'; -import { start } from 'flavours/glitch/common'; import ComposeContainer from 'flavours/glitch/containers/compose_container'; import { loadPolyfills } from 'flavours/glitch/polyfills'; import ready from 'flavours/glitch/ready'; -start(); - function loaded() { const mountNode = document.getElementById('mastodon-compose'); diff --git a/app/javascript/flavours/glitch/entrypoints/sign_up.ts b/app/javascript/flavours/glitch/entrypoints/sign_up.ts index 72f3b638c3..e7581eee8a 100644 --- a/app/javascript/flavours/glitch/entrypoints/sign_up.ts +++ b/app/javascript/flavours/glitch/entrypoints/sign_up.ts @@ -1,4 +1,3 @@ -import '@/entrypoints/public-path'; import axios from 'axios'; import ready from 'flavours/glitch/ready'; diff --git a/app/javascript/flavours/glitch/features/about/index.jsx b/app/javascript/flavours/glitch/features/about/index.jsx index 970972ee2e..2b0c356acb 100644 --- a/app/javascript/flavours/glitch/features/about/index.jsx +++ b/app/javascript/flavours/glitch/features/about/index.jsx @@ -44,6 +44,7 @@ const severityMessages = { const mapStateToProps = state => ({ server: state.getIn(['server', 'server']), + locale: state.getIn(['meta', 'locale']), extendedDescription: state.getIn(['server', 'extendedDescription']), domainBlocks: state.getIn(['server', 'domainBlocks']), }); @@ -91,6 +92,7 @@ class About extends PureComponent { static propTypes = { server: ImmutablePropTypes.map, + locale: ImmutablePropTypes.string, extendedDescription: ImmutablePropTypes.map, domainBlocks: ImmutablePropTypes.contains({ isLoading: PropTypes.bool, @@ -114,7 +116,7 @@ class About extends PureComponent { }; render () { - const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props; + const { multiColumn, intl, server, extendedDescription, domainBlocks, locale } = this.props; const isLoading = server.get('isLoading'); return ( @@ -168,12 +170,15 @@ class About extends PureComponent {

) : (
    - {server.get('rules').map(rule => ( -
  1. -
    {rule.get('text')}
    - {rule.get('hint').length > 0 && (
    {rule.get('hint')}
    )} -
  2. - ))} + {server.get('rules').map(rule => { + const text = rule.getIn(['translations', locale, 'text']) || rule.getIn(['translations', locale.split('-')[0], 'text']) || rule.get('text'); + const hint = rule.getIn(['translations', locale, 'hint']) || rule.getIn(['translations', locale.split('-')[0], 'hint']) || rule.get('hint'); + return ( +
  3. +
    {text}
    + {hint.length > 0 && (
    {hint}
    )} +
  4. + )})}
))} diff --git a/app/javascript/flavours/glitch/features/account_featured/index.tsx b/app/javascript/flavours/glitch/features/account_featured/index.tsx index 1efa7bdd6c..ea1bac2e9d 100644 --- a/app/javascript/flavours/glitch/features/account_featured/index.tsx +++ b/app/javascript/flavours/glitch/features/account_featured/index.tsx @@ -14,7 +14,7 @@ import { Account } from 'flavours/glitch/components/account'; import { ColumnBackButton } from 'flavours/glitch/components/column_back_button'; import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import { RemoteHint } from 'flavours/glitch/components/remote_hint'; -import StatusContainer from 'flavours/glitch/containers/status_container'; +import { StatusQuoteManager } from 'flavours/glitch/components/status_quoted'; import { AccountHeader } from 'flavours/glitch/features/account_timeline/components/account_header'; import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; import Column from 'flavours/glitch/features/ui/components/column'; @@ -142,9 +142,8 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ /> {featuredStatusIds.map((statusId) => ( - diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/familiar_followers.tsx b/app/javascript/flavours/glitch/features/account_timeline/components/familiar_followers.tsx index f77018db9d..0b9ceace8c 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/familiar_followers.tsx +++ b/app/javascript/flavours/glitch/features/account_timeline/components/familiar_followers.tsx @@ -1,15 +1,12 @@ -import { useEffect } from 'react'; - import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; -import { fetchAccountsFamiliarFollowers } from '@/flavours/glitch/actions/accounts_familiar_followers'; import { Avatar } from '@/flavours/glitch/components/avatar'; import { AvatarGroup } from '@/flavours/glitch/components/avatar_group'; import type { Account } from '@/flavours/glitch/models/account'; -import { getAccountFamiliarFollowers } from '@/flavours/glitch/selectors/accounts'; -import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store'; + +import { useFetchFamiliarFollowers } from '../hooks/familiar_followers'; const AccountLink: React.FC<{ account?: Account }> = ({ account }) => { if (!account) { @@ -64,20 +61,11 @@ const FamiliarFollowersReadout: React.FC<{ familiarFollowers: Account[] }> = ({ export const FamiliarFollowers: React.FC<{ accountId: string }> = ({ accountId, }) => { - const dispatch = useAppDispatch(); - const familiarFollowers = useAppSelector((state) => - getAccountFamiliarFollowers(state, accountId), - ); + const { familiarFollowers, isLoading } = useFetchFamiliarFollowers({ + accountId, + }); - const hasNoData = familiarFollowers === null; - - useEffect(() => { - if (hasNoData) { - void dispatch(fetchAccountsFamiliarFollowers({ id: accountId })); - } - }, [dispatch, accountId, hasNoData]); - - if (hasNoData || familiarFollowers.length === 0) { + if (isLoading || familiarFollowers.length === 0) { return null; } diff --git a/app/javascript/flavours/glitch/features/account_timeline/hooks/familiar_followers.ts b/app/javascript/flavours/glitch/features/account_timeline/hooks/familiar_followers.ts new file mode 100644 index 0000000000..41bb3c3f60 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_timeline/hooks/familiar_followers.ts @@ -0,0 +1,30 @@ +import { useEffect } from 'react'; + +import { fetchAccountsFamiliarFollowers } from '@/flavours/glitch/actions/accounts_familiar_followers'; +import { getAccountFamiliarFollowers } from '@/flavours/glitch/selectors/accounts'; +import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store'; +import { me } from 'flavours/glitch/initial_state'; + +export const useFetchFamiliarFollowers = ({ + accountId, +}: { + accountId?: string; +}) => { + const dispatch = useAppDispatch(); + const familiarFollowers = useAppSelector((state) => + accountId ? getAccountFamiliarFollowers(state, accountId) : null, + ); + + const hasNoData = familiarFollowers === null; + + useEffect(() => { + if (hasNoData && accountId && accountId !== me) { + void dispatch(fetchAccountsFamiliarFollowers({ id: accountId })); + } + }, [dispatch, accountId, hasNoData]); + + return { + familiarFollowers: hasNoData ? [] : familiarFollowers, + isLoading: hasNoData, + }; +}; diff --git a/app/javascript/flavours/glitch/features/alt_text_modal/index.tsx b/app/javascript/flavours/glitch/features/alt_text_modal/index.tsx index c2aba44dc2..cd9ef957ef 100644 --- a/app/javascript/flavours/glitch/features/alt_text_modal/index.tsx +++ b/app/javascript/flavours/glitch/features/alt_text_modal/index.tsx @@ -15,10 +15,6 @@ import type { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import { useSpring, animated } from '@react-spring/web'; import Textarea from 'react-textarea-autosize'; import { length } from 'stringz'; -// eslint-disable-next-line import/extensions -import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js'; -// eslint-disable-next-line import/no-extraneous-dependencies -import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js'; import { showAlertForError } from 'flavours/glitch/actions/alerts'; import { uploadThumbnail } from 'flavours/glitch/actions/compose'; @@ -350,9 +346,15 @@ export const AltTextModal = forwardRef>( fetchTesseract() .then(async ({ createWorker }) => { + const [tesseractWorkerPath, tesseractCorePath] = await Promise.all([ + // eslint-disable-next-line import/extensions + import('tesseract.js/dist/worker.min.js?url'), + // eslint-disable-next-line import/no-extraneous-dependencies + import('tesseract.js-core/tesseract-core.wasm.js?url'), + ]); const worker = await createWorker('eng', 1, { - workerPath: tesseractWorkerPath as string, - corePath: tesseractCorePath as string, + workerPath: tesseractWorkerPath.default, + corePath: tesseractCorePath.default, langPath: `${assetHost}/ocr/lang-data`, cacheMethod: 'write', }); @@ -501,5 +503,4 @@ export const AltTextModal = forwardRef>( ); }, ); - AltTextModal.displayName = 'AltTextModal'; diff --git a/app/javascript/flavours/glitch/features/emoji/emoji_compressed.d.ts b/app/javascript/flavours/glitch/features/emoji/emoji_compressed.d.mts similarity index 100% rename from app/javascript/flavours/glitch/features/emoji/emoji_compressed.d.ts rename to app/javascript/flavours/glitch/features/emoji/emoji_compressed.d.mts diff --git a/app/javascript/mastodon/features/emoji/emoji_compressed.js b/app/javascript/flavours/glitch/features/emoji/emoji_compressed.mjs similarity index 58% rename from app/javascript/mastodon/features/emoji/emoji_compressed.js rename to app/javascript/flavours/glitch/features/emoji/emoji_compressed.mjs index db406393eb..bab2f6bfca 100644 --- a/app/javascript/mastodon/features/emoji/emoji_compressed.js +++ b/app/javascript/flavours/glitch/features/emoji/emoji_compressed.mjs @@ -9,28 +9,27 @@ // to ensure that the prevaled file is regenerated by Babel // version: 4 -const { NimbleEmojiIndex } = require('emoji-mart'); -const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data'); +import { NimbleEmojiIndex } from 'emoji-mart'; +import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data'; - -let data = require('./emoji_data.json'); -const emojiMap = require('./emoji_map.json'); -const { unicodeToFilename } = require('./unicode_to_filename'); -const { unicodeToUnifiedName } = require('./unicode_to_unified_name'); +import data from './emoji_data.json'; +import emojiMap from './emoji_map.json'; +import { unicodeToFilename } from './unicode_to_filename'; +import { unicodeToUnifiedName } from './unicode_to_unified_name'; emojiMartUncompress(data); const emojiMartData = data; const emojiIndex = new NimbleEmojiIndex(emojiMartData); -const excluded = ['®', '©', '™']; -const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿']; -const shortcodeMap = {}; +const excluded = ['®', '©', '™']; +const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿']; +const shortcodeMap = {}; const shortCodesToEmojiData = {}; const emojisWithoutShortCodes = []; -Object.keys(emojiIndex.emojis).forEach(key => { +Object.keys(emojiIndex.emojis).forEach((key) => { let emoji = emojiIndex.emojis[key]; // Emojis with skin tone modifiers are stored like this @@ -41,22 +40,22 @@ Object.keys(emojiIndex.emojis).forEach(key => { shortcodeMap[emoji.native] = emoji.id; }); -const stripModifiers = unicode => { - skinTones.forEach(tone => { +const stripModifiers = (unicode) => { + skinTones.forEach((tone) => { unicode = unicode.replace(tone, ''); }); return unicode; }; -Object.keys(emojiMap).forEach(key => { +Object.keys(emojiMap).forEach((key) => { if (excluded.includes(key)) { delete emojiMap[key]; return; } const normalizedKey = stripModifiers(key); - let shortcode = shortcodeMap[normalizedKey]; + let shortcode = shortcodeMap[normalizedKey]; if (!shortcode) { shortcode = shortcodeMap[normalizedKey + '\uFE0F']; @@ -82,7 +81,7 @@ Object.keys(emojiMap).forEach(key => { } }); -Object.keys(emojiIndex.emojis).forEach(key => { +Object.keys(emojiIndex.emojis).forEach((key) => { let emoji = emojiIndex.emojis[key]; // Emojis with skin tone modifiers are stored like this @@ -94,9 +93,11 @@ Object.keys(emojiIndex.emojis).forEach(key => { let { short_names, search, unified } = emojiMartData.emojis[key]; if (short_names[0] !== key) { - throw new Error('The compressor expects the first short_code to be the ' + - 'key. It may need to be rewritten if the emoji change such that this ' + - 'is no longer the case.'); + throw new Error( + 'The compressor expects the first short_code to be the ' + + 'key. It may need to be rewritten if the emoji change such that this ' + + 'is no longer the case.', + ); } short_names = short_names.slice(1); // first short name can be inferred from the key @@ -117,20 +118,22 @@ Object.keys(emojiIndex.emojis).forEach(key => { // JSON.parse/stringify is to emulate what @preval is doing and avoid any // inconsistent behavior in dev mode -module.exports = JSON.parse(JSON.stringify([ - shortCodesToEmojiData, - /* - * The property `skins` is not found in the current context. - * This could potentially lead to issues when interacting with modules or data structures - * that expect the presence of `skins` property. - * Currently, no definitions or references to `skins` property can be found in: - * - {@link node_modules/emoji-mart/dist/utils/data.js} - * - {@link node_modules/emoji-mart/data/all.json} - * - {@link app/javascript/mastodon/features/emoji/emoji_compressed.d.ts#Skins} - * Future refactorings or updates should consider adding definitions or handling for `skins` property. - */ - emojiMartData.skins, - emojiMartData.categories, - emojiMartData.aliases, - emojisWithoutShortCodes -])); +export default JSON.parse( + JSON.stringify([ + shortCodesToEmojiData, + /* + * The property `skins` is not found in the current context. + * This could potentially lead to issues when interacting with modules or data structures + * that expect the presence of `skins` property. + * Currently, no definitions or references to `skins` property can be found in: + * - {@link node_modules/emoji-mart/dist/utils/data.js} + * - {@link node_modules/emoji-mart/data/all.json} + * - {@link app/javascript/flavours/glitch/features/emoji/emoji_compressed.d.ts#Skins} + * Future refactorings or updates should consider adding definitions or handling for `skins` property. + */ + emojiMartData.skins, + emojiMartData.categories, + emojiMartData.aliases, + emojisWithoutShortCodes, + ]), +); diff --git a/app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.ts b/app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.ts index 8eeb457055..ecdda58513 100644 --- a/app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.ts +++ b/app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.ts @@ -3,9 +3,13 @@ // emojiIndex.search functionality. import type { BaseEmoji } from 'emoji-mart'; import type { Emoji } from 'emoji-mart/dist-es/utils/data'; +// eslint-disable-next-line import/no-unresolved +import emojiCompressed from 'virtual:mastodon-emoji-compressed'; +import type { + Search, + ShortCodesToEmojiData, +} from 'virtual:mastodon-emoji-compressed'; -import type { Search, ShortCodesToEmojiData } from './emoji_compressed'; -import emojiCompressed from './emoji_compressed'; import { unicodeToUnifiedName } from './unicode_to_unified_name'; type Emojis = Record< diff --git a/app/javascript/flavours/glitch/features/emoji/emoji_unicode_mapping_light.ts b/app/javascript/flavours/glitch/features/emoji/emoji_unicode_mapping_light.ts index 0a5a4c1d76..283508db58 100644 --- a/app/javascript/flavours/glitch/features/emoji/emoji_unicode_mapping_light.ts +++ b/app/javascript/flavours/glitch/features/emoji/emoji_unicode_mapping_light.ts @@ -2,11 +2,13 @@ // (i.e. the svg filename) and a shortCode intended to be shown // as a "title" attribute in an HTML element (aka tooltip). +// eslint-disable-next-line import/no-unresolved +import emojiCompressed from 'virtual:mastodon-emoji-compressed'; import type { FilenameData, ShortCodesToEmojiDataKey, -} from './emoji_compressed'; -import emojiCompressed from './emoji_compressed'; +} from 'virtual:mastodon-emoji-compressed'; + import { unicodeToFilename } from './unicode_to_filename'; type UnicodeMapping = Record< diff --git a/app/javascript/flavours/glitch/features/emoji/unicode_to_filename.js b/app/javascript/flavours/glitch/features/emoji/unicode_to_filename.js index c75c4cd7d0..cfe5539c7b 100644 --- a/app/javascript/flavours/glitch/features/emoji/unicode_to_filename.js +++ b/app/javascript/flavours/glitch/features/emoji/unicode_to_filename.js @@ -1,6 +1,6 @@ // taken from: // https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866 -exports.unicodeToFilename = (str) => { +export const unicodeToFilename = (str) => { let result = ''; let charCode = 0; let p = 0; diff --git a/app/javascript/flavours/glitch/features/emoji/unicode_to_unified_name.js b/app/javascript/flavours/glitch/features/emoji/unicode_to_unified_name.js index d29550f122..15f60aa7c3 100644 --- a/app/javascript/flavours/glitch/features/emoji/unicode_to_unified_name.js +++ b/app/javascript/flavours/glitch/features/emoji/unicode_to_unified_name.js @@ -6,7 +6,7 @@ function padLeft(str, num) { return str; } -exports.unicodeToUnifiedName = (str) => { +export const unicodeToUnifiedName = (str) => { let output = ''; for (let i = 0; i < str.length; i += 2) { diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx index 6ec613e209..a2ed584135 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx @@ -16,7 +16,7 @@ import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react'; import { Account } from 'flavours/glitch/components/account'; import { Icon } from 'flavours/glitch/components/icon'; import { Permalink } from 'flavours/glitch/components/permalink'; -import StatusContainer from 'flavours/glitch/containers/status_container'; +import { StatusQuoteManager } from 'flavours/glitch/components/status_quoted'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; import FollowRequestContainer from '../containers/follow_request_container'; @@ -159,7 +159,7 @@ class Notification extends ImmutablePureComponent { renderMention (notification) { return ( -