diff --git a/.eslintrc.js b/.eslintrc.js index 9e965791b..badcb1dcb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -55,10 +55,7 @@ module.exports = { '\\.(css|scss|json)$', ], 'import/resolver': { - node: { - paths: ['app/javascript'], - extensions: ['.js', '.jsx', '.ts', '.tsx'], - }, + typescript: {}, }, }, @@ -104,7 +101,6 @@ module.exports = { 'react/jsx-equals-spacing': 'error', 'react/jsx-no-bind': 'error', 'react/jsx-no-target-blank': 'off', - 'react/no-deprecated': 'off', 'react/no-unknown-property': 'off', 'react/self-closing-comp': 'error', @@ -168,11 +164,14 @@ module.exports = { { js: 'never', jsx: 'never', + mjs: 'never', ts: 'never', tsx: 'never', }, ], + 'import/first': 'error', 'import/newline-after-import': 'error', + 'import/no-anonymous-default-export': 'error', 'import/no-extraneous-dependencies': [ 'error', { @@ -187,6 +186,9 @@ module.exports = { 'import/no-amd': 'error', 'import/no-commonjs': 'error', 'import/no-import-module-exports': 'error', + 'import/no-relative-packages': 'error', + 'import/no-self-import': 'error', + 'import/no-useless-path-segments': 'error', 'import/no-webpack-loader-syntax': 'error', 'promise/always-return': 'off', @@ -258,6 +260,7 @@ module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', 'plugin:react/recommended', 'plugin:react-hooks/recommended', 'plugin:jsx-a11y/recommended', @@ -268,8 +271,66 @@ module.exports = { 'plugin:prettier/recommended', ], + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + rules: { - '@typescript-eslint/no-explicit-any': 'off', + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], + + 'import/order': [ + 'error', + { + alphabetize: { order: 'asc' }, + 'newlines-between': 'always', + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + ['index', 'sibling'], + 'object', + ], + pathGroups: [ + // React core packages + { + pattern: '{react,react-dom,prop-types}', + group: 'builtin', + position: 'after', + }, + // I18n + { + pattern: 'react-intl', + group: 'builtin', + position: 'after', + }, + // Common React utilities + { + pattern: '{classnames,react-helmet}', + group: 'external', + position: 'before', + }, + // Immutable / Redux / data store + { + pattern: '{immutable,react-redux,react-immutable-proptypes,react-immutable-pure-component,reselect}', + group: 'external', + position: 'before', + }, + // Internal packages + { + pattern: '{mastodon/**,flavours/glitch-soc/**}', + group: 'internal', + position: 'after', + }, + ], + pathGroupsExcludedImportTypes: [], + }, + ], + + '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], + '@typescript-eslint/consistent-type-exports': 'error', + '@typescript-eslint/consistent-type-imports': 'error', 'jsdoc/require-jsdoc': 'off', diff --git a/.github/workflows/test-migrations-one-step.yml b/.github/workflows/test-migrations-one-step.yml index d7e424a8c..212b2cfe7 100644 --- a/.github/workflows/test-migrations-one-step.yml +++ b/.github/workflows/test-migrations-one-step.yml @@ -23,9 +23,17 @@ jobs: needs: pre_job if: needs.pre_job.outputs.should_skip != 'true' + strategy: + fail-fast: false + + matrix: + postgres: + - 14-alpine + - 15-alpine + services: postgres: - image: postgres:14-alpine + image: postgres:${{ matrix.postgres}} env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml index 25bf5ba87..310153929 100644 --- a/.github/workflows/test-migrations-two-step.yml +++ b/.github/workflows/test-migrations-two-step.yml @@ -23,9 +23,17 @@ jobs: needs: pre_job if: needs.pre_job.outputs.should_skip != 'true' + strategy: + fail-fast: false + + matrix: + postgres: + - 14-alpine + - 15-alpine + services: postgres: - image: postgres:14-alpine + image: postgres:${{ matrix.postgres}} env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index df1b4718e..77caad492 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -21,12 +21,6 @@ Layout/ArgumentAlignment: - 'config/initializers/cors.rb' - 'config/initializers/session_store.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. -Layout/ExtraSpacing: - Exclude: - - 'config/initializers/omniauth.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. # SupportedHashRocketStyles: key, separator, table @@ -39,12 +33,6 @@ Layout/HashAlignment: - 'config/initializers/rack_attack.rb' - 'config/routes.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Width, AllowedPatterns. -Layout/IndentationWidth: - Exclude: - - 'config/initializers/ffmpeg.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment. Layout/LeadingCommentSpace: @@ -52,14 +40,6 @@ Layout/LeadingCommentSpace: - 'config/application.rb' - 'config/initializers/omniauth.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceBeforeBlockBraces: - Exclude: - - 'config/initializers/paperclip.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: require_no_space, require_space @@ -68,19 +48,6 @@ Layout/SpaceInLambdaLiteral: - 'config/environments/production.rb' - 'config/initializers/content_security_policy.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, no_space -Layout/SpaceInsideStringInterpolation: - Exclude: - - 'config/initializers/webauthn.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowInHeredoc. -Layout/TrailingWhitespace: - Exclude: - - 'config/initializers/paperclip.rb' - # Configuration parameters: AllowedMethods, AllowedPatterns. Lint/AmbiguousBlockAssociation: Exclude: @@ -94,11 +61,6 @@ Lint/AmbiguousBlockAssociation: - 'spec/services/unsuspend_account_service_spec.rb' - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' -# This cop supports safe autocorrection (--autocorrect). -Lint/AmbiguousOperatorPrecedence: - Exclude: - - 'config/initializers/rack_attack.rb' - # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: Exclude: @@ -277,31 +239,6 @@ Naming/VariableNumber: - 'spec/models/user_spec.rb' - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/MapCompact: - Exclude: - - 'app/lib/admin/metrics/dimension.rb' - - 'app/lib/admin/metrics/measure.rb' - - 'app/lib/feed_manager.rb' - - 'app/models/account.rb' - - 'app/models/account_statuses_cleanup_policy.rb' - - 'app/models/account_suggestions/setting_source.rb' - - 'app/models/account_suggestions/source.rb' - - 'app/models/follow_recommendation_filter.rb' - - 'app/models/notification.rb' - - 'app/models/user_role.rb' - - 'app/models/webhook.rb' - - 'app/services/process_mentions_service.rb' - - 'app/validators/existing_username_validator.rb' - - 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb' - - 'spec/presenters/status_relationships_presenter_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SafeMultiline. -Performance/StartWith: - Exclude: - - 'app/lib/extractor.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Performance/UnfreezeString: Exclude: @@ -626,7 +563,6 @@ RSpec/NoExpectationExample: RSpec/PendingWithoutReason: Exclude: - - 'spec/controllers/statuses_controller_spec.rb' - 'spec/models/account_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). @@ -638,32 +574,6 @@ RSpec/PredicateMatcher: - 'spec/models/user_spec.rb' - 'spec/services/post_status_service_spec.rb' -RSpec/RepeatedExample: - Exclude: - - 'spec/policies/status_policy_spec.rb' - -RSpec/RepeatedExampleGroupBody: - Exclude: - - 'spec/controllers/statuses_controller_spec.rb' - -RSpec/RepeatedExampleGroupDescription: - Exclude: - - 'spec/controllers/admin/reports/actions_controller_spec.rb' - - 'spec/policies/report_note_policy_spec.rb' - -RSpec/ScatteredSetup: - Exclude: - - 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb' - - 'spec/controllers/activitypub/outboxes_controller_spec.rb' - - 'spec/controllers/admin/disputes/appeals_controller_spec.rb' - - 'spec/controllers/auth/registrations_controller_spec.rb' - - 'spec/services/activitypub/process_account_service_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -RSpec/SharedContext: - Exclude: - - 'spec/services/unsuspend_account_service_spec.rb' - RSpec/StubbedMock: Exclude: - 'spec/controllers/api/base_controller_spec.rb' diff --git a/Dockerfile b/Dockerfile index 91c26d2ac..cb5096581 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,7 +55,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] ENV DEBIAN_FRONTEND="noninteractive" \ PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" -# Ignoreing these here since we don't want to pin any versions and the Debian image removes apt-get content after use +# Ignoring these here since we don't want to pin any versions and the Debian image removes apt-get content after use # hadolint ignore=DL3008,DL3009 RUN apt-get update && \ echo "Etc/UTC" > /etc/localtime && \ diff --git a/Gemfile b/Gemfile index e55b21c9e..88ac7c5ae 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ gem 'makara', '~> 0.5' gem 'pghero' gem 'dotenv-rails', '~> 2.8' -gem 'aws-sdk-s3', '~> 1.120', require: false +gem 'aws-sdk-s3', '~> 1.122', require: false gem 'fog-core', '<= 2.4.0' gem 'fog-openstack', '~> 0.3', require: false gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b' @@ -75,7 +75,7 @@ gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-s gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' -gem 'rqrcode', '~> 2.1' +gem 'rqrcode', '~> 2.2' gem 'ruby-progressbar', '~> 1.13' gem 'sanitize', '~> 6.0' gem 'scenic', '~> 1.7' @@ -99,54 +99,87 @@ gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' -group :development, :test do - gem 'fabrication', '~> 2.30' - gem 'fuubar', '~> 2.5' - gem 'i18n-tasks', '~> 1.0', require: false - gem 'rspec-rails', '~> 6.0' - gem 'rspec_chunked', '~> 0.6' - - gem 'rubocop-capybara', require: false - gem 'rubocop-performance', require: false - gem 'rubocop-rails', require: false - gem 'rubocop-rspec', require: false - gem 'rubocop', require: false -end - -group :production, :test do - gem 'private_address_check', '~> 0.5' -end +gem 'private_address_check', '~> 0.5' group :test do - gem 'capybara', '~> 3.39' - gem 'climate_control' - gem 'faker', '~> 3.2' - gem 'json-schema', '~> 4.0' - gem 'rack-test', '~> 2.1' - gem 'rails-controller-testing', '~> 1.0' - gem 'rspec_junit_formatter', '~> 0.6' + # RSpec runner for rails + gem 'rspec-rails', '~> 6.0' + + # Used to split testing into chunks in CI + gem 'rspec_chunked', '~> 0.6' + + # RSpec progress bar formatter + gem 'fuubar', '~> 2.5' + + # Extra RSpec extenion methods and helpers for sidekiq gem 'rspec-sidekiq', '~> 3.1' + + # Browser integration testing + gem 'capybara', '~> 3.39' + + # Used to mock environment variables + gem 'climate_control', '~> 0.2' + + # Generating fake data for specs + gem 'faker', '~> 3.2' + + # Generate test objects for specs + gem 'fabrication', '~> 2.30' + + # Add back helpers functions removed in Rails 5.1 + gem 'rails-controller-testing', '~> 1.0' + + # Validate schemas in specs + gem 'json-schema', '~> 4.0' + + # Test harness fo rack components + gem 'rack-test', '~> 2.1' + + # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false gem 'simplecov', '~> 0.22', require: false + + # Stub web requests for specs gem 'webmock', '~> 3.18' end group :development do + # Code linting CLI and plugins + gem 'rubocop', require: false + gem 'rubocop-capybara', require: false + gem 'rubocop-performance', require: false + gem 'rubocop-rails', require: false + gem 'rubocop-rspec', require: false + + # Annotates modules with schema gem 'annotate', '~> 3.2' + + # Enhanced error message pages for development gem 'better_errors', '~> 2.9' gem 'binding_of_caller', '~> 1.0' + + # Preview mail in the browser gem 'letter_opener', '~> 1.8' gem 'letter_opener_web', '~> 2.0' - gem 'memory_profiler' + + # Security analysis CLI tools gem 'brakeman', '~> 5.4', require: false gem 'bundler-audit', '~> 0.9', require: false + + # Linter CLI for HAML files gem 'haml_lint', require: false + # Deployment automation gem 'capistrano', '~> 3.17' gem 'capistrano-rails', '~> 1.6' gem 'capistrano-rbenv', '~> 2.2' gem 'capistrano-yarn', '~> 2.0' - gem 'stackprof' + # Validate missing i18n keys + gem 'i18n-tasks', '~> 1.0', require: false + + # Profiling tools + gem 'memory_profiler', require: false + gem 'stackprof', require: false end group :production do @@ -157,8 +190,9 @@ gem 'concurrent-ruby', require: false gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' -gem 'hcaptcha', '~> 7.1' gem 'cocoon', '~> 1.2' gem 'net-http', '~> 0.3.2' gem 'rubyzip', '~> 2.3' + +gem 'hcaptcha', '~> 7.1' diff --git a/Gemfile.lock b/Gemfile.lock index f22d5b372..21647e1b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -109,16 +109,16 @@ GEM attr_required (1.0.1) awrence (1.2.1) aws-eventstream (1.2.0) - aws-partitions (1.752.0) - aws-sdk-core (3.171.0) + aws-partitions (1.761.0) + aws-sdk-core (3.172.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.63.0) + aws-sdk-kms (1.64.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.121.0) + aws-sdk-s3 (1.122.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) @@ -166,7 +166,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.39.0) + capybara (3.39.1) addressable matrix mini_mime (>= 0.1.3) @@ -189,7 +189,7 @@ GEM coderay (1.1.3) color_diff (0.1) concurrent-ruby (1.2.2) - connection_pool (2.4.0) + connection_pool (2.4.1) cose (1.3.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) @@ -331,7 +331,7 @@ GEM httplog (1.6.2) rack (>= 2.0) rainbow (>= 2.0.0) - i18n (1.12.0) + i18n (1.13.0) concurrent-ruby (~> 1.0) i18n-tasks (1.0.12) activesupport (>= 4.0.2) @@ -398,9 +398,9 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.20.0) + loofah (2.21.3) crass (~> 1.0.2) - nokogiri (>= 1.5.9) + nokogiri (>= 1.12.0) mail (2.8.1) mini_mime (>= 0.1.1) net-imap @@ -418,7 +418,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2023.0218.1) mini_mime (1.1.2) - mini_portile2 (2.8.1) + mini_portile2 (2.8.2) minitest (5.18.0) msgpack (1.7.0) multi_json (1.15.0) @@ -576,7 +576,7 @@ GEM rexml (3.2.5) rotp (6.2.2) rpam2 (4.0.2) - rqrcode (2.1.2) + rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) @@ -588,22 +588,20 @@ GEM rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.0.1) + rspec-rails (6.0.2) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) - rspec-core (~> 3.11) - rspec-expectations (~> 3.11) - rspec-mocks (~> 3.11) - rspec-support (~> 3.11) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) rspec-sidekiq (3.1.0) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.12.0) rspec_chunked (0.6) - rspec_junit_formatter (0.6.0) - rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.50.2) + rubocop (1.51.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.2.0.0) @@ -613,11 +611,11 @@ GEM rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.28.0) + rubocop-ast (1.28.1) parser (>= 3.2.1.0) rubocop-capybara (2.18.0) rubocop (~> 1.41) - rubocop-performance (1.17.1) + rubocop-performance (1.18.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) rubocop-rails (2.19.1) @@ -698,7 +696,7 @@ GEM unicode-display_width (>= 1.1.1, < 3) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) - thor (1.2.1) + thor (1.2.2) tilt (2.1.0) timeout (0.3.2) tpm-key_attestation (0.12.0) @@ -763,7 +761,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.7) + zeitwerk (2.6.8) PLATFORMS ruby @@ -772,7 +770,7 @@ DEPENDENCIES active_model_serializers (~> 0.10) addressable (~> 2.8) annotate (~> 3.2) - aws-sdk-s3 (~> 1.120) + aws-sdk-s3 (~> 1.122) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) @@ -787,7 +785,7 @@ DEPENDENCIES capybara (~> 3.39) charlock_holmes (~> 0.7.7) chewy (~> 7.3) - climate_control + climate_control (~> 0.2) cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby @@ -862,11 +860,10 @@ DEPENDENCIES redcarpet (~> 3.6) redis (~> 4.5) redis-namespace (~> 1.10) - rqrcode (~> 2.1) + rqrcode (~> 2.2) rspec-rails (~> 6.0) rspec-sidekiq (~> 3.1) rspec_chunked (~> 0.6) - rspec_junit_formatter (~> 0.6) rubocop rubocop-capybara rubocop-performance diff --git a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb index 9ef1b3be7..7b192b979 100644 --- a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb +++ b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb @@ -58,7 +58,7 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController end def set_canonical_email_blocks_from_test - @canonical_email_blocks = CanonicalEmailBlock.matching_email(params[:email]) + @canonical_email_blocks = CanonicalEmailBlock.matching_email(params.require(:email)) end def set_canonical_email_block diff --git a/app/controllers/api/v1/admin/domain_allows_controller.rb b/app/controllers/api/v1/admin/domain_allows_controller.rb index 61e1d481c..dd54d6710 100644 --- a/app/controllers/api/v1/admin/domain_allows_controller.rb +++ b/app/controllers/api/v1/admin/domain_allows_controller.rb @@ -29,7 +29,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController def create authorize :domain_allow, :create? - @domain_allow = DomainAllow.find_by(resource_params) + @domain_allow = DomainAllow.find_by(domain: resource_params[:domain]) if @domain_allow.nil? @domain_allow = DomainAllow.create!(resource_params) diff --git a/app/controllers/api/v1/emails/confirmations_controller.rb b/app/controllers/api/v1/emails/confirmations_controller.rb index 32fb8e39f..29ff897b9 100644 --- a/app/controllers/api/v1/emails/confirmations_controller.rb +++ b/app/controllers/api/v1/emails/confirmations_controller.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true class Api::V1::Emails::ConfirmationsController < Api::BaseController - before_action -> { doorkeeper_authorize! :write, :'write:accounts' } - before_action :require_user_owned_by_application! - before_action :require_user_not_confirmed! + before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: :check + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check + before_action :require_user_owned_by_application!, except: :check + before_action :require_user_not_confirmed!, except: :check def create current_user.update!(email: params[:email]) if params.key?(:email) @@ -12,6 +13,10 @@ class Api::V1::Emails::ConfirmationsController < Api::BaseController render_empty end + def check + render json: current_user.confirmed? + end + private def require_user_owned_by_application! diff --git a/app/controllers/api/v1/featured_tags_controller.rb b/app/controllers/api/v1/featured_tags_controller.rb index edb42a94e..5c81877bd 100644 --- a/app/controllers/api/v1/featured_tags_controller.rb +++ b/app/controllers/api/v1/featured_tags_controller.rb @@ -13,7 +13,7 @@ class Api::V1::FeaturedTagsController < Api::BaseController end def create - featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name]) + featured_tag = CreateFeaturedTagService.new.call(current_account, params.require(:name)) render json: featured_tag, serializer: REST::FeaturedTagSerializer end @@ -33,6 +33,6 @@ class Api::V1::FeaturedTagsController < Api::BaseController end def featured_tag_params - params.permit(:name) + params.require(:name) end end diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 1be15a5a4..e3769437b 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -2,6 +2,8 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController include Authorization + include Redisable + include Lockable before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action :require_user! @@ -10,7 +12,9 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController override_rate_limit_headers :create, family: :statuses def create - @status = ReblogService.new.call(current_account, @reblog, reblog_params) + with_redis_lock("reblog:#{current_account.id}:#{@reblog.id}") do + @status = ReblogService.new.call(current_account, @reblog, reblog_params) + end render json: @status, serializer: REST::StatusSerializer end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index a49d23a9f..a9d92b6e2 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -132,7 +132,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_sessions - @sessions = current_user.session_activations + @sessions = current_user.session_activations.order(updated_at: :desc) end def set_strikes diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index 3ee35d141..8edca4d01 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -45,6 +45,6 @@ class Auth::SetupController < ApplicationController end def set_pack - use_pack 'auth' + use_pack 'sign_up' end end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index efae7e35f..0a1df5506 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -10,6 +10,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :set_body_classes before_action :set_cache_headers + before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json } + skip_before_action :require_functional! include Localized @@ -40,4 +42,14 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio def set_cache_headers response.cache_control.replace(private: true, no_store: true) end + + def set_last_used_at_by_app + @last_used_at_by_app = Doorkeeper::AccessToken + .select('DISTINCT ON (application_id) application_id, last_used_at') + .where(resource_owner_id: current_resource_owner.id) + .where.not(last_used_at: nil) + .order(application_id: :desc, last_used_at: :desc) + .pluck(:application_id, :last_used_at) + .to_h + end end diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml index b9144e43a..30676dcf5 100644 --- a/app/javascript/core/theme.yml +++ b/app/javascript/core/theme.yml @@ -16,4 +16,5 @@ pack: modal: public.js public: public.js settings: settings.js + sign_up: share: diff --git a/app/javascript/flavours/glitch/actions/app.ts b/app/javascript/flavours/glitch/actions/app.ts index 46b2cdf93..6fbfc07f6 100644 --- a/app/javascript/flavours/glitch/actions/app.ts +++ b/app/javascript/flavours/glitch/actions/app.ts @@ -1,8 +1,9 @@ import { createAction } from '@reduxjs/toolkit'; + import type { LayoutType } from '../is_mobile'; -type ChangeLayoutPayload = { +interface ChangeLayoutPayload { layout: LayoutType; -}; +} export const changeLayout = createAction('APP_LAYOUT_CHANGE'); diff --git a/app/javascript/flavours/glitch/actions/pin_statuses.js b/app/javascript/flavours/glitch/actions/pin_statuses.js index d8c0a1373..a0d5933b1 100644 --- a/app/javascript/flavours/glitch/actions/pin_statuses.js +++ b/app/javascript/flavours/glitch/actions/pin_statuses.js @@ -1,12 +1,12 @@ import api from '../api'; import { importFetchedStatuses } from './importer'; +import { me } from 'flavours/glitch/initial_state'; + export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; -import { me } from 'flavours/glitch/initial_state'; - export function fetchPinnedStatuses() { return (dispatch, getState) => { dispatch(fetchPinnedStatusesRequest()); diff --git a/app/javascript/flavours/glitch/components/account.jsx b/app/javascript/flavours/glitch/components/account.jsx index e9f04fcda..35f83beab 100644 --- a/app/javascript/flavours/glitch/components/account.jsx +++ b/app/javascript/flavours/glitch/components/account.jsx @@ -2,14 +2,14 @@ import React, { Fragment } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { Avatar } from './avatar'; -import DisplayName from './display_name'; +import { DisplayName } from './display_name'; import Permalink from './permalink'; import { IconButton } from './icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { me } from 'flavours/glitch/initial_state'; import { RelativeTimestamp } from './relative_timestamp'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, diff --git a/app/javascript/flavours/glitch/components/admin/Counter.jsx b/app/javascript/flavours/glitch/components/admin/Counter.jsx index 5b6a19f8d..9c0b848d0 100644 --- a/app/javascript/flavours/glitch/components/admin/Counter.jsx +++ b/app/javascript/flavours/glitch/components/admin/Counter.jsx @@ -4,7 +4,7 @@ import api from 'flavours/glitch/api'; import { FormattedNumber } from 'react-intl'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; import classNames from 'classnames'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; const percIncrease = (a, b) => { let percent; diff --git a/app/javascript/flavours/glitch/components/admin/Dimension.jsx b/app/javascript/flavours/glitch/components/admin/Dimension.jsx index 3dac8c6c2..8b8466ee7 100644 --- a/app/javascript/flavours/glitch/components/admin/Dimension.jsx +++ b/app/javascript/flavours/glitch/components/admin/Dimension.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import api from 'flavours/glitch/api'; import { FormattedNumber } from 'react-intl'; import { roundTo10 } from 'flavours/glitch/utils/numbers'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; export default class Dimension extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/animated_number.tsx b/app/javascript/flavours/glitch/components/animated_number.tsx index f6c77d35f..b6b073161 100644 --- a/app/javascript/flavours/glitch/components/animated_number.tsx +++ b/app/javascript/flavours/glitch/components/animated_number.tsx @@ -1,8 +1,11 @@ import React, { useCallback, useState } from 'react'; -import ShortNumber from './short_number'; + import { TransitionMotion, spring } from 'react-motion'; + import { reduceMotion } from '../initial_state'; +import ShortNumber from './short_number'; + const obfuscatedCount = (count: number) => { if (count < 0) { return 0; @@ -13,10 +16,10 @@ const obfuscatedCount = (count: number) => { } }; -type Props = { +interface Props { value: number; obfuscate?: boolean; -}; +} export const AnimatedNumber: React.FC = ({ value, obfuscate }) => { const [previousValue, setPreviousValue] = useState(value); const [direction, setDirection] = useState<1 | -1>(1); @@ -64,7 +67,11 @@ export const AnimatedNumber: React.FC = ({ value, obfuscate }) => { transform: `translateY(${style.y * 100}%)`, }} > - {obfuscate ? obfuscatedCount(data) : } + {obfuscate ? ( + obfuscatedCount(data as number) + ) : ( + + )} ))} diff --git a/app/javascript/flavours/glitch/components/autosuggest_input.jsx b/app/javascript/flavours/glitch/components/autosuggest_input.jsx index ea9fd0828..9e74590e6 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_input.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_input.jsx @@ -154,7 +154,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { this.input.focus(); }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { this.setState({ suggestionsHidden: false }); } diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx index a016e44b7..fc763146e 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx @@ -153,7 +153,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { this.textarea.focus(); }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { this.setState({ suggestionsHidden: false }); } diff --git a/app/javascript/flavours/glitch/components/avatar.tsx b/app/javascript/flavours/glitch/components/avatar.tsx index 1bd7739f5..11253a8e9 100644 --- a/app/javascript/flavours/glitch/components/avatar.tsx +++ b/app/javascript/flavours/glitch/components/avatar.tsx @@ -1,16 +1,18 @@ import * as React from 'react'; + import classNames from 'classnames'; -import { autoPlayGif } from 'flavours/glitch/initial_state'; + import { useHovering } from 'flavours/glitch/hooks/useHovering'; +import { autoPlayGif } from 'flavours/glitch/initial_state'; import type { Account } from 'flavours/glitch/types/resources'; -type Props = { +interface Props { account: Account | undefined; className?: string; size: number; style?: React.CSSProperties; inline?: boolean; -}; +} export const Avatar: React.FC = ({ account, diff --git a/app/javascript/flavours/glitch/components/blurhash.tsx b/app/javascript/flavours/glitch/components/blurhash.tsx index 700513676..1550d0b7a 100644 --- a/app/javascript/flavours/glitch/components/blurhash.tsx +++ b/app/javascript/flavours/glitch/components/blurhash.tsx @@ -1,14 +1,14 @@ -import { decode } from 'blurhash'; import React, { useRef, useEffect } from 'react'; -type Props = { +import { decode } from 'blurhash'; + +interface Props extends React.HTMLAttributes { hash: string; width?: number; height?: number; dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched children?: never; - [key: string]: any; -}; +} const Blurhash: React.FC = ({ hash, width = 32, @@ -21,6 +21,7 @@ const Blurhash: React.FC = ({ useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const canvas = canvasRef.current!; + // eslint-disable-next-line no-self-assign canvas.width = canvas.width; // resets canvas diff --git a/app/javascript/flavours/glitch/components/column.jsx b/app/javascript/flavours/glitch/components/column.jsx index 47293ef18..fce6a6c40 100644 --- a/app/javascript/flavours/glitch/components/column.jsx +++ b/app/javascript/flavours/glitch/components/column.jsx @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import { supportsPassiveEvents } from 'detect-passive-events'; import { scrollTop } from '../scroll'; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + export default class Column extends React.PureComponent { static propTypes = { @@ -37,17 +39,17 @@ export default class Column extends React.PureComponent { componentDidMount () { if (this.props.bindToDocument) { - document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); + document.addEventListener('wheel', this.handleWheel, listenerOptions); } else { - this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); + this.node.addEventListener('wheel', this.handleWheel, listenerOptions); } } componentWillUnmount () { if (this.props.bindToDocument) { - document.removeEventListener('wheel', this.handleWheel); + document.removeEventListener('wheel', this.handleWheel, listenerOptions); } else { - this.node.removeEventListener('wheel', this.handleWheel); + this.node.removeEventListener('wheel', this.handleWheel, listenerOptions); } } diff --git a/app/javascript/flavours/glitch/components/display_name.jsx b/app/javascript/flavours/glitch/components/display_name.jsx deleted file mode 100644 index fceca5d96..000000000 --- a/app/javascript/flavours/glitch/components/display_name.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { autoPlayGif } from 'flavours/glitch/initial_state'; -import Skeleton from 'flavours/glitch/components/skeleton'; - -export default class DisplayName extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map, - others: ImmutablePropTypes.list, - localDomain: PropTypes.string, - inline: PropTypes.bool, - }; - - handleMouseEnter = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-original'); - } - }; - - handleMouseLeave = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-static'); - } - }; - - render () { - const { others, localDomain, inline } = this.props; - - let displayName, suffix, account; - - if (others && others.size > 1) { - displayName = others.take(2).map(a => ).reduce((prev, cur) => [prev, ', ', cur]); - - if (others.size - 2 > 0) { - suffix = `+${others.size - 2}`; - } - } else if ((others && others.size > 0) || this.props.account) { - if (others && others.size > 0) { - account = others.first(); - } else { - account = this.props.account; - } - - let acct = account.get('acct'); - - if (acct.indexOf('@') === -1 && localDomain) { - acct = `${acct}@${localDomain}`; - } - - displayName = ; - suffix = @{acct}; - } else { - displayName = ; - suffix = ; - } - - return ( - - {displayName} - {inline ? ' ' : null} - {suffix} - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/display_name.tsx b/app/javascript/flavours/glitch/components/display_name.tsx new file mode 100644 index 000000000..7224ac3d7 --- /dev/null +++ b/app/javascript/flavours/glitch/components/display_name.tsx @@ -0,0 +1,124 @@ +import React from 'react'; + +import classNames from 'classnames'; + +import type { List } from 'immutable'; + +import type { Account } from 'flavours/glitch/types/resources'; + +import { autoPlayGif } from '../initial_state'; + +import { Skeleton } from './skeleton'; + +interface Props { + account: Account; + others: List; + localDomain: string; + inline?: boolean; +} +export class DisplayName extends React.PureComponent { + handleMouseEnter: React.ReactEventHandler = ({ + currentTarget, + }) => { + if (autoPlayGif) { + return; + } + + const emojis = + currentTarget.querySelectorAll('img.custom-emoji'); + + emojis.forEach((emoji) => { + const originalSrc = emoji.getAttribute('data-original'); + if (originalSrc != null) emoji.src = originalSrc; + }); + }; + + handleMouseLeave: React.ReactEventHandler = ({ + currentTarget, + }) => { + if (autoPlayGif) { + return; + } + + const emojis = + currentTarget.querySelectorAll('img.custom-emoji'); + + emojis.forEach((emoji) => { + const staticSrc = emoji.getAttribute('data-static'); + if (staticSrc != null) emoji.src = staticSrc; + }); + }; + + render() { + const { others, localDomain, inline } = this.props; + + let displayName: React.ReactNode, suffix: React.ReactNode, account: Account; + + if (others && others.size > 1) { + displayName = others + .take(2) + .map((a) => ( + + + + )) + .reduce((prev, cur) => [prev, ', ', cur]); + + if (others.size - 2 > 0) { + suffix = `+${others.size - 2}`; + } + } else if ((others && others.size > 0) || this.props.account) { + if (others && others.size > 0) { + account = others.first(); + } else { + account = this.props.account; + } + + let acct = account.get('acct'); + + if (acct.indexOf('@') === -1 && localDomain) { + acct = `${acct}@${localDomain}`; + } + + displayName = ( + + + + ); + suffix = @{acct}; + } else { + displayName = ( + + + + + + ); + suffix = ( + + + + ); + } + + return ( + + {displayName} + {inline ? ' ' : null} + {suffix} + + ); + } +} diff --git a/app/javascript/flavours/glitch/components/domain.tsx b/app/javascript/flavours/glitch/components/domain.tsx index af0fec35a..9e8e04b65 100644 --- a/app/javascript/flavours/glitch/components/domain.tsx +++ b/app/javascript/flavours/glitch/components/domain.tsx @@ -1,6 +1,9 @@ import React, { useCallback } from 'react'; + +import type { InjectedIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; + import { IconButton } from './icon_button'; -import { InjectedIntl, defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ unblockDomain: { @@ -9,11 +12,11 @@ const messages = defineMessages({ }, }); -type Props = { +interface Props { domain: string; onUnblockDomain: (domain: string) => void; intl: InjectedIntl; -}; +} const _Domain: React.FC = ({ domain, onUnblockDomain, intl }) => { const handleDomainUnblock = useCallback(() => { onUnblockDomain(domain); diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/components/dropdown_menu.jsx index 3f17bc129..901c7fe22 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.jsx +++ b/app/javascript/flavours/glitch/components/dropdown_menu.jsx @@ -7,7 +7,7 @@ import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; import { CircularProgress } from 'flavours/glitch/components/loading_indicator'; -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; let id = 0; class DropdownMenu extends React.PureComponent { @@ -35,12 +35,13 @@ class DropdownMenu extends React.PureComponent { handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); + e.stopPropagation(); } }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('keydown', this.handleKeyDown, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); + document.addEventListener('keydown', this.handleKeyDown, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); if (this.focusedItem && this.props.openedViaKeyboard) { @@ -49,8 +50,8 @@ class DropdownMenu extends React.PureComponent { } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('keydown', this.handleKeyDown, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); + document.removeEventListener('keydown', this.handleKeyDown, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } diff --git a/app/javascript/flavours/glitch/components/gifv.tsx b/app/javascript/flavours/glitch/components/gifv.tsx index 72914ba74..c606a2904 100644 --- a/app/javascript/flavours/glitch/components/gifv.tsx +++ b/app/javascript/flavours/glitch/components/gifv.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from 'react'; -type Props = { +interface Props { src: string; key: string; alt?: string; @@ -8,7 +8,7 @@ type Props = { width: number; height: number; onClick?: () => void; -}; +} export const GIFV: React.FC = ({ src, diff --git a/app/javascript/flavours/glitch/components/hashtag.jsx b/app/javascript/flavours/glitch/components/hashtag.jsx index 235aaeb3c..ced250b79 100644 --- a/app/javascript/flavours/glitch/components/hashtag.jsx +++ b/app/javascript/flavours/glitch/components/hashtag.jsx @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Permalink from './permalink'; import ShortNumber from 'flavours/glitch/components/short_number'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; import classNames from 'classnames'; class SilentErrorBoundary extends React.Component { diff --git a/app/javascript/flavours/glitch/components/icon.tsx b/app/javascript/flavours/glitch/components/icon.tsx index 4eb948dc7..6bd15da6a 100644 --- a/app/javascript/flavours/glitch/components/icon.tsx +++ b/app/javascript/flavours/glitch/components/icon.tsx @@ -1,13 +1,14 @@ import React from 'react'; + import classNames from 'classnames'; -type Props = { +interface Props extends React.HTMLAttributes { id: string; className?: string; fixedWidth?: boolean; children?: never; - [key: string]: any; -}; +} + export const Icon: React.FC = ({ id, className, diff --git a/app/javascript/flavours/glitch/components/icon_button.tsx b/app/javascript/flavours/glitch/components/icon_button.tsx index 80b20ae1b..0b8128824 100644 --- a/app/javascript/flavours/glitch/components/icon_button.tsx +++ b/app/javascript/flavours/glitch/components/icon_button.tsx @@ -1,9 +1,11 @@ import React from 'react'; -import classNames from 'classnames'; -import { Icon } from './icon'; -import { AnimatedNumber } from './animated_number'; -type Props = { +import classNames from 'classnames'; + +import { AnimatedNumber } from './animated_number'; +import { Icon } from './icon'; + +interface Props { className?: string; title: string; icon: string; @@ -26,11 +28,11 @@ type Props = { obfuscateCount?: boolean; href?: string; ariaHidden: boolean; -}; -type States = { +} +interface States { activate: boolean; deactivate: boolean; -}; +} export class IconButton extends React.PureComponent { static defaultProps = { size: 18, diff --git a/app/javascript/flavours/glitch/components/icon_with_badge.tsx b/app/javascript/flavours/glitch/components/icon_with_badge.tsx index bf86814c0..e427b7172 100644 --- a/app/javascript/flavours/glitch/components/icon_with_badge.tsx +++ b/app/javascript/flavours/glitch/components/icon_with_badge.tsx @@ -1,14 +1,15 @@ import React from 'react'; + import { Icon } from './icon'; const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num); -type Props = { +interface Props { id: string; count: number; issueBadge: boolean; className: string; -}; +} export const IconWithBadge: React.FC = ({ id, count, diff --git a/app/javascript/flavours/glitch/components/media_gallery.jsx b/app/javascript/flavours/glitch/components/media_gallery.jsx index 1b4ce0574..33ddb8a29 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.jsx +++ b/app/javascript/flavours/glitch/components/media_gallery.jsx @@ -254,7 +254,7 @@ class MediaGallery extends React.PureComponent { window.removeEventListener('resize', this.handleResize); } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) { this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' }); } else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { @@ -286,7 +286,7 @@ class MediaGallery extends React.PureComponent { }; handleClick = (index) => { - this.props.onOpenMedia(this.props.media, index); + this.props.onOpenMedia(this.props.media, index, this.props.lang); }; handleRef = (node) => { diff --git a/app/javascript/flavours/glitch/components/modal_root.jsx b/app/javascript/flavours/glitch/components/modal_root.jsx index 5a5563e87..f08542e7f 100644 --- a/app/javascript/flavours/glitch/components/modal_root.jsx +++ b/app/javascript/flavours/glitch/components/modal_root.jsx @@ -62,7 +62,7 @@ export default class ModalRoot extends React.PureComponent { } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!!nextProps.children && !this.props.children) { this.activeElement = document.activeElement; diff --git a/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx b/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx index 53945d6a7..ce94c5d87 100644 --- a/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx +++ b/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx @@ -1,4 +1,5 @@ import React from 'react'; + import { FormattedMessage } from 'react-intl'; export const NotSignedInIndicator: React.FC = () => ( @@ -6,7 +7,7 @@ export const NotSignedInIndicator: React.FC = () => (
diff --git a/app/javascript/flavours/glitch/components/radio_button.tsx b/app/javascript/flavours/glitch/components/radio_button.tsx index 829f47174..67acb09f4 100644 --- a/app/javascript/flavours/glitch/components/radio_button.tsx +++ b/app/javascript/flavours/glitch/components/radio_button.tsx @@ -1,13 +1,14 @@ import React from 'react'; + import classNames from 'classnames'; -type Props = { +interface Props { value: string; checked: boolean; name: string; onChange: (event: React.ChangeEvent) => void; label: React.ReactNode; -}; +} export const RadioButton: React.FC = ({ name, diff --git a/app/javascript/flavours/glitch/components/relative_timestamp.tsx b/app/javascript/flavours/glitch/components/relative_timestamp.tsx index 65d9d27cb..e0e0d4bb5 100644 --- a/app/javascript/flavours/glitch/components/relative_timestamp.tsx +++ b/app/javascript/flavours/glitch/components/relative_timestamp.tsx @@ -1,5 +1,7 @@ import React from 'react'; -import { injectIntl, defineMessages, InjectedIntl } from 'react-intl'; + +import type { InjectedIntl } from 'react-intl'; +import { injectIntl, defineMessages } from 'react-intl'; const messages = defineMessages({ today: { id: 'relative_time.today', defaultMessage: 'today' }, @@ -187,16 +189,16 @@ const timeRemainingString = ( return relativeTime; }; -type Props = { +interface Props { intl: InjectedIntl; timestamp: string; year: number; futureDate?: boolean; short?: boolean; -}; -type States = { +} +interface States { now: number; -}; +} class RelativeTimestamp extends React.Component { state = { now: this.props.intl.now(), diff --git a/app/javascript/flavours/glitch/components/scrollable_list.jsx b/app/javascript/flavours/glitch/components/scrollable_list.jsx index 13701bcd3..69925321b 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.jsx +++ b/app/javascript/flavours/glitch/components/scrollable_list.jsx @@ -15,6 +15,8 @@ import { connect } from 'react-redux'; const MOUSE_IDLE_DELAY = 300; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + const mapStateToProps = (state, { scrollKey }) => { return { preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']), @@ -237,20 +239,20 @@ class ScrollableList extends PureComponent { attachScrollListener () { if (this.props.bindToDocument) { document.addEventListener('scroll', this.handleScroll); - document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : undefined); + document.addEventListener('wheel', this.handleWheel, listenerOptions); } else { this.node.addEventListener('scroll', this.handleScroll); - this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : undefined); + this.node.addEventListener('wheel', this.handleWheel, listenerOptions); } } detachScrollListener () { if (this.props.bindToDocument) { document.removeEventListener('scroll', this.handleScroll); - document.removeEventListener('wheel', this.handleWheel); + document.removeEventListener('wheel', this.handleWheel, listenerOptions); } else { this.node.removeEventListener('scroll', this.handleScroll); - this.node.removeEventListener('wheel', this.handleWheel); + this.node.removeEventListener('wheel', this.handleWheel, listenerOptions); } } diff --git a/app/javascript/flavours/glitch/components/server_banner.jsx b/app/javascript/flavours/glitch/components/server_banner.jsx index 8b9c50153..25ec40560 100644 --- a/app/javascript/flavours/glitch/components/server_banner.jsx +++ b/app/javascript/flavours/glitch/components/server_banner.jsx @@ -4,10 +4,10 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { fetchServer } from 'flavours/glitch/actions/server'; import ShortNumber from 'flavours/glitch/components/short_number'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; import Account from 'flavours/glitch/containers/account_container'; import { domain } from 'flavours/glitch/initial_state'; -import { Image } from 'flavours/glitch/components/image'; +import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image'; import { Link } from 'react-router-dom'; const messages = defineMessages({ @@ -41,7 +41,7 @@ class ServerBanner extends React.PureComponent { {domain}
, mastodon: Mastodon }} /> - +
{isLoading ? ( diff --git a/app/javascript/mastodon/components/image.tsx b/app/javascript/flavours/glitch/components/server_hero_image.tsx similarity index 90% rename from app/javascript/mastodon/components/image.tsx rename to app/javascript/flavours/glitch/components/server_hero_image.tsx index 490543424..973d1d1b3 100644 --- a/app/javascript/mastodon/components/image.tsx +++ b/app/javascript/flavours/glitch/components/server_hero_image.tsx @@ -1,15 +1,17 @@ import React, { useCallback, useState } from 'react'; -import { Blurhash } from './blurhash'; + import classNames from 'classnames'; -type Props = { +import { Blurhash } from './blurhash'; + +interface Props { src: string; srcSet?: string; blurhash?: string; className?: string; -}; +} -export const Image: React.FC = ({ +export const ServerHeroImage: React.FC = ({ src, srcSet, blurhash, diff --git a/app/javascript/flavours/glitch/components/skeleton.jsx b/app/javascript/flavours/glitch/components/skeleton.jsx deleted file mode 100644 index 6a17ffb26..000000000 --- a/app/javascript/flavours/glitch/components/skeleton.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const Skeleton = ({ width, height }) => ; - -Skeleton.propTypes = { - width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), -}; - -export default Skeleton; diff --git a/app/javascript/flavours/glitch/components/skeleton.tsx b/app/javascript/flavours/glitch/components/skeleton.tsx new file mode 100644 index 000000000..8d43e6827 --- /dev/null +++ b/app/javascript/flavours/glitch/components/skeleton.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +interface Props { + width?: number | string; + height?: number | string; +} + +export const Skeleton: React.FC = ({ width, height }) => ( + + ‌ + +); diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index 78deb5f2a..441936a43 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -388,11 +388,12 @@ class Status extends ImmutablePureComponent { handleOpenVideo = (options) => { const { status } = this.props; - this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options); + this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), status.get('language'), options); }; handleOpenMedia = (media, index) => { - this.props.onOpenMedia(this.props.status.get('id'), media, index); + const { status } = this.props; + this.props.onOpenMedia(status.get('id'), media, index, status.get('language')); }; handleHotkeyOpenMedia = e => { diff --git a/app/javascript/flavours/glitch/components/status_header.jsx b/app/javascript/flavours/glitch/components/status_header.jsx index 8813d19b9..8f5461357 100644 --- a/app/javascript/flavours/glitch/components/status_header.jsx +++ b/app/javascript/flavours/glitch/components/status_header.jsx @@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; // Mastodon imports. import { Avatar } from './avatar'; import AvatarOverlay from './avatar_overlay'; -import DisplayName from './display_name'; +import { DisplayName } from './display_name'; export default class StatusHeader extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/status_list.jsx b/app/javascript/flavours/glitch/components/status_list.jsx index a9c06f693..a225883a8 100644 --- a/app/javascript/flavours/glitch/components/status_list.jsx +++ b/app/javascript/flavours/glitch/components/status_list.jsx @@ -26,6 +26,7 @@ export default class StatusList extends ImmutablePureComponent { alwaysPrepend: PropTypes.bool, withCounters: PropTypes.bool, timelineId: PropTypes.string.isRequired, + lastId: PropTypes.string, regex: PropTypes.string, }; @@ -56,7 +57,8 @@ export default class StatusList extends ImmutablePureComponent { }; handleLoadOlder = debounce(() => { - this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined); + const { statusIds, lastId, onLoadMore } = this.props; + onLoadMore(lastId || (statusIds.size > 0 ? statusIds.last() : undefined)); }, 300, { leading: true }); _selectChild (index, align_top) { diff --git a/app/javascript/flavours/glitch/components/timeline_hint.jsx b/app/javascript/flavours/glitch/components/timeline_hint.jsx deleted file mode 100644 index fb55a62cc..000000000 --- a/app/javascript/flavours/glitch/components/timeline_hint.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; - -const TimelineHint = ({ resource, url }) => ( -
- -
- -
-); - -TimelineHint.propTypes = { - resource: PropTypes.node.isRequired, - url: PropTypes.string.isRequired, -}; - -export default TimelineHint; diff --git a/app/javascript/flavours/glitch/components/timeline_hint.tsx b/app/javascript/flavours/glitch/components/timeline_hint.tsx new file mode 100644 index 000000000..712b4c293 --- /dev/null +++ b/app/javascript/flavours/glitch/components/timeline_hint.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import { FormattedMessage } from 'react-intl'; + +interface Props { + resource: JSX.Element; + url: string; +} + +export const TimelineHint: React.FC = ({ resource, url }) => ( +
+ + + +
+ + + +
+); diff --git a/app/javascript/flavours/glitch/containers/media_container.jsx b/app/javascript/flavours/glitch/containers/media_container.jsx index f245e2f0a..99e1ed55c 100644 --- a/app/javascript/flavours/glitch/containers/media_container.jsx +++ b/app/javascript/flavours/glitch/containers/media_container.jsx @@ -1,5 +1,5 @@ import React, { PureComponent, Fragment } from 'react'; -import ReactDOM from 'react-dom'; +import { createPortal } from 'react-dom'; import PropTypes from 'prop-types'; import { IntlProvider, addLocaleData } from 'react-intl'; import { fromJS } from 'immutable'; @@ -29,19 +29,20 @@ export default class MediaContainer extends PureComponent { state = { media: null, index: null, + lang: null, time: null, backgroundColor: null, options: null, }; - handleOpenMedia = (media, index) => { + handleOpenMedia = (media, index, lang) => { document.body.classList.add('with-modals--active'); document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; - this.setState({ media, index }); + this.setState({ media, index, lang }); }; - handleOpenVideo = (options) => { + handleOpenVideo = (lang, options) => { const { components } = this.props; const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props')); const mediaList = fromJS(media); @@ -49,7 +50,7 @@ export default class MediaContainer extends PureComponent { document.body.classList.add('with-modals--active'); document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; - this.setState({ media: mediaList, options }); + this.setState({ media: mediaList, lang, options }); }; handleCloseMedia = () => { @@ -94,7 +95,7 @@ export default class MediaContainer extends PureComponent { }), }); - return ReactDOM.createPortal( + return createPortal( , component, ); @@ -105,6 +106,7 @@ export default class MediaContainer extends PureComponent { ({ dispatch(mentionCompose(account, router)); }, - onOpenMedia (statusId, media, index) { - dispatch(openModal('MEDIA', { statusId, media, index })); + onOpenMedia (statusId, media, index, lang) { + dispatch(openModal('MEDIA', { statusId, media, index, lang })); }, - onOpenVideo (statusId, media, options) { - dispatch(openModal('VIDEO', { statusId, media, options })); + onOpenVideo (statusId, media, lang, options) { + dispatch(openModal('VIDEO', { statusId, media, lang, options })); }, onBlock (status) { diff --git a/app/javascript/flavours/glitch/features/about/index.jsx b/app/javascript/flavours/glitch/features/about/index.jsx index e2556ec34..8b65d9571 100644 --- a/app/javascript/flavours/glitch/features/about/index.jsx +++ b/app/javascript/flavours/glitch/features/about/index.jsx @@ -8,10 +8,10 @@ import LinkFooter from 'flavours/glitch/features/ui/components/link_footer'; import { Helmet } from 'react-helmet'; import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server'; import Account from 'flavours/glitch/containers/account_container'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; import { Icon } from 'flavours/glitch/components/icon'; import classNames from 'classnames'; -import { Image } from 'flavours/glitch/components/image'; +import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image'; const messages = defineMessages({ title: { id: 'column.about', defaultMessage: 'About' }, @@ -114,7 +114,7 @@ class About extends React.PureComponent {
- `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' /> + `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />

{isLoading ? : server.get('domain')}

Mastodon }} />

diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.jsx b/app/javascript/flavours/glitch/features/account_gallery/index.jsx index b4fc8be72..26c45eff6 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.jsx +++ b/app/javascript/flavours/glitch/features/account_gallery/index.jsx @@ -142,16 +142,17 @@ class AccountGallery extends ImmutablePureComponent { handleOpenMedia = attachment => { const { dispatch } = this.props; const statusId = attachment.getIn(['status', 'id']); + const lang = attachment.getIn(['status', 'language']); if (attachment.get('type') === 'video') { - dispatch(openModal('VIDEO', { media: attachment, statusId, options: { autoPlay: true } })); + dispatch(openModal('VIDEO', { media: attachment, statusId, lang, options: { autoPlay: true } })); } else if (attachment.get('type') === 'audio') { - dispatch(openModal('AUDIO', { media: attachment, statusId, options: { autoPlay: true } })); + dispatch(openModal('AUDIO', { media: attachment, statusId, lang, options: { autoPlay: true } })); } else { const media = attachment.getIn(['status', 'media_attachments']); const index = media.findIndex(x => x.get('id') === attachment.get('id')); - dispatch(openModal('MEDIA', { media, index, statusId })); + dispatch(openModal('MEDIA', { media, index, statusId, lang })); } }; diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx index 908b70c5f..7d025ed41 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx @@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import AvatarOverlay from '../../../components/avatar_overlay'; -import DisplayName from '../../../components/display_name'; +import { DisplayName } from '../../../components/display_name'; import { Icon } from 'flavours/glitch/components/icon'; export default class MovedNote extends ImmutablePureComponent { diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.jsx b/app/javascript/flavours/glitch/features/account_timeline/index.jsx index 87c8d7467..b90918a01 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/index.jsx @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts'; -import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines'; +import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines'; import StatusList from '../../components/status_list'; import LoadingIndicator from '../../components/loading_indicator'; import Column from '../ui/components/column'; @@ -12,7 +12,7 @@ import HeaderContainer from './containers/header_container'; import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; -import TimelineHint from 'flavours/glitch/components/timeline_hint'; +import { TimelineHint } from 'flavours/glitch/components/timeline_hint'; import LimitedAccountHint from './components/limited_account_hint'; import { getAccountHidden } from 'flavours/glitch/selectors'; import { fetchFeaturedTags } from '../../actions/featured_tags'; @@ -122,7 +122,7 @@ class AccountTimeline extends ImmutablePureComponent { } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { const { dispatch } = this.props; if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { diff --git a/app/javascript/flavours/glitch/features/audio/index.jsx b/app/javascript/flavours/glitch/features/audio/index.jsx index 8c13f6797..25252c01c 100644 --- a/app/javascript/flavours/glitch/features/audio/index.jsx +++ b/app/javascript/flavours/glitch/features/audio/index.jsx @@ -142,7 +142,7 @@ class Audio extends React.PureComponent { } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { this.setState({ revealed: nextProps.visible }); } diff --git a/app/javascript/flavours/glitch/features/blocks/index.jsx b/app/javascript/flavours/glitch/features/blocks/index.jsx index 461dac2ec..669425c7f 100644 --- a/app/javascript/flavours/glitch/features/blocks/index.jsx +++ b/app/javascript/flavours/glitch/features/blocks/index.jsx @@ -34,7 +34,7 @@ class Blocks extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchBlocks()); } diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx index 90d8fd0ef..d9c074682 100644 --- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx +++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx @@ -34,7 +34,7 @@ class Bookmarks extends ImmutablePureComponent { isLoading: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchBookmarkedStatuses()); } diff --git a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx index 8b7969250..db17c6e72 100644 --- a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { Avatar } from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; +import { DisplayName } from 'flavours/glitch/components/display_name'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx index 786f59307..28f316619 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx @@ -2,12 +2,12 @@ import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; +import { supportsPassiveEvents } from 'detect-passive-events'; // Components. import { Icon } from 'flavours/glitch/components/icon'; -// Utils. -import { withPassive } from 'flavours/glitch/utils/dom_helpers'; +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; // The component. export default class ComposerOptionsDropdownContent extends React.PureComponent { @@ -41,6 +41,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent handleDocumentClick = (e) => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); + e.stopPropagation(); } }; @@ -51,8 +52,8 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent // On mounting, we add our listeners. componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, withPassive); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); if (this.focusedItem) { this.focusedItem.focus({ preventScroll: true }); } else { @@ -62,8 +63,8 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent // On unmounting, we remove our listeners. componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, withPassive); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } handleClick = (e) => { diff --git a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx index 1b8991f00..68ff37b80 100644 --- a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx @@ -28,7 +28,7 @@ const messages = defineMessages({ let EmojiPicker, Emoji; // load asynchronously -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`; @@ -60,7 +60,7 @@ class ModifierPickerMenu extends React.PureComponent { this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1); }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.active) { this.attachListeners(); } else { @@ -79,12 +79,12 @@ class ModifierPickerMenu extends React.PureComponent { }; attachListeners () { - document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); } removeListeners () { - document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } @@ -177,7 +177,7 @@ class EmojiPickerMenuImpl extends React.PureComponent { }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need @@ -192,7 +192,7 @@ class EmojiPickerMenuImpl extends React.PureComponent { } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx index 05614de01..2d4ae72d4 100644 --- a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx @@ -15,7 +15,7 @@ const messages = defineMessages({ clear: { id: 'emoji_button.clear', defaultMessage: 'Clear' }, }); -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; class LanguageDropdownMenu extends React.PureComponent { @@ -39,11 +39,12 @@ class LanguageDropdownMenu extends React.PureComponent { handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); + e.stopPropagation(); } }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need @@ -57,7 +58,7 @@ class LanguageDropdownMenu extends React.PureComponent { } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.jsx b/app/javascript/flavours/glitch/features/directory/components/account_card.jsx index 79543389c..da11f881f 100644 --- a/app/javascript/flavours/glitch/features/directory/components/account_card.jsx +++ b/app/javascript/flavours/glitch/features/directory/components/account_card.jsx @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { makeGetAccount } from 'flavours/glitch/selectors'; import { Avatar } from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; +import { DisplayName } from 'flavours/glitch/components/display_name'; import Permalink from 'flavours/glitch/components/permalink'; import { IconButton } from 'flavours/glitch/components/icon_button'; import Button from 'flavours/glitch/components/button'; diff --git a/app/javascript/flavours/glitch/features/domain_blocks/index.jsx b/app/javascript/flavours/glitch/features/domain_blocks/index.jsx index 1ab7c3663..b9ef73959 100644 --- a/app/javascript/flavours/glitch/features/domain_blocks/index.jsx +++ b/app/javascript/flavours/glitch/features/domain_blocks/index.jsx @@ -34,7 +34,7 @@ class Blocks extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchDomainBlocks()); } diff --git a/app/javascript/flavours/glitch/features/explore/components/story.jsx b/app/javascript/flavours/glitch/features/explore/components/story.jsx index d70914d41..cadc710d4 100644 --- a/app/javascript/flavours/glitch/features/explore/components/story.jsx +++ b/app/javascript/flavours/glitch/features/explore/components/story.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Blurhash } from 'flavours/glitch/components/blurhash'; import { accountsCountRenderer } from 'flavours/glitch/components/hashtag'; import ShortNumber from 'flavours/glitch/components/short_number'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; import classNames from 'classnames'; export default class Story extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/features/favourited_statuses/index.jsx b/app/javascript/flavours/glitch/features/favourited_statuses/index.jsx index 60d281f97..9b63dec23 100644 --- a/app/javascript/flavours/glitch/features/favourited_statuses/index.jsx +++ b/app/javascript/flavours/glitch/features/favourited_statuses/index.jsx @@ -34,7 +34,7 @@ class Favourites extends ImmutablePureComponent { isLoading: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchFavouritedStatuses()); } diff --git a/app/javascript/flavours/glitch/features/favourites/index.jsx b/app/javascript/flavours/glitch/features/favourites/index.jsx index 8755811d9..3287c5989 100644 --- a/app/javascript/flavours/glitch/features/favourites/index.jsx +++ b/app/javascript/flavours/glitch/features/favourites/index.jsx @@ -32,13 +32,13 @@ class Favourites extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; - componentWillMount () { + UNSAFE_componentWillMount () { if (!this.props.accountIds) { this.props.dispatch(fetchFavourites(this.props.params.statusId)); } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { this.props.dispatch(fetchFavourites(nextProps.params.statusId)); } diff --git a/app/javascript/flavours/glitch/features/follow_recommendations/components/account.jsx b/app/javascript/flavours/glitch/features/follow_recommendations/components/account.jsx index e15bc1422..4eeffc402 100644 --- a/app/javascript/flavours/glitch/features/follow_recommendations/components/account.jsx +++ b/app/javascript/flavours/glitch/features/follow_recommendations/components/account.jsx @@ -5,7 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; import { makeGetAccount } from 'flavours/glitch/selectors'; import { Avatar } from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; +import { DisplayName } from 'flavours/glitch/components/display_name'; import Permalink from 'flavours/glitch/components/permalink'; import { IconButton } from 'flavours/glitch/components/icon_button'; import { injectIntl, defineMessages } from 'react-intl'; diff --git a/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.jsx b/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.jsx index b2dfc5dbb..31e79f042 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.jsx +++ b/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Permalink from 'flavours/glitch/components/permalink'; import { Avatar } from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; +import { DisplayName } from 'flavours/glitch/components/display_name'; import { IconButton } from 'flavours/glitch/components/icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; diff --git a/app/javascript/flavours/glitch/features/follow_requests/index.jsx b/app/javascript/flavours/glitch/features/follow_requests/index.jsx index a9a35f54b..7ca707ec9 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/index.jsx +++ b/app/javascript/flavours/glitch/features/follow_requests/index.jsx @@ -39,7 +39,7 @@ class FollowRequests extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchFollowRequests()); } diff --git a/app/javascript/flavours/glitch/features/followers/index.jsx b/app/javascript/flavours/glitch/features/followers/index.jsx index 2c4db665a..39eaed128 100644 --- a/app/javascript/flavours/glitch/features/followers/index.jsx +++ b/app/javascript/flavours/glitch/features/followers/index.jsx @@ -17,7 +17,7 @@ import ProfileColumnHeader from 'flavours/glitch/features/account/components/pro import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; -import TimelineHint from 'flavours/glitch/components/timeline_hint'; +import { TimelineHint } from 'flavours/glitch/components/timeline_hint'; import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; import { getAccountHidden } from 'flavours/glitch/selectors'; import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; diff --git a/app/javascript/flavours/glitch/features/following/index.jsx b/app/javascript/flavours/glitch/features/following/index.jsx index 39ff4c603..39f87eef9 100644 --- a/app/javascript/flavours/glitch/features/following/index.jsx +++ b/app/javascript/flavours/glitch/features/following/index.jsx @@ -17,7 +17,7 @@ import ProfileColumnHeader from 'flavours/glitch/features/account/components/pro import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; -import TimelineHint from 'flavours/glitch/components/timeline_hint'; +import { TimelineHint } from 'flavours/glitch/components/timeline_hint'; import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; import { getAccountHidden } from 'flavours/glitch/selectors'; import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; diff --git a/app/javascript/flavours/glitch/features/getting_started/index.jsx b/app/javascript/flavours/glitch/features/getting_started/index.jsx index 2ef007da5..8356e977e 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.jsx +++ b/app/javascript/flavours/glitch/features/getting_started/index.jsx @@ -96,7 +96,7 @@ class GettingStarted extends ImmutablePureComponent { openSettings: PropTypes.func.isRequired, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.fetchLists(); } diff --git a/app/javascript/flavours/glitch/features/interaction_modal/index.jsx b/app/javascript/flavours/glitch/features/interaction_modal/index.jsx index 380edee65..515e47de2 100644 --- a/app/javascript/flavours/glitch/features/interaction_modal/index.jsx +++ b/app/javascript/flavours/glitch/features/interaction_modal/index.jsx @@ -143,7 +143,7 @@ class InteractionModal extends React.PureComponent {

- + {signupButton}
diff --git a/app/javascript/flavours/glitch/features/list_adder/components/account.jsx b/app/javascript/flavours/glitch/features/list_adder/components/account.jsx index 64522a3a8..0e4b098d9 100644 --- a/app/javascript/flavours/glitch/features/list_adder/components/account.jsx +++ b/app/javascript/flavours/glitch/features/list_adder/components/account.jsx @@ -4,7 +4,7 @@ import { makeGetAccount } from '../../../selectors'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Avatar } from '../../../components/avatar'; -import DisplayName from '../../../components/display_name'; +import { DisplayName } from '../../../components/display_name'; import { injectIntl } from 'react-intl'; const makeMapStateToProps = () => { diff --git a/app/javascript/flavours/glitch/features/list_editor/components/account.jsx b/app/javascript/flavours/glitch/features/list_editor/components/account.jsx index 69fbaf504..b94b7e3f6 100644 --- a/app/javascript/flavours/glitch/features/list_editor/components/account.jsx +++ b/app/javascript/flavours/glitch/features/list_editor/components/account.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Avatar } from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; +import { DisplayName } from 'flavours/glitch/components/display_name'; import { IconButton } from 'flavours/glitch/components/icon_button'; import { defineMessages } from 'react-intl'; diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.jsx b/app/javascript/flavours/glitch/features/list_timeline/index.jsx index f7abe248d..880366476 100644 --- a/app/javascript/flavours/glitch/features/list_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/list_timeline/index.jsx @@ -76,7 +76,7 @@ class ListTimeline extends React.PureComponent { this.disconnect = dispatch(connectListStream(id)); } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { const { dispatch } = this.props; const { id } = nextProps.params; diff --git a/app/javascript/flavours/glitch/features/lists/index.jsx b/app/javascript/flavours/glitch/features/lists/index.jsx index dce0dcd8f..375ab5fae 100644 --- a/app/javascript/flavours/glitch/features/lists/index.jsx +++ b/app/javascript/flavours/glitch/features/lists/index.jsx @@ -42,7 +42,7 @@ class Lists extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchLists()); } diff --git a/app/javascript/flavours/glitch/features/mutes/index.jsx b/app/javascript/flavours/glitch/features/mutes/index.jsx index b699fdb27..e1a76e2fe 100644 --- a/app/javascript/flavours/glitch/features/mutes/index.jsx +++ b/app/javascript/flavours/glitch/features/mutes/index.jsx @@ -35,7 +35,7 @@ class Mutes extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchMutes()); } diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx b/app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx index d7c229404..c61f656f6 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx @@ -2,7 +2,7 @@ import React, { Fragment } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { Avatar } from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; +import { DisplayName } from 'flavours/glitch/components/display_name'; import Permalink from 'flavours/glitch/components/permalink'; import { IconButton } from 'flavours/glitch/components/icon_button'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx b/app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx index b9c72c276..43a910c50 100644 --- a/app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx +++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import { IconButton } from 'flavours/glitch/components/icon_button'; import { Link } from 'react-router-dom'; import { Avatar } from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; +import { DisplayName } from 'flavours/glitch/components/display_name'; import { defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ diff --git a/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx b/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx index 41be2f7f3..5f6035cb1 100644 --- a/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx +++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx @@ -29,7 +29,7 @@ class PinnedStatuses extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchPinnedStatuses()); } diff --git a/app/javascript/flavours/glitch/features/privacy_policy/index.jsx b/app/javascript/flavours/glitch/features/privacy_policy/index.jsx index a43befa73..44cb9b0d0 100644 --- a/app/javascript/flavours/glitch/features/privacy_policy/index.jsx +++ b/app/javascript/flavours/glitch/features/privacy_policy/index.jsx @@ -4,7 +4,7 @@ import { Helmet } from 'react-helmet'; import { FormattedMessage, FormattedDate, injectIntl, defineMessages } from 'react-intl'; import Column from 'flavours/glitch/components/column'; import api from 'flavours/glitch/api'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; const messages = defineMessages({ title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' }, diff --git a/app/javascript/flavours/glitch/features/reblogs/index.jsx b/app/javascript/flavours/glitch/features/reblogs/index.jsx index fe74f1fbc..5b03500c2 100644 --- a/app/javascript/flavours/glitch/features/reblogs/index.jsx +++ b/app/javascript/flavours/glitch/features/reblogs/index.jsx @@ -32,13 +32,13 @@ class Reblogs extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; - componentWillMount () { + UNSAFE_componentWillMount () { if (!this.props.accountIds) { this.props.dispatch(fetchReblogs(this.props.params.statusId)); } } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { this.props.dispatch(fetchReblogs(nextProps.params.statusId)); } diff --git a/app/javascript/flavours/glitch/features/report/components/status_check_box.jsx b/app/javascript/flavours/glitch/features/report/components/status_check_box.jsx index 6b74aafee..92ccd4092 100644 --- a/app/javascript/flavours/glitch/features/report/components/status_check_box.jsx +++ b/app/javascript/flavours/glitch/features/report/components/status_check_box.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import StatusContent from 'flavours/glitch/components/status_content'; import { Avatar } from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; +import { DisplayName } from 'flavours/glitch/components/display_name'; import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; import Option from './option'; import MediaAttachments from 'flavours/glitch/components/media_attachments'; diff --git a/app/javascript/flavours/glitch/features/status/components/card.jsx b/app/javascript/flavours/glitch/features/status/components/card.jsx index 9b84da06e..dd91b9334 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.jsx +++ b/app/javascript/flavours/glitch/features/status/components/card.jsx @@ -57,7 +57,7 @@ export default class Card extends React.PureComponent { revealed: !this.props.sensitive, }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!Immutable.is(this.props.card, nextProps.card)) { this.setState({ embedded: false, previewLoaded: false }); } diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx index 992dd677d..e36ab818f 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Avatar } from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; +import { DisplayName } from 'flavours/glitch/components/display_name'; import StatusContent from 'flavours/glitch/components/status_content'; import MediaGallery from 'flavours/glitch/components/media_gallery'; import AttachmentList from 'flavours/glitch/components/attachment_list'; diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js index 25517a9f7..0b3e0a2f0 100644 --- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js +++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js @@ -125,12 +125,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(mentionCompose(account, router)); }, - onOpenMedia (media, index) { - dispatch(openModal('MEDIA', { media, index })); + onOpenMedia (media, index, lang) { + dispatch(openModal('MEDIA', { media, index, lang })); }, - onOpenVideo (media, options) { - dispatch(openModal('VIDEO', { media, options })); + onOpenVideo (media, lang, options) { + dispatch(openModal('VIDEO', { media, lang, options })); }, onBlock (status) { diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index 09c1aa992..39a778045 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -392,12 +392,12 @@ class Status extends ImmutablePureComponent { this.props.dispatch(mentionCompose(account, router)); }; - handleOpenMedia = (media, index) => { - this.props.dispatch(openModal('MEDIA', { statusId: this.props.status.get('id'), media, index })); + handleOpenMedia = (media, index, lang) => { + this.props.dispatch(openModal('MEDIA', { statusId: this.props.status.get('id'), media, index, lang })); }; - handleOpenVideo = (media, options) => { - this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, options })); + handleOpenVideo = (media, lang, options) => { + this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, lang, options })); }; handleHotkeyOpenMedia = e => { diff --git a/app/javascript/flavours/glitch/features/ui/components/actions_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/actions_modal.jsx index 6a98e8b53..cf4bf4634 100644 --- a/app/javascript/flavours/glitch/features/ui/components/actions_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/actions_modal.jsx @@ -5,7 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import StatusContent from 'flavours/glitch/components/status_content'; import { Avatar } from 'flavours/glitch/components/avatar'; import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; -import DisplayName from 'flavours/glitch/components/display_name'; +import { DisplayName } from 'flavours/glitch/components/display_name'; import classNames from 'classnames'; import { IconButton } from 'flavours/glitch/components/icon_button'; diff --git a/app/javascript/flavours/glitch/features/ui/components/boost_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/boost_modal.jsx index 31ae4f68f..6128df249 100644 --- a/app/javascript/flavours/glitch/features/ui/components/boost_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/boost_modal.jsx @@ -7,7 +7,7 @@ import Button from 'flavours/glitch/components/button'; import StatusContent from 'flavours/glitch/components/status_content'; import { Avatar } from 'flavours/glitch/components/avatar'; import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; -import DisplayName from 'flavours/glitch/components/display_name'; +import { DisplayName } from 'flavours/glitch/components/display_name'; import AttachmentList from 'flavours/glitch/components/attachment_list'; import { Icon } from 'flavours/glitch/components/icon'; import ImmutablePureComponent from 'react-immutable-pure-component'; diff --git a/app/javascript/flavours/glitch/features/ui/components/bundle.jsx b/app/javascript/flavours/glitch/features/ui/components/bundle.jsx index 27b13ecfe..0e3a9ae04 100644 --- a/app/javascript/flavours/glitch/features/ui/components/bundle.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/bundle.jsx @@ -33,11 +33,11 @@ class Bundle extends React.Component { forceRender: false, }; - componentWillMount() { + UNSAFE_componentWillMount() { this.load(this.props); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.fetchComponent !== this.props.fetchComponent) { this.load(nextProps); } diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx b/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx index 458f48cde..a466a3eb6 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx @@ -18,7 +18,7 @@ import { BookmarkedStatuses, ListTimeline, Directory, -} from '../../ui/util/async-components'; +} from '../util/async-components'; import ComposePanel from './compose_panel'; import NavigationPanel from './navigation_panel'; diff --git a/app/javascript/flavours/glitch/features/ui/components/embed_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/embed_modal.jsx index 1c83517a1..7c961fa57 100644 --- a/app/javascript/flavours/glitch/features/ui/components/embed_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/embed_modal.jsx @@ -85,7 +85,7 @@ class EmbedModal extends ImmutablePureComponent { className='embed-modal__iframe' frameBorder='0' ref={this.setIframeRef} - sandbox='allow-same-origin' + sandbox='allow-scripts allow-same-origin' title='preview' />
diff --git a/app/javascript/flavours/glitch/features/ui/components/favourite_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/favourite_modal.jsx index 4090b6500..766a35c86 100644 --- a/app/javascript/flavours/glitch/features/ui/components/favourite_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/favourite_modal.jsx @@ -6,7 +6,7 @@ import Button from 'flavours/glitch/components/button'; import StatusContent from 'flavours/glitch/components/status_content'; import { Avatar } from 'flavours/glitch/components/avatar'; import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; -import DisplayName from 'flavours/glitch/components/display_name'; +import { DisplayName } from 'flavours/glitch/components/display_name'; import AttachmentList from 'flavours/glitch/components/attachment_list'; import { Icon } from 'flavours/glitch/components/icon'; import ImmutablePureComponent from 'react-immutable-pure-component'; diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx index 2b61bad70..250e781cf 100644 --- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; import classNames from 'classnames'; -import { changeUploadCompose, uploadThumbnail, onChangeMediaDescription, onChangeMediaFocus } from 'flavours/glitch/actions/compose'; +import { changeUploadCompose, uploadThumbnail, onChangeMediaDescription, onChangeMediaFocus } from '../../../actions/compose'; import Video, { getPointerPosition } from 'flavours/glitch/features/video'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { IconButton } from 'flavours/glitch/components/icon_button'; diff --git a/app/javascript/flavours/glitch/features/ui/components/header.jsx b/app/javascript/flavours/glitch/features/ui/components/header.jsx index 519f917ca..3e56f9043 100644 --- a/app/javascript/flavours/glitch/features/ui/components/header.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/header.jsx @@ -52,13 +52,13 @@ class Header extends React.PureComponent { if (registrationsOpen) { signupButton = ( - + ); } else { signupButton = ( - ); @@ -66,8 +66,8 @@ class Header extends React.PureComponent { content = ( <> - {signupButton} + ); } diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx index 1e51db009..36d42d270 100644 --- a/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx @@ -3,7 +3,6 @@ import ReactSwipeableViews from 'react-swipeable-views'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import Video from 'flavours/glitch/features/video'; -import { connect } from 'react-redux'; import classNames from 'classnames'; import { defineMessages, injectIntl } from 'react-intl'; import { IconButton } from 'flavours/glitch/components/icon_button'; @@ -21,10 +20,6 @@ const messages = defineMessages({ next: { id: 'lightbox.next', defaultMessage: 'Next' }, }); -const mapStateToProps = (state, { statusId }) => ({ - language: state.getIn(['statuses', statusId, 'language']), -}); - class MediaModal extends ImmutablePureComponent { static contextTypes = { @@ -34,6 +29,7 @@ class MediaModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.list.isRequired, statusId: PropTypes.string, + lang: PropTypes.string, index: PropTypes.number.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -135,7 +131,7 @@ class MediaModal extends ImmutablePureComponent { } render () { - const { media, language, statusId, intl, onClose } = this.props; + const { media, statusId, lang, intl, onClose } = this.props; const { navigationHidden } = this.state; const index = this.getIndex(); @@ -155,7 +151,7 @@ class MediaModal extends ImmutablePureComponent { width={width} height={height} alt={image.get('description')} - lang={language} + lang={lang} key={image.get('url')} onClick={this.toggleNavigation} zoomButtonHidden={this.state.zoomButtonHidden} @@ -178,7 +174,7 @@ class MediaModal extends ImmutablePureComponent { onCloseVideo={onClose} detailed alt={image.get('description')} - lang={language} + lang={lang} key={image.get('url')} /> ); @@ -190,7 +186,7 @@ class MediaModal extends ImmutablePureComponent { height={height} key={image.get('url')} alt={image.get('description')} - lang={language} + lang={lang} onClick={this.toggleNavigation} /> ); @@ -258,4 +254,4 @@ class MediaModal extends ImmutablePureComponent { } -export default connect(mapStateToProps, null, null, { forwardRef: true })(injectIntl(MediaModal)); +export default injectIntl(MediaModal); diff --git a/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.jsx index a28be0e88..4e8e4b496 100644 --- a/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.jsx @@ -184,7 +184,7 @@ class OnboardingModal extends React.PureComponent { currentIndex: 0, }; - componentWillMount() { + UNSAFE_componentWillMount() { const { myAccount, admin, domain, intl } = this.props; this.pages = [ , diff --git a/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.jsx b/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.jsx index c0d62aca0..86f96da9b 100644 --- a/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.jsx @@ -16,13 +16,13 @@ const SignInBanner = () => { if (registrationsOpen) { signupButton = ( - + ); } else { signupButton = ( - ); @@ -30,9 +30,9 @@ const SignInBanner = () => { return (
-

- +

{signupButton} +
); }; diff --git a/app/javascript/flavours/glitch/features/ui/components/upload_area.jsx b/app/javascript/flavours/glitch/features/ui/components/upload_area.jsx index 0e07b67f8..5b4d0ce60 100644 --- a/app/javascript/flavours/glitch/features/ui/components/upload_area.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/upload_area.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Motion from '../../ui/util/optional_motion'; +import Motion from '../util/optional_motion'; import spring from 'react-motion/lib/spring'; import { FormattedMessage } from 'react-intl'; diff --git a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js index 3cd0707f2..92ddc01c8 100644 --- a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js +++ b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js @@ -60,6 +60,7 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, { timelineId, regex }) => ({ statusIds: getStatusIds(state, { type: timelineId, regex }), + lastId: state.getIn(['timelines', timelineId, 'items'])?.last(), isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), hasMore: state.getIn(['timelines', timelineId, 'hasMore']), diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx index a29070bc7..15b94273d 100644 --- a/app/javascript/flavours/glitch/features/ui/index.jsx +++ b/app/javascript/flavours/glitch/features/ui/index.jsx @@ -64,7 +64,7 @@ import Header from './components/header'; // Dummy import, to make sure that ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. -import '../../../glitch/components/status'; +import "../../components/status"; const messages = defineMessages({ beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' }, @@ -133,7 +133,7 @@ class SwitchingColumnsArea extends React.PureComponent { mobile: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { if (this.props.mobile) { document.body.classList.toggle('layout-single-column', true); document.body.classList.toggle('layout-multiple-columns', false); @@ -438,7 +438,7 @@ class UI extends React.Component { } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.layout_local_setting !== this.props.layout_local_setting) { const layout = layoutFromWindow(nextProps.layout_local_setting); diff --git a/app/javascript/flavours/glitch/features/video/index.jsx b/app/javascript/flavours/glitch/features/video/index.jsx index fb791fa9f..8375ffe94 100644 --- a/app/javascript/flavours/glitch/features/video/index.jsx +++ b/app/javascript/flavours/glitch/features/video/index.jsx @@ -373,7 +373,7 @@ class Video extends React.PureComponent { } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { this.setState({ revealed: nextProps.visible }); } @@ -476,7 +476,7 @@ class Video extends React.PureComponent { handleOpenVideo = () => { this.video.pause(); - this.props.onOpenVideo({ + this.props.onOpenVideo(this.props.lang, { startTime: this.video.currentTime, autoPlay: !this.state.paused, defaultVolume: this.state.volume, diff --git a/app/javascript/flavours/glitch/is_mobile.ts b/app/javascript/flavours/glitch/is_mobile.ts index c91ba4d53..d69beb240 100644 --- a/app/javascript/flavours/glitch/is_mobile.ts +++ b/app/javascript/flavours/glitch/is_mobile.ts @@ -1,4 +1,5 @@ import { supportsPassiveEvents } from 'detect-passive-events'; + import { forceSingleColumn } from 'flavours/glitch/initial_state'; const LAYOUT_BREAKPOINT = 630; diff --git a/app/javascript/flavours/glitch/main.jsx b/app/javascript/flavours/glitch/main.jsx index 1ac7d579e..63195518b 100644 --- a/app/javascript/flavours/glitch/main.jsx +++ b/app/javascript/flavours/glitch/main.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications'; import Mastodon from 'flavours/glitch/containers/mastodon'; import { store } from 'flavours/glitch/store'; @@ -18,7 +18,8 @@ function main() { const mountNode = document.getElementById('mastodon'); const props = JSON.parse(mountNode.getAttribute('data-props')); - ReactDOM.render(, mountNode); + const root = createRoot(mountNode); + root.render(); store.dispatch(setupBrowserNotifications()); if (process.env.NODE_ENV === 'production' && me && 'serviceWorker' in navigator) { diff --git a/app/javascript/flavours/glitch/packs/admin.jsx b/app/javascript/flavours/glitch/packs/admin.jsx index 7544ae4e9..7b0917df8 100644 --- a/app/javascript/flavours/glitch/packs/admin.jsx +++ b/app/javascript/flavours/glitch/packs/admin.jsx @@ -1,7 +1,7 @@ import 'packs/public-path'; import ready from 'flavours/glitch/ready'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; ready(() => { [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => { @@ -10,11 +10,13 @@ ready(() => { import('flavours/glitch/containers/admin_component').then(({ default: AdminComponent }) => { return import('flavours/glitch/components/admin/' + componentName).then(({ default: Component }) => { - ReactDOM.render(( + const root = createRoot(element); + + root.render ( - - ), element); + , + ); }); }).catch(error => { console.error(error); diff --git a/app/javascript/flavours/glitch/packs/common.js b/app/javascript/flavours/glitch/packs/common.js index 7dc34eba9..a750c3668 100644 --- a/app/javascript/flavours/glitch/packs/common.js +++ b/app/javascript/flavours/glitch/packs/common.js @@ -1,9 +1,9 @@ import 'packs/public-path'; import { start } from '@rails/ujs'; -start(); - import 'flavours/glitch/styles/index.scss'; +start(); + // This ensures that webpack compiles our images. require.context('../images', true); diff --git a/app/javascript/flavours/glitch/packs/public.jsx b/app/javascript/flavours/glitch/packs/public.jsx index 34a8f3513..7bb85fd2f 100644 --- a/app/javascript/flavours/glitch/packs/public.jsx +++ b/app/javascript/flavours/glitch/packs/public.jsx @@ -11,7 +11,7 @@ import { delegate } from '@rails/ujs'; import emojify from 'flavours/glitch/features/emoji/emoji'; import { getLocale } from 'locales'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { createBrowserHistory } from 'history'; const messages = defineMessages({ @@ -130,7 +130,8 @@ function main() { const content = document.createElement('div'); - ReactDOM.render(, content); + const root = createRoot(content); + root.render(); document.body.appendChild(content); scrollToDetailedStatus(); }) diff --git a/app/javascript/flavours/glitch/packs/share.jsx b/app/javascript/flavours/glitch/packs/share.jsx index 47a51a5b0..a835058ac 100644 --- a/app/javascript/flavours/glitch/packs/share.jsx +++ b/app/javascript/flavours/glitch/packs/share.jsx @@ -1,9 +1,9 @@ import 'packs/public-path'; import { loadPolyfills } from 'flavours/glitch/polyfills'; +import ready from 'flavours/glitch/ready'; import ComposeContainer from 'flavours/glitch/containers/compose_container'; import React from 'react'; -import ReactDOM from 'react-dom'; -import ready from 'flavours/glitch/ready'; +import { createRoot } from 'react-dom/client'; function loaded() { const mountNode = document.getElementById('mastodon-compose'); @@ -13,7 +13,8 @@ function loaded() { if(!attr) return; const props = JSON.parse(attr); - ReactDOM.render(, mountNode); + const root = createRoot(mountNode); + root.render(); } } diff --git a/app/javascript/flavours/glitch/packs/sign_up.js b/app/javascript/flavours/glitch/packs/sign_up.js new file mode 100644 index 000000000..ed03f1cf4 --- /dev/null +++ b/app/javascript/flavours/glitch/packs/sign_up.js @@ -0,0 +1,15 @@ +import 'packs/public-path'; +import ready from 'flavours/glitch/ready'; +import axios from 'axios'; + +ready(() => { + setInterval(() => { + axios.get('/api/v1/emails/check_confirmation').then((response) => { + if (response.data) { + window.location = '/start'; + } + }).catch(error => { + console.error(error); + }); + }, 5000); +}); diff --git a/app/javascript/flavours/glitch/polyfills/base_polyfills.ts b/app/javascript/flavours/glitch/polyfills/base_polyfills.ts index 64211c11e..e008d8f02 100644 --- a/app/javascript/flavours/glitch/polyfills/base_polyfills.ts +++ b/app/javascript/flavours/glitch/polyfills/base_polyfills.ts @@ -10,8 +10,13 @@ if (!HTMLCanvasElement.prototype.toBlob) { const BASE64_MARKER = ';base64,'; Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { - value(callback: BlobCallback, type = 'image/png', quality: any) { - const dataURL = this.toDataURL(type, quality); + value: function ( + this: HTMLCanvasElement, + callback: BlobCallback, + type = 'image/png', + quality: unknown + ) { + const dataURL: string = this.toDataURL(type, quality); let data; if (dataURL.indexOf(BASE64_MARKER) >= 0) { diff --git a/app/javascript/flavours/glitch/reducers/index.ts b/app/javascript/flavours/glitch/reducers/index.ts index da2174fc2..eaef569f0 100644 --- a/app/javascript/flavours/glitch/reducers/index.ts +++ b/app/javascript/flavours/glitch/reducers/index.ts @@ -1,48 +1,49 @@ -import { combineReducers } from 'redux-immutable'; -import dropdown_menu from './dropdown_menu'; -import timelines from './timelines'; -import meta from './meta'; -import alerts from './alerts'; import { loadingBarReducer } from 'react-redux-loading-bar'; -import modal from './modal'; -import user_lists from './user_lists'; -import domain_lists from './domain_lists'; +import { combineReducers } from 'redux-immutable'; + +import account_notes from './account_notes'; import accounts from './accounts'; import accounts_counters from './accounts_counters'; -import statuses from './statuses'; -import relationships from './relationships'; -import settings from './settings'; -import local_settings from './local_settings'; -import push_notifications from './push_notifications'; -import status_lists from './status_lists'; -import mutes from './mutes'; +import accounts_map from './accounts_map'; +import alerts from './alerts'; +import announcements from './announcements'; import blocks from './blocks'; -import server from './server'; import boosts from './boosts'; -import contexts from './contexts'; import compose from './compose'; -import search from './search'; -import media_attachments from './media_attachments'; -import notifications from './notifications'; -import height_cache from './height_cache'; -import custom_emojis from './custom_emojis'; -import lists from './lists'; -import listEditor from './list_editor'; -import listAdder from './list_adder'; -import filters from './filters'; +import contexts from './contexts'; import conversations from './conversations'; -import suggestions from './suggestions'; +import custom_emojis from './custom_emojis'; +import domain_lists from './domain_lists'; +import dropdown_menu from './dropdown_menu'; +import filters from './filters'; +import followed_tags from './followed_tags'; +import height_cache from './height_cache'; +import history from './history'; +import listAdder from './list_adder'; +import listEditor from './list_editor'; +import lists from './lists'; +import local_settings from './local_settings'; +import markers from './markers'; +import media_attachments from './media_attachments'; +import meta from './meta'; +import modal from './modal'; +import mutes from './mutes'; +import notifications from './notifications'; +import picture_in_picture from './picture_in_picture'; import pinnedAccountsEditor from './pinned_accounts_editor'; import polls from './polls'; -import trends from './trends'; -import announcements from './announcements'; -import markers from './markers'; -import account_notes from './account_notes'; -import picture_in_picture from './picture_in_picture'; -import accounts_map from './accounts_map'; -import history from './history'; +import push_notifications from './push_notifications'; +import relationships from './relationships'; +import search from './search'; +import server from './server'; +import settings from './settings'; +import status_lists from './status_lists'; +import statuses from './statuses'; +import suggestions from './suggestions'; import tags from './tags'; -import followed_tags from './followed_tags'; +import timelines from './timelines'; +import trends from './trends'; +import user_lists from './user_lists'; const reducers = { announcements, diff --git a/app/javascript/flavours/glitch/reducers/markers.js b/app/javascript/flavours/glitch/reducers/markers.js index e3d1b1936..3e8b1780a 100644 --- a/app/javascript/flavours/glitch/reducers/markers.js +++ b/app/javascript/flavours/glitch/reducers/markers.js @@ -2,13 +2,13 @@ import { MARKERS_SUBMIT_SUCCESS, } from '../actions/markers'; +import { Map as ImmutableMap } from 'immutable'; + const initialState = ImmutableMap({ home: '0', notifications: '0', }); -import { Map as ImmutableMap } from 'immutable'; - export default function markers(state = initialState, action) { switch(action.type) { case MARKERS_SUBMIT_SUCCESS: diff --git a/app/javascript/flavours/glitch/store/index.ts b/app/javascript/flavours/glitch/store/index.ts index 6c3e963d9..1ef9cb818 100644 --- a/app/javascript/flavours/glitch/store/index.ts +++ b/app/javascript/flavours/glitch/store/index.ts @@ -1,14 +1,32 @@ +import type { TypedUseSelectorHook } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; + import { configureStore } from '@reduxjs/toolkit'; + import { rootReducer } from '../reducers'; -import { loadingBarMiddleware } from './middlewares/loading_bar'; + import { errorsMiddleware } from './middlewares/errors'; +import { loadingBarMiddleware } from './middlewares/loading_bar'; import { soundsMiddleware } from './middlewares/sounds'; -import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; export const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => - getDefaultMiddleware() + getDefaultMiddleware({ + // In development, Redux Toolkit enables 2 default middlewares to detect + // common issues with states. Unfortunately, our use of ImmutableJS for state + // triggers both, so lets disable them until our state is fully refactored + + // https://redux-toolkit.js.org/api/serializabilityMiddleware + // This checks recursively that every values in the state are serializable in JSON + // Which is not the case, as we use ImmutableJS structures, but also File objects + serializableCheck: false, + + // https://redux-toolkit.js.org/api/immutabilityMiddleware + // This checks recursively if every value in the state is immutable (ie, a JS primitive type) + // But this is not the case, as our Root State is an ImmutableJS map, which is an object + immutableCheck: false, + }) .concat( loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'], diff --git a/app/javascript/flavours/glitch/store/middlewares/errors.ts b/app/javascript/flavours/glitch/store/middlewares/errors.ts index 5dec423af..2697f80f1 100644 --- a/app/javascript/flavours/glitch/store/middlewares/errors.ts +++ b/app/javascript/flavours/glitch/store/middlewares/errors.ts @@ -1,17 +1,19 @@ -import { Middleware } from 'redux'; +import type { AnyAction, Middleware } from 'redux'; + import { showAlertForError } from 'flavours/glitch/actions/alerts'; -import { RootState } from '..'; + +import type { RootState } from '..'; const defaultFailSuffix = 'FAIL'; export const errorsMiddleware: Middleware, RootState> = ({ dispatch }) => (next) => - (action) => { + (action: AnyAction & { skipAlert?: boolean; skipNotFound?: boolean }) => { if (action.type && !action.skipAlert) { const isFail = new RegExp(`${defaultFailSuffix}$`, 'g'); - if (action.type.match(isFail)) { + if (typeof action.type === 'string' && action.type.match(isFail)) { dispatch(showAlertForError(action.error, action.skipNotFound)); } } diff --git a/app/javascript/flavours/glitch/store/middlewares/loading_bar.ts b/app/javascript/flavours/glitch/store/middlewares/loading_bar.ts index 183c0cf9d..0f997fd34 100644 --- a/app/javascript/flavours/glitch/store/middlewares/loading_bar.ts +++ b/app/javascript/flavours/glitch/store/middlewares/loading_bar.ts @@ -1,6 +1,7 @@ import { showLoading, hideLoading } from 'react-redux-loading-bar'; -import { Middleware } from 'redux'; -import { RootState } from '..'; +import type { AnyAction, Middleware } from 'redux'; + +import type { RootState } from '..'; interface Config { promiseTypeSuffixes?: string[]; @@ -19,7 +20,7 @@ export const loadingBarMiddleware = ( return ({ dispatch }) => (next) => - (action) => { + (action: AnyAction) => { if (action.type && !action.skipLoading) { const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes; @@ -27,13 +28,15 @@ export const loadingBarMiddleware = ( const isFulfilled = new RegExp(`${FULFILLED}$`, 'g'); const isRejected = new RegExp(`${REJECTED}$`, 'g'); - if (action.type.match(isPending)) { - dispatch(showLoading()); - } else if ( - action.type.match(isFulfilled) || - action.type.match(isRejected) - ) { - dispatch(hideLoading()); + if (typeof action.type === 'string') { + if (action.type.match(isPending)) { + dispatch(showLoading()); + } else if ( + action.type.match(isFulfilled) || + action.type.match(isRejected) + ) { + dispatch(hideLoading()); + } } } diff --git a/app/javascript/flavours/glitch/store/middlewares/sounds.ts b/app/javascript/flavours/glitch/store/middlewares/sounds.ts index e7c87df7e..6005d3649 100644 --- a/app/javascript/flavours/glitch/store/middlewares/sounds.ts +++ b/app/javascript/flavours/glitch/store/middlewares/sounds.ts @@ -1,5 +1,6 @@ -import { Middleware, AnyAction } from 'redux'; -import { RootState } from '..'; +import type { Middleware, AnyAction } from 'redux'; + +import type { RootState } from '..'; interface AudioSource { src: string; @@ -27,7 +28,7 @@ const play = (audio: HTMLAudioElement) => { } } - audio.play(); + void audio.play(); }; export const soundsMiddleware = (): Middleware< @@ -47,13 +48,15 @@ export const soundsMiddleware = (): Middleware< ]), }; - return () => (next) => (action: AnyAction) => { - const sound = action?.meta?.sound; + return () => + (next) => + (action: AnyAction & { meta?: { sound?: string } }) => { + const sound = action?.meta?.sound; - if (sound && soundCache[sound]) { - play(soundCache[sound]); - } + if (sound && soundCache[sound]) { + play(soundCache[sound]); + } - return next(action); - }; + return next(action); + }; }; diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index a708d066a..92633fc3f 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -96,13 +96,6 @@ grid-column: span 2; } - &.standalone { - .media-gallery__item-gifv-thumbnail { - transform: none; - top: 0; - } - } - .full-width & { border-radius: 0; } @@ -161,8 +154,6 @@ cursor: zoom-in; height: 100%; width: 100%; - position: relative; - z-index: 1; object-fit: contain; user-select: none; @@ -455,6 +446,8 @@ border-radius: 4px; box-sizing: border-box; color: $white; + display: flex; + align-items: center; &.editable { border-radius: 0; @@ -497,9 +490,6 @@ &.inline { video { object-fit: contain; - position: relative; - top: 50%; - transform: translateY(-50%); } } diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml index 672dd5440..0877e4d40 100644 --- a/app/javascript/flavours/glitch/theme.yml +++ b/app/javascript/flavours/glitch/theme.yml @@ -20,6 +20,7 @@ pack: modal: public: packs/public.jsx settings: packs/settings.js + sign_up: packs/sign_up.js share: packs/share.jsx # (OPTIONAL) The directory which contains localization files for diff --git a/app/javascript/flavours/glitch/types/resources.ts b/app/javascript/flavours/glitch/types/resources.ts index 090650415..63ec2993b 100644 --- a/app/javascript/flavours/glitch/types/resources.ts +++ b/app/javascript/flavours/glitch/types/resources.ts @@ -12,7 +12,7 @@ type AccountField = Record<{ verified_at: string | null; }>; -type AccountApiResponseValues = { +interface AccountApiResponseValues { acct: string; avatar: string; avatar_static: string; @@ -34,7 +34,7 @@ type AccountApiResponseValues = { statuses_count: number; url: string; username: string; -}; +} type NormalizedAccountField = Record<{ name_emojified: string; @@ -42,12 +42,12 @@ type NormalizedAccountField = Record<{ value_plain: string; }>; -type NormalizedAccountValues = { +interface NormalizedAccountValues { display_name_html: string; fields: NormalizedAccountField[]; note_emojified: string; note_plain: string; -}; +} export type Account = Record< AccountApiResponseValues & NormalizedAccountValues diff --git a/app/javascript/flavours/glitch/utils/dom_helpers.js b/app/javascript/flavours/glitch/utils/dom_helpers.js index d94aeb9d4..bc5e200fc 100644 --- a/app/javascript/flavours/glitch/utils/dom_helpers.js +++ b/app/javascript/flavours/glitch/utils/dom_helpers.js @@ -1,10 +1,3 @@ -// Package imports. -import { supportsPassiveEvents } from 'detect-passive-events'; - -// This will either be a passive lister options object (if passive -// events are supported), or `false`. -export const withPassive = supportsPassiveEvents ? { passive: true } : false; - // Focuses the root element. export function focusRoot () { let e; diff --git a/app/javascript/flavours/glitch/utils/resize_image.js b/app/javascript/flavours/glitch/utils/resize_image.js index fb8c3c11e..e3d4e6a35 100644 --- a/app/javascript/flavours/glitch/utils/resize_image.js +++ b/app/javascript/flavours/glitch/utils/resize_image.js @@ -170,7 +170,7 @@ const resizeImage = (img, type = 'image/png') => new Promise((resolve, reject) = .catch(reject); }); -export default inputFile => new Promise((resolve) => { +const resizeFile = (inputFile) => new Promise((resolve) => { if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') { resolve(inputFile); return; @@ -187,3 +187,5 @@ export default inputFile => new Promise((resolve) => { .catch(() => resolve(inputFile)); }).catch(() => resolve(inputFile)); }); + +export default resizeFile; diff --git a/app/javascript/flavours/glitch/uuid.ts b/app/javascript/flavours/glitch/uuid.ts index 6cadbd6bb..0b4d55beb 100644 --- a/app/javascript/flavours/glitch/uuid.ts +++ b/app/javascript/flavours/glitch/uuid.ts @@ -1,8 +1,9 @@ export function uuid(a?: string): string { return a ? ( - (a as any as number) ^ - ((Math.random() * 16) >> ((a as any as number) / 4)) + (a as unknown as number) ^ + ((Math.random() * 16) >> ((a as unknown as number) / 4)) ).toString(16) - : ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid); + : // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid); } diff --git a/app/javascript/flavours/vanilla/theme.yml b/app/javascript/flavours/vanilla/theme.yml index ccab925aa..470ded530 100644 --- a/app/javascript/flavours/vanilla/theme.yml +++ b/app/javascript/flavours/vanilla/theme.yml @@ -20,6 +20,7 @@ pack: modal: public: public.jsx settings: public.jsx + sign_up: sign_up.js share: share.jsx # (OPTIONAL) The directory which contains localization files for diff --git a/app/javascript/mastodon/actions/app.ts b/app/javascript/mastodon/actions/app.ts index 50fd317a6..be1a5cced 100644 --- a/app/javascript/mastodon/actions/app.ts +++ b/app/javascript/mastodon/actions/app.ts @@ -1,11 +1,12 @@ import { createAction } from '@reduxjs/toolkit'; + import type { LayoutType } from '../is_mobile'; export const focusApp = createAction('APP_FOCUS'); export const unfocusApp = createAction('APP_UNFOCUS'); -type ChangeLayoutPayload = { +interface ChangeLayoutPayload { layout: LayoutType; -}; +} export const changeLayout = createAction('APP_LAYOUT_CHANGE'); diff --git a/app/javascript/mastodon/actions/pin_statuses.js b/app/javascript/mastodon/actions/pin_statuses.js index e2de98ca9..1e4bd37bf 100644 --- a/app/javascript/mastodon/actions/pin_statuses.js +++ b/app/javascript/mastodon/actions/pin_statuses.js @@ -1,12 +1,12 @@ import api from '../api'; import { importFetchedStatuses } from './importer'; +import { me } from '../initial_state'; + export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; -import { me } from '../initial_state'; - export function fetchPinnedStatuses() { return (dispatch, getState) => { dispatch(fetchPinnedStatusesRequest()); diff --git a/app/javascript/mastodon/components/__tests__/display_name-test.jsx b/app/javascript/mastodon/components/__tests__/display_name-test.jsx index 0d040c4cd..afb6c4758 100644 --- a/app/javascript/mastodon/components/__tests__/display_name-test.jsx +++ b/app/javascript/mastodon/components/__tests__/display_name-test.jsx @@ -1,7 +1,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { fromJS } from 'immutable'; -import DisplayName from '../display_name'; +import { DisplayName } from '../display_name'; describe('', () => { it('renders display name + account name', () => { diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index b0bbea7ce..08dfb4793 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -2,18 +2,18 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { Avatar } from './avatar'; -import DisplayName from './display_name'; +import { DisplayName } from './display_name'; import { IconButton } from './icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { me } from '../initial_state'; import { RelativeTimestamp } from './relative_timestamp'; -import Skeleton from 'mastodon/components/skeleton'; import { Link } from 'react-router-dom'; import { counterRenderer } from 'mastodon/components/common_counter'; import ShortNumber from 'mastodon/components/short_number'; import classNames from 'classnames'; import { VerifiedBadge } from 'mastodon/components/verified_badge'; +import { EmptyAccount } from 'mastodon/components/empty_account'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, @@ -77,20 +77,7 @@ class Account extends ImmutablePureComponent { const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props; if (!account) { - return ( -
-
-
-
- -
- - -
-
-
-
- ); + return ; } if (hidden) { diff --git a/app/javascript/mastodon/components/admin/Counter.jsx b/app/javascript/mastodon/components/admin/Counter.jsx index 5a5b2b869..569f8628a 100644 --- a/app/javascript/mastodon/components/admin/Counter.jsx +++ b/app/javascript/mastodon/components/admin/Counter.jsx @@ -4,7 +4,7 @@ import api from 'mastodon/api'; import { FormattedNumber } from 'react-intl'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; import classNames from 'classnames'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; const percIncrease = (a, b) => { let percent; diff --git a/app/javascript/mastodon/components/admin/Dimension.jsx b/app/javascript/mastodon/components/admin/Dimension.jsx index 977c8208d..3005c15ae 100644 --- a/app/javascript/mastodon/components/admin/Dimension.jsx +++ b/app/javascript/mastodon/components/admin/Dimension.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import api from 'mastodon/api'; import { FormattedNumber } from 'react-intl'; import { roundTo10 } from 'mastodon/utils/numbers'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; export default class Dimension extends React.PureComponent { diff --git a/app/javascript/mastodon/components/animated_number.tsx b/app/javascript/mastodon/components/animated_number.tsx index f6c77d35f..b6b073161 100644 --- a/app/javascript/mastodon/components/animated_number.tsx +++ b/app/javascript/mastodon/components/animated_number.tsx @@ -1,8 +1,11 @@ import React, { useCallback, useState } from 'react'; -import ShortNumber from './short_number'; + import { TransitionMotion, spring } from 'react-motion'; + import { reduceMotion } from '../initial_state'; +import ShortNumber from './short_number'; + const obfuscatedCount = (count: number) => { if (count < 0) { return 0; @@ -13,10 +16,10 @@ const obfuscatedCount = (count: number) => { } }; -type Props = { +interface Props { value: number; obfuscate?: boolean; -}; +} export const AnimatedNumber: React.FC = ({ value, obfuscate }) => { const [previousValue, setPreviousValue] = useState(value); const [direction, setDirection] = useState<1 | -1>(1); @@ -64,7 +67,11 @@ export const AnimatedNumber: React.FC = ({ value, obfuscate }) => { transform: `translateY(${style.y * 100}%)`, }} > - {obfuscate ? obfuscatedCount(data) : } + {obfuscate ? ( + obfuscatedCount(data as number) + ) : ( + + )} ))} diff --git a/app/javascript/mastodon/components/autosuggest_input.jsx b/app/javascript/mastodon/components/autosuggest_input.jsx index a68e2a01b..218faabb7 100644 --- a/app/javascript/mastodon/components/autosuggest_input.jsx +++ b/app/javascript/mastodon/components/autosuggest_input.jsx @@ -154,7 +154,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { this.input.focus(); }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { this.setState({ suggestionsHidden: false }); } diff --git a/app/javascript/mastodon/components/autosuggest_textarea.jsx b/app/javascript/mastodon/components/autosuggest_textarea.jsx index a627bc1ec..50cc24b00 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.jsx +++ b/app/javascript/mastodon/components/autosuggest_textarea.jsx @@ -153,7 +153,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { this.textarea.focus(); }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { this.setState({ suggestionsHidden: false }); } diff --git a/app/javascript/mastodon/components/avatar.tsx b/app/javascript/mastodon/components/avatar.tsx index 8be94d3f5..2b46b05d6 100644 --- a/app/javascript/mastodon/components/avatar.tsx +++ b/app/javascript/mastodon/components/avatar.tsx @@ -1,16 +1,18 @@ import * as React from 'react'; + import classNames from 'classnames'; -import { autoPlayGif } from '../initial_state'; + import { useHovering } from '../../hooks/useHovering'; import type { Account } from '../../types/resources'; +import { autoPlayGif } from '../initial_state'; -type Props = { +interface Props { account: Account; size: number; style?: React.CSSProperties; inline?: boolean; animate?: boolean; -}; +} export const Avatar: React.FC = ({ account, diff --git a/app/javascript/mastodon/components/avatar_overlay.tsx b/app/javascript/mastodon/components/avatar_overlay.tsx index 1dbd53323..d1d158126 100644 --- a/app/javascript/mastodon/components/avatar_overlay.tsx +++ b/app/javascript/mastodon/components/avatar_overlay.tsx @@ -1,15 +1,16 @@ import React from 'react'; -import type { Account } from '../../types/resources'; + import { useHovering } from '../../hooks/useHovering'; +import type { Account } from '../../types/resources'; import { autoPlayGif } from '../initial_state'; -type Props = { +interface Props { account: Account; friend: Account; size?: number; baseSize?: number; overlaySize?: number; -}; +} export const AvatarOverlay: React.FC = ({ account, diff --git a/app/javascript/mastodon/components/blurhash.tsx b/app/javascript/mastodon/components/blurhash.tsx index 700513676..1550d0b7a 100644 --- a/app/javascript/mastodon/components/blurhash.tsx +++ b/app/javascript/mastodon/components/blurhash.tsx @@ -1,14 +1,14 @@ -import { decode } from 'blurhash'; import React, { useRef, useEffect } from 'react'; -type Props = { +import { decode } from 'blurhash'; + +interface Props extends React.HTMLAttributes { hash: string; width?: number; height?: number; dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched children?: never; - [key: string]: any; -}; +} const Blurhash: React.FC = ({ hash, width = 32, @@ -21,6 +21,7 @@ const Blurhash: React.FC = ({ useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const canvas = canvasRef.current!; + // eslint-disable-next-line no-self-assign canvas.width = canvas.width; // resets canvas diff --git a/app/javascript/mastodon/components/column.jsx b/app/javascript/mastodon/components/column.jsx index 5780a1397..c9ea5f7ac 100644 --- a/app/javascript/mastodon/components/column.jsx +++ b/app/javascript/mastodon/components/column.jsx @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import { supportsPassiveEvents } from 'detect-passive-events'; import { scrollTop } from '../scroll'; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + export default class Column extends React.PureComponent { static propTypes = { @@ -35,17 +37,17 @@ export default class Column extends React.PureComponent { componentDidMount () { if (this.props.bindToDocument) { - document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); + document.addEventListener('wheel', this.handleWheel, listenerOptions); } else { - this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); + this.node.addEventListener('wheel', this.handleWheel, listenerOptions); } } componentWillUnmount () { if (this.props.bindToDocument) { - document.removeEventListener('wheel', this.handleWheel); + document.removeEventListener('wheel', this.handleWheel, listenerOptions); } else { - this.node.removeEventListener('wheel', this.handleWheel); + this.node.removeEventListener('wheel', this.handleWheel, listenerOptions); } } diff --git a/app/javascript/mastodon/components/display_name.jsx b/app/javascript/mastodon/components/display_name.jsx deleted file mode 100644 index 1dd9fb1d6..000000000 --- a/app/javascript/mastodon/components/display_name.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { autoPlayGif } from 'mastodon/initial_state'; -import Skeleton from 'mastodon/components/skeleton'; - -export default class DisplayName extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map, - others: ImmutablePropTypes.list, - localDomain: PropTypes.string, - }; - - handleMouseEnter = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-original'); - } - }; - - handleMouseLeave = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-static'); - } - }; - - render () { - const { others, localDomain } = this.props; - - let displayName, suffix, account; - - if (others && others.size > 1) { - displayName = others.take(2).map(a => ).reduce((prev, cur) => [prev, ', ', cur]); - - if (others.size - 2 > 0) { - suffix = `+${others.size - 2}`; - } - } else if ((others && others.size > 0) || this.props.account) { - if (others && others.size > 0) { - account = others.first(); - } else { - account = this.props.account; - } - - let acct = account.get('acct'); - - if (acct.indexOf('@') === -1 && localDomain) { - acct = `${acct}@${localDomain}`; - } - - displayName = ; - suffix = @{acct}; - } else { - displayName = ; - suffix = ; - } - - return ( - - {displayName} {suffix} - - ); - } - -} diff --git a/app/javascript/mastodon/components/display_name.tsx b/app/javascript/mastodon/components/display_name.tsx new file mode 100644 index 000000000..c537cd24c --- /dev/null +++ b/app/javascript/mastodon/components/display_name.tsx @@ -0,0 +1,121 @@ +import React from 'react'; + +import type { List } from 'immutable'; + +import type { Account } from '../../types/resources'; +import { autoPlayGif } from '../initial_state'; + +import { Skeleton } from './skeleton'; + +interface Props { + account?: Account; + others?: List; + localDomain?: string; +} + +export class DisplayName extends React.PureComponent { + handleMouseEnter: React.ReactEventHandler = ({ + currentTarget, + }) => { + if (autoPlayGif) { + return; + } + + const emojis = + currentTarget.querySelectorAll('img.custom-emoji'); + + emojis.forEach((emoji) => { + const originalSrc = emoji.getAttribute('data-original'); + if (originalSrc != null) emoji.src = originalSrc; + }); + }; + + handleMouseLeave: React.ReactEventHandler = ({ + currentTarget, + }) => { + if (autoPlayGif) { + return; + } + + const emojis = + currentTarget.querySelectorAll('img.custom-emoji'); + + emojis.forEach((emoji) => { + const staticSrc = emoji.getAttribute('data-static'); + if (staticSrc != null) emoji.src = staticSrc; + }); + }; + + render() { + const { others, localDomain } = this.props; + + let displayName: React.ReactNode, + suffix: React.ReactNode, + account: Account | undefined; + + if (others && others.size > 0) { + account = others.first(); + } else if (this.props.account) { + account = this.props.account; + } + + if (others && others.size > 1) { + displayName = others + .take(2) + .map((a) => ( + + + + )) + .reduce((prev, cur) => [prev, ', ', cur]); + + if (others.size - 2 > 0) { + suffix = `+${others.size - 2}`; + } + } else if (account) { + let acct = account.get('acct'); + + if (acct.indexOf('@') === -1 && localDomain) { + acct = `${acct}@${localDomain}`; + } + + displayName = ( + + + + ); + suffix = @{acct}; + } else { + displayName = ( + + + + + + ); + suffix = ( + + + + ); + } + + return ( + + {displayName} {suffix} + + ); + } +} diff --git a/app/javascript/mastodon/components/domain.tsx b/app/javascript/mastodon/components/domain.tsx index af0fec35a..9e8e04b65 100644 --- a/app/javascript/mastodon/components/domain.tsx +++ b/app/javascript/mastodon/components/domain.tsx @@ -1,6 +1,9 @@ import React, { useCallback } from 'react'; + +import type { InjectedIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; + import { IconButton } from './icon_button'; -import { InjectedIntl, defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ unblockDomain: { @@ -9,11 +12,11 @@ const messages = defineMessages({ }, }); -type Props = { +interface Props { domain: string; onUnblockDomain: (domain: string) => void; intl: InjectedIntl; -}; +} const _Domain: React.FC = ({ domain, onUnblockDomain, intl }) => { const handleDomainUnblock = useCallback(() => { onUnblockDomain(domain); diff --git a/app/javascript/mastodon/components/dropdown_menu.jsx b/app/javascript/mastodon/components/dropdown_menu.jsx index 0ed3e904f..0336585f1 100644 --- a/app/javascript/mastodon/components/dropdown_menu.jsx +++ b/app/javascript/mastodon/components/dropdown_menu.jsx @@ -7,7 +7,7 @@ import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; import { CircularProgress } from 'mastodon/components/loading_indicator'; -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; let id = 0; class DropdownMenu extends React.PureComponent { @@ -35,12 +35,13 @@ class DropdownMenu extends React.PureComponent { handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); + e.stopPropagation(); } }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('keydown', this.handleKeyDown, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); + document.addEventListener('keydown', this.handleKeyDown, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); if (this.focusedItem && this.props.openedViaKeyboard) { @@ -49,8 +50,8 @@ class DropdownMenu extends React.PureComponent { } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('keydown', this.handleKeyDown, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); + document.removeEventListener('keydown', this.handleKeyDown, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } diff --git a/app/javascript/mastodon/components/empty_account.tsx b/app/javascript/mastodon/components/empty_account.tsx new file mode 100644 index 000000000..a4a6b7f82 --- /dev/null +++ b/app/javascript/mastodon/components/empty_account.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import classNames from 'classnames'; + +import { DisplayName } from 'mastodon/components/display_name'; +import { Skeleton } from 'mastodon/components/skeleton'; + +interface Props { + size?: number; + minimal?: boolean; +} + +export const EmptyAccount: React.FC = ({ + size = 46, + minimal = false, +}) => { + return ( +
+
+
+
+ +
+ +
+ + +
+
+
+
+ ); +}; diff --git a/app/javascript/mastodon/components/gifv.tsx b/app/javascript/mastodon/components/gifv.tsx index 72914ba74..c606a2904 100644 --- a/app/javascript/mastodon/components/gifv.tsx +++ b/app/javascript/mastodon/components/gifv.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from 'react'; -type Props = { +interface Props { src: string; key: string; alt?: string; @@ -8,7 +8,7 @@ type Props = { width: number; height: number; onClick?: () => void; -}; +} export const GIFV: React.FC = ({ src, diff --git a/app/javascript/mastodon/components/hashtag.jsx b/app/javascript/mastodon/components/hashtag.jsx index d03b1a45a..3efd679a5 100644 --- a/app/javascript/mastodon/components/hashtag.jsx +++ b/app/javascript/mastodon/components/hashtag.jsx @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Link } from 'react-router-dom'; import ShortNumber from 'mastodon/components/short_number'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; import classNames from 'classnames'; class SilentErrorBoundary extends React.Component { diff --git a/app/javascript/mastodon/components/icon.tsx b/app/javascript/mastodon/components/icon.tsx index 4eb948dc7..6bd15da6a 100644 --- a/app/javascript/mastodon/components/icon.tsx +++ b/app/javascript/mastodon/components/icon.tsx @@ -1,13 +1,14 @@ import React from 'react'; + import classNames from 'classnames'; -type Props = { +interface Props extends React.HTMLAttributes { id: string; className?: string; fixedWidth?: boolean; children?: never; - [key: string]: any; -}; +} + export const Icon: React.FC = ({ id, className, diff --git a/app/javascript/mastodon/components/icon_button.tsx b/app/javascript/mastodon/components/icon_button.tsx index 178641400..c995ed0eb 100644 --- a/app/javascript/mastodon/components/icon_button.tsx +++ b/app/javascript/mastodon/components/icon_button.tsx @@ -1,9 +1,11 @@ import React from 'react'; -import classNames from 'classnames'; -import { Icon } from './icon'; -import { AnimatedNumber } from './animated_number'; -type Props = { +import classNames from 'classnames'; + +import { AnimatedNumber } from './animated_number'; +import { Icon } from './icon'; + +interface Props { className?: string; title: string; icon: string; @@ -25,11 +27,11 @@ type Props = { obfuscateCount?: boolean; href?: string; ariaHidden: boolean; -}; -type States = { +} +interface States { activate: boolean; deactivate: boolean; -}; +} export class IconButton extends React.PureComponent { static defaultProps = { size: 18, diff --git a/app/javascript/mastodon/components/icon_with_badge.tsx b/app/javascript/mastodon/components/icon_with_badge.tsx index bf86814c0..e427b7172 100644 --- a/app/javascript/mastodon/components/icon_with_badge.tsx +++ b/app/javascript/mastodon/components/icon_with_badge.tsx @@ -1,14 +1,15 @@ import React from 'react'; + import { Icon } from './icon'; const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num); -type Props = { +interface Props { id: string; count: number; issueBadge: boolean; className: string; -}; +} export const IconWithBadge: React.FC = ({ id, count, diff --git a/app/javascript/mastodon/components/logo.jsx b/app/javascript/mastodon/components/logo.tsx similarity index 75% rename from app/javascript/mastodon/components/logo.jsx rename to app/javascript/mastodon/components/logo.tsx index 60e8f40b2..6594ef1fd 100644 --- a/app/javascript/mastodon/components/logo.jsx +++ b/app/javascript/mastodon/components/logo.tsx @@ -1,15 +1,14 @@ import React from 'react'; + import logo from 'mastodon/../images/logo.svg'; -export const WordmarkLogo = () => ( +export const WordmarkLogo: React.FC = () => ( Mastodon ); -export const SymbolLogo = () => ( +export const SymbolLogo: React.FC = () => ( Mastodon ); - -export default WordmarkLogo; diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx index 54b414de2..6653f8632 100644 --- a/app/javascript/mastodon/components/media_gallery.jsx +++ b/app/javascript/mastodon/components/media_gallery.jsx @@ -231,7 +231,7 @@ class MediaGallery extends React.PureComponent { window.removeEventListener('resize', this.handleResize); } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) { this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' }); } else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { @@ -256,7 +256,7 @@ class MediaGallery extends React.PureComponent { }; handleClick = (index) => { - this.props.onOpenMedia(this.props.media, index); + this.props.onOpenMedia(this.props.media, index, this.props.lang); }; handleRef = c => { diff --git a/app/javascript/mastodon/components/modal_root.jsx b/app/javascript/mastodon/components/modal_root.jsx index c0525c221..7671d2725 100644 --- a/app/javascript/mastodon/components/modal_root.jsx +++ b/app/javascript/mastodon/components/modal_root.jsx @@ -57,7 +57,7 @@ export default class ModalRoot extends React.PureComponent { this.history = this.context.router ? this.context.router.history : createBrowserHistory(); } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!!nextProps.children && !this.props.children) { this.activeElement = document.activeElement; diff --git a/app/javascript/mastodon/components/not_signed_in_indicator.tsx b/app/javascript/mastodon/components/not_signed_in_indicator.tsx index 53945d6a7..ce94c5d87 100644 --- a/app/javascript/mastodon/components/not_signed_in_indicator.tsx +++ b/app/javascript/mastodon/components/not_signed_in_indicator.tsx @@ -1,4 +1,5 @@ import React from 'react'; + import { FormattedMessage } from 'react-intl'; export const NotSignedInIndicator: React.FC = () => ( @@ -6,7 +7,7 @@ export const NotSignedInIndicator: React.FC = () => (
diff --git a/app/javascript/mastodon/components/radio_button.tsx b/app/javascript/mastodon/components/radio_button.tsx index 829f47174..67acb09f4 100644 --- a/app/javascript/mastodon/components/radio_button.tsx +++ b/app/javascript/mastodon/components/radio_button.tsx @@ -1,13 +1,14 @@ import React from 'react'; + import classNames from 'classnames'; -type Props = { +interface Props { value: string; checked: boolean; name: string; onChange: (event: React.ChangeEvent) => void; label: React.ReactNode; -}; +} export const RadioButton: React.FC = ({ name, diff --git a/app/javascript/mastodon/components/relative_timestamp.tsx b/app/javascript/mastodon/components/relative_timestamp.tsx index 65d9d27cb..e0e0d4bb5 100644 --- a/app/javascript/mastodon/components/relative_timestamp.tsx +++ b/app/javascript/mastodon/components/relative_timestamp.tsx @@ -1,5 +1,7 @@ import React from 'react'; -import { injectIntl, defineMessages, InjectedIntl } from 'react-intl'; + +import type { InjectedIntl } from 'react-intl'; +import { injectIntl, defineMessages } from 'react-intl'; const messages = defineMessages({ today: { id: 'relative_time.today', defaultMessage: 'today' }, @@ -187,16 +189,16 @@ const timeRemainingString = ( return relativeTime; }; -type Props = { +interface Props { intl: InjectedIntl; timestamp: string; year: number; futureDate?: boolean; short?: boolean; -}; -type States = { +} +interface States { now: number; -}; +} class RelativeTimestamp extends React.Component { state = { now: this.props.intl.now(), diff --git a/app/javascript/mastodon/components/scrollable_list.jsx b/app/javascript/mastodon/components/scrollable_list.jsx index 3f4e4a59c..9b9e1e744 100644 --- a/app/javascript/mastodon/components/scrollable_list.jsx +++ b/app/javascript/mastodon/components/scrollable_list.jsx @@ -15,6 +15,8 @@ import { connect } from 'react-redux'; const MOUSE_IDLE_DELAY = 300; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + const mapStateToProps = (state, { scrollKey }) => { return { preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']), @@ -237,20 +239,20 @@ class ScrollableList extends PureComponent { attachScrollListener () { if (this.props.bindToDocument) { document.addEventListener('scroll', this.handleScroll); - document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : undefined); + document.addEventListener('wheel', this.handleWheel, listenerOptions); } else { this.node.addEventListener('scroll', this.handleScroll); - this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : undefined); + this.node.addEventListener('wheel', this.handleWheel, listenerOptions); } } detachScrollListener () { if (this.props.bindToDocument) { document.removeEventListener('scroll', this.handleScroll); - document.removeEventListener('wheel', this.handleWheel); + document.removeEventListener('wheel', this.handleWheel, listenerOptions); } else { this.node.removeEventListener('scroll', this.handleScroll); - this.node.removeEventListener('wheel', this.handleWheel); + this.node.removeEventListener('wheel', this.handleWheel, listenerOptions); } } diff --git a/app/javascript/mastodon/components/server_banner.jsx b/app/javascript/mastodon/components/server_banner.jsx index 991907a3f..6c3abd5b6 100644 --- a/app/javascript/mastodon/components/server_banner.jsx +++ b/app/javascript/mastodon/components/server_banner.jsx @@ -4,10 +4,10 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { fetchServer } from 'mastodon/actions/server'; import ShortNumber from 'mastodon/components/short_number'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; import Account from 'mastodon/containers/account_container'; import { domain } from 'mastodon/initial_state'; -import { Image } from 'mastodon/components/image'; +import { ServerHeroImage } from 'mastodon/components/server_hero_image'; import { Link } from 'react-router-dom'; const messages = defineMessages({ @@ -41,7 +41,7 @@ class ServerBanner extends React.PureComponent { {domain}, mastodon: Mastodon }} />
- +
{isLoading ? ( diff --git a/app/javascript/flavours/glitch/components/image.tsx b/app/javascript/mastodon/components/server_hero_image.tsx similarity index 90% rename from app/javascript/flavours/glitch/components/image.tsx rename to app/javascript/mastodon/components/server_hero_image.tsx index 490543424..973d1d1b3 100644 --- a/app/javascript/flavours/glitch/components/image.tsx +++ b/app/javascript/mastodon/components/server_hero_image.tsx @@ -1,15 +1,17 @@ import React, { useCallback, useState } from 'react'; -import { Blurhash } from './blurhash'; + import classNames from 'classnames'; -type Props = { +import { Blurhash } from './blurhash'; + +interface Props { src: string; srcSet?: string; blurhash?: string; className?: string; -}; +} -export const Image: React.FC = ({ +export const ServerHeroImage: React.FC = ({ src, srcSet, blurhash, diff --git a/app/javascript/mastodon/components/skeleton.jsx b/app/javascript/mastodon/components/skeleton.jsx deleted file mode 100644 index 6a17ffb26..000000000 --- a/app/javascript/mastodon/components/skeleton.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const Skeleton = ({ width, height }) => ; - -Skeleton.propTypes = { - width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), -}; - -export default Skeleton; diff --git a/app/javascript/mastodon/components/skeleton.tsx b/app/javascript/mastodon/components/skeleton.tsx new file mode 100644 index 000000000..8d43e6827 --- /dev/null +++ b/app/javascript/mastodon/components/skeleton.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +interface Props { + width?: number | string; + height?: number | string; +} + +export const Skeleton: React.FC = ({ width, height }) => ( + + ‌ + +); diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 3ca840b72..070ec4672 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import { Avatar } from './avatar'; import { AvatarOverlay } from './avatar_overlay'; import { RelativeTimestamp } from './relative_timestamp'; -import DisplayName from './display_name'; +import { DisplayName } from './display_name'; import StatusContent from './status_content'; import StatusActionBar from './status_action_bar'; import AttachmentList from './attachment_list'; @@ -194,11 +194,12 @@ class Status extends ImmutablePureComponent { handleOpenVideo = (options) => { const status = this._properStatus(); - this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options); + this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), status.get('language'), options); }; handleOpenMedia = (media, index) => { - this.props.onOpenMedia(this._properStatus().get('id'), media, index); + const status = this._properStatus(); + this.props.onOpenMedia(status.get('id'), media, index, status.get('language')); }; handleHotkeyOpenMedia = e => { @@ -208,10 +209,11 @@ class Status extends ImmutablePureComponent { e.preventDefault(); if (status.get('media_attachments').size > 0) { + const lang = status.get('language'); if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), { startTime: 0 }); + onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, { startTime: 0 }); } else { - onOpenMedia(status.get('id'), status.get('media_attachments'), 0); + onOpenMedia(status.get('id'), status.get('media_attachments'), 0, lang); } } }; diff --git a/app/javascript/mastodon/components/status_list.jsx b/app/javascript/mastodon/components/status_list.jsx index 3d513bbf8..34b773278 100644 --- a/app/javascript/mastodon/components/status_list.jsx +++ b/app/javascript/mastodon/components/status_list.jsx @@ -26,6 +26,7 @@ export default class StatusList extends ImmutablePureComponent { alwaysPrepend: PropTypes.bool, withCounters: PropTypes.bool, timelineId: PropTypes.string, + lastId: PropTypes.string, }; static defaultProps = { @@ -55,7 +56,8 @@ export default class StatusList extends ImmutablePureComponent { }; handleLoadOlder = debounce(() => { - this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined); + const { statusIds, lastId, onLoadMore } = this.props; + onLoadMore(lastId || (statusIds.size > 0 ? statusIds.last() : undefined)); }, 300, { leading: true }); _selectChild (index, align_top) { diff --git a/app/javascript/mastodon/components/timeline_hint.jsx b/app/javascript/mastodon/components/timeline_hint.jsx deleted file mode 100644 index ac9a79dcc..000000000 --- a/app/javascript/mastodon/components/timeline_hint.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; - -const TimelineHint = ({ resource, url }) => ( -
- -
- -
-); - -TimelineHint.propTypes = { - resource: PropTypes.node.isRequired, - url: PropTypes.string.isRequired, -}; - -export default TimelineHint; diff --git a/app/javascript/mastodon/components/timeline_hint.tsx b/app/javascript/mastodon/components/timeline_hint.tsx new file mode 100644 index 000000000..712b4c293 --- /dev/null +++ b/app/javascript/mastodon/components/timeline_hint.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import { FormattedMessage } from 'react-intl'; + +interface Props { + resource: JSX.Element; + url: string; +} + +export const TimelineHint: React.FC = ({ resource, url }) => ( +
+ + + +
+ + + +
+); diff --git a/app/javascript/mastodon/components/verified_badge.tsx b/app/javascript/mastodon/components/verified_badge.tsx index da3cab9fe..4c5de3120 100644 --- a/app/javascript/mastodon/components/verified_badge.tsx +++ b/app/javascript/mastodon/components/verified_badge.tsx @@ -1,9 +1,10 @@ import React from 'react'; + import { Icon } from './icon'; -type Props = { +interface Props { link: string; -}; +} export const VerifiedBadge: React.FC = ({ link }) => ( diff --git a/app/javascript/mastodon/containers/media_container.jsx b/app/javascript/mastodon/containers/media_container.jsx index f9244f8dd..1b6caaba8 100644 --- a/app/javascript/mastodon/containers/media_container.jsx +++ b/app/javascript/mastodon/containers/media_container.jsx @@ -1,5 +1,5 @@ import React, { PureComponent, Fragment } from 'react'; -import ReactDOM from 'react-dom'; +import { createPortal } from 'react-dom'; import PropTypes from 'prop-types'; import { IntlProvider, addLocaleData } from 'react-intl'; import { fromJS } from 'immutable'; @@ -29,19 +29,20 @@ export default class MediaContainer extends PureComponent { state = { media: null, index: null, + lang: null, time: null, backgroundColor: null, options: null, }; - handleOpenMedia = (media, index) => { + handleOpenMedia = (media, index, lang) => { document.body.classList.add('with-modals--active'); document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; - this.setState({ media, index }); + this.setState({ media, index, lang }); }; - handleOpenVideo = (options) => { + handleOpenVideo = (lang, options) => { const { components } = this.props; const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props')); const mediaList = fromJS(media); @@ -49,7 +50,7 @@ export default class MediaContainer extends PureComponent { document.body.classList.add('with-modals--active'); document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; - this.setState({ media: mediaList, options }); + this.setState({ media: mediaList, lang, options }); }; handleCloseMedia = () => { @@ -94,7 +95,7 @@ export default class MediaContainer extends PureComponent { }), }); - return ReactDOM.createPortal( + return createPortal( , component, ); @@ -105,6 +106,7 @@ export default class MediaContainer extends PureComponent { ({ dispatch(mentionCompose(account, router)); }, - onOpenMedia (statusId, media, index) { - dispatch(openModal('MEDIA', { statusId, media, index })); + onOpenMedia (statusId, media, index, lang) { + dispatch(openModal('MEDIA', { statusId, media, index, lang })); }, - onOpenVideo (statusId, media, options) { - dispatch(openModal('VIDEO', { statusId, media, options })); + onOpenVideo (statusId, media, lang, options) { + dispatch(openModal('VIDEO', { statusId, media, lang, options })); }, onBlock (status) { diff --git a/app/javascript/mastodon/features/about/index.jsx b/app/javascript/mastodon/features/about/index.jsx index 11aff67c6..61a9180a1 100644 --- a/app/javascript/mastodon/features/about/index.jsx +++ b/app/javascript/mastodon/features/about/index.jsx @@ -8,10 +8,10 @@ import LinkFooter from 'mastodon/features/ui/components/link_footer'; import { Helmet } from 'react-helmet'; import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server'; import Account from 'mastodon/containers/account_container'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; import { Icon } from 'mastodon/components/icon'; import classNames from 'classnames'; -import { Image } from 'mastodon/components/image'; +import { ServerHeroImage } from 'mastodon/components/server_hero_image'; const messages = defineMessages({ title: { id: 'column.about', defaultMessage: 'About' }, @@ -114,7 +114,7 @@ class About extends React.PureComponent {
- `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' /> + `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />

{isLoading ? : server.get('domain')}

Mastodon }} />

diff --git a/app/javascript/mastodon/features/account/components/account_note.jsx b/app/javascript/mastodon/features/account/components/account_note.jsx index 5201ebd4d..9a81b0aee 100644 --- a/app/javascript/mastodon/features/account/components/account_note.jsx +++ b/app/javascript/mastodon/features/account/components/account_note.jsx @@ -22,7 +22,7 @@ class InlineAlert extends React.PureComponent { static TRANSITION_DELAY = 200; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!this.props.show && nextProps.show) { this.setState({ mountMessage: true }); } else if (this.props.show && !nextProps.show) { @@ -58,11 +58,11 @@ class AccountNote extends ImmutablePureComponent { saved: false, }; - componentWillMount () { + UNSAFE_componentWillMount () { this._reset(); } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { const accountWillChange = !is(this.props.account, nextProps.account); const newState = {}; diff --git a/app/javascript/mastodon/features/account_gallery/index.jsx b/app/javascript/mastodon/features/account_gallery/index.jsx index b876df6a2..8c44fa346 100644 --- a/app/javascript/mastodon/features/account_gallery/index.jsx +++ b/app/javascript/mastodon/features/account_gallery/index.jsx @@ -136,16 +136,17 @@ class AccountGallery extends ImmutablePureComponent { handleOpenMedia = attachment => { const { dispatch } = this.props; const statusId = attachment.getIn(['status', 'id']); + const lang = attachment.getIn(['status', 'language']); if (attachment.get('type') === 'video') { - dispatch(openModal('VIDEO', { media: attachment, statusId, options: { autoPlay: true } })); + dispatch(openModal('VIDEO', { media: attachment, statusId, lang, options: { autoPlay: true } })); } else if (attachment.get('type') === 'audio') { - dispatch(openModal('AUDIO', { media: attachment, statusId, options: { autoPlay: true } })); + dispatch(openModal('AUDIO', { media: attachment, statusId, lang, options: { autoPlay: true } })); } else { const media = attachment.getIn(['status', 'media_attachments']); const index = media.findIndex(x => x.get('id') === attachment.get('id')); - dispatch(openModal('MEDIA', { media, index, statusId })); + dispatch(openModal('MEDIA', { media, index, statusId, lang })); } }; diff --git a/app/javascript/mastodon/features/account_timeline/components/moved_note.jsx b/app/javascript/mastodon/features/account_timeline/components/moved_note.jsx index 273583c0a..29861612c 100644 --- a/app/javascript/mastodon/features/account_timeline/components/moved_note.jsx +++ b/app/javascript/mastodon/features/account_timeline/components/moved_note.jsx @@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { AvatarOverlay } from '../../../components/avatar_overlay'; -import DisplayName from '../../../components/display_name'; +import { DisplayName } from '../../../components/display_name'; import { Link } from 'react-router-dom'; export default class MovedNote extends ImmutablePureComponent { diff --git a/app/javascript/mastodon/features/account_timeline/index.jsx b/app/javascript/mastodon/features/account_timeline/index.jsx index bc66f128d..ce9485216 100644 --- a/app/javascript/mastodon/features/account_timeline/index.jsx +++ b/app/javascript/mastodon/features/account_timeline/index.jsx @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { lookupAccount, fetchAccount } from '../../actions/accounts'; -import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines'; +import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines'; import StatusList from '../../components/status_list'; import LoadingIndicator from '../../components/loading_indicator'; import Column from '../ui/components/column'; @@ -12,9 +12,8 @@ import ColumnBackButton from '../../components/column_back_button'; import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; -import TimelineHint from 'mastodon/components/timeline_hint'; +import { TimelineHint } from 'mastodon/components/timeline_hint'; import { me } from 'mastodon/initial_state'; -import { connectTimeline, disconnectTimeline } from 'mastodon/actions/timelines'; import LimitedAccountHint from './components/limited_account_hint'; import { getAccountHidden } from 'mastodon/selectors'; import { fetchFeaturedTags } from '../../actions/featured_tags'; diff --git a/app/javascript/mastodon/features/audio/index.jsx b/app/javascript/mastodon/features/audio/index.jsx index 56e913ae3..5ed02d937 100644 --- a/app/javascript/mastodon/features/audio/index.jsx +++ b/app/javascript/mastodon/features/audio/index.jsx @@ -136,7 +136,7 @@ class Audio extends React.PureComponent { } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { this.setState({ revealed: nextProps.visible }); } diff --git a/app/javascript/mastodon/features/blocks/index.jsx b/app/javascript/mastodon/features/blocks/index.jsx index 38616ba84..da28f12d7 100644 --- a/app/javascript/mastodon/features/blocks/index.jsx +++ b/app/javascript/mastodon/features/blocks/index.jsx @@ -34,7 +34,7 @@ class Blocks extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchBlocks()); } diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx b/app/javascript/mastodon/features/bookmarked_statuses/index.jsx index 46fff856e..1ffe7e768 100644 --- a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx +++ b/app/javascript/mastodon/features/bookmarked_statuses/index.jsx @@ -34,7 +34,7 @@ class Bookmarks extends ImmutablePureComponent { isLoading: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchBookmarkedStatuses()); } diff --git a/app/javascript/mastodon/features/compose/components/autosuggest_account.jsx b/app/javascript/mastodon/features/compose/components/autosuggest_account.jsx index 2c92016e8..a635657d9 100644 --- a/app/javascript/mastodon/features/compose/components/autosuggest_account.jsx +++ b/app/javascript/mastodon/features/compose/components/autosuggest_account.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { Avatar } from '../../../components/avatar'; -import DisplayName from '../../../components/display_name'; +import { DisplayName } from '../../../components/display_name'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx index 4fb131b47..c12d56e4a 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx @@ -27,7 +27,7 @@ const messages = defineMessages({ let EmojiPicker, Emoji; // load asynchronously -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`; @@ -59,7 +59,7 @@ class ModifierPickerMenu extends React.PureComponent { this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1); }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.active) { this.attachListeners(); } else { @@ -78,12 +78,12 @@ class ModifierPickerMenu extends React.PureComponent { }; attachListeners () { - document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); } removeListeners () { - document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } @@ -176,7 +176,7 @@ class EmojiPickerMenuImpl extends React.PureComponent { }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need @@ -191,7 +191,7 @@ class EmojiPickerMenuImpl extends React.PureComponent { } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } diff --git a/app/javascript/mastodon/features/compose/components/language_dropdown.jsx b/app/javascript/mastodon/features/compose/components/language_dropdown.jsx index 08542e3a9..731d7b38d 100644 --- a/app/javascript/mastodon/features/compose/components/language_dropdown.jsx +++ b/app/javascript/mastodon/features/compose/components/language_dropdown.jsx @@ -15,7 +15,7 @@ const messages = defineMessages({ clear: { id: 'emoji_button.clear', defaultMessage: 'Clear' }, }); -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; class LanguageDropdownMenu extends React.PureComponent { @@ -39,11 +39,12 @@ class LanguageDropdownMenu extends React.PureComponent { handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); + e.stopPropagation(); } }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need @@ -57,7 +58,7 @@ class LanguageDropdownMenu extends React.PureComponent { } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx b/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx index ecb7accad..dd20cccc2 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx @@ -19,7 +19,7 @@ const messages = defineMessages({ change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, }); -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; class PrivacyDropdownMenu extends React.PureComponent { @@ -34,6 +34,7 @@ class PrivacyDropdownMenu extends React.PureComponent { handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); + e.stopPropagation(); } }; @@ -91,13 +92,13 @@ class PrivacyDropdownMenu extends React.PureComponent { }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } @@ -212,7 +213,7 @@ class PrivacyDropdown extends React.PureComponent { this.props.onChange(value); }; - componentWillMount () { + UNSAFE_componentWillMount () { const { intl: { formatMessage } } = this.props; this.options = [ diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.jsx b/app/javascript/mastodon/features/compose/components/reply_indicator.jsx index 68cd7b080..b3f1b1b48 100644 --- a/app/javascript/mastodon/features/compose/components/reply_indicator.jsx +++ b/app/javascript/mastodon/features/compose/components/reply_indicator.jsx @@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { Avatar } from '../../../components/avatar'; import { IconButton } from '../../../components/icon_button'; -import DisplayName from '../../../components/display_name'; +import { DisplayName } from '../../../components/display_name'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import AttachmentList from 'mastodon/components/attachment_list'; diff --git a/app/javascript/mastodon/features/directory/components/account_card.jsx b/app/javascript/mastodon/features/directory/components/account_card.jsx index 495142715..1ef9d6481 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.jsx +++ b/app/javascript/mastodon/features/directory/components/account_card.jsx @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { makeGetAccount } from 'mastodon/selectors'; import { Avatar } from 'mastodon/components/avatar'; -import DisplayName from 'mastodon/components/display_name'; +import { DisplayName } from 'mastodon/components/display_name'; import { Link } from 'react-router-dom'; import Button from 'mastodon/components/button'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; diff --git a/app/javascript/mastodon/features/domain_blocks/index.jsx b/app/javascript/mastodon/features/domain_blocks/index.jsx index 0c22aa239..6a9f6e4cf 100644 --- a/app/javascript/mastodon/features/domain_blocks/index.jsx +++ b/app/javascript/mastodon/features/domain_blocks/index.jsx @@ -34,7 +34,7 @@ class Blocks extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchDomainBlocks()); } diff --git a/app/javascript/mastodon/features/explore/components/story.jsx b/app/javascript/mastodon/features/explore/components/story.jsx index c7320c886..6e8db6230 100644 --- a/app/javascript/mastodon/features/explore/components/story.jsx +++ b/app/javascript/mastodon/features/explore/components/story.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Blurhash } from 'mastodon/components/blurhash'; import { accountsCountRenderer } from 'mastodon/components/hashtag'; import ShortNumber from 'mastodon/components/short_number'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; import classNames from 'classnames'; export default class Story extends React.PureComponent { diff --git a/app/javascript/mastodon/features/favourited_statuses/index.jsx b/app/javascript/mastodon/features/favourited_statuses/index.jsx index 2cbf00291..161297114 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.jsx +++ b/app/javascript/mastodon/features/favourited_statuses/index.jsx @@ -34,7 +34,7 @@ class Favourites extends ImmutablePureComponent { isLoading: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchFavouritedStatuses()); } diff --git a/app/javascript/mastodon/features/favourites/index.jsx b/app/javascript/mastodon/features/favourites/index.jsx index 4a8c1deb6..ed210dad5 100644 --- a/app/javascript/mastodon/features/favourites/index.jsx +++ b/app/javascript/mastodon/features/favourites/index.jsx @@ -31,13 +31,13 @@ class Favourites extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; - componentWillMount () { + UNSAFE_componentWillMount () { if (!this.props.accountIds) { this.props.dispatch(fetchFavourites(this.props.params.statusId)); } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { this.props.dispatch(fetchFavourites(nextProps.params.statusId)); } diff --git a/app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx b/app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx index b370b33ec..5d0632b0f 100644 --- a/app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx +++ b/app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Link } from 'react-router-dom'; import { Avatar } from '../../../components/avatar'; -import DisplayName from '../../../components/display_name'; +import { DisplayName } from '../../../components/display_name'; import { IconButton } from '../../../components/icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; diff --git a/app/javascript/mastodon/features/follow_requests/index.jsx b/app/javascript/mastodon/features/follow_requests/index.jsx index a8875bbd3..779bc473e 100644 --- a/app/javascript/mastodon/features/follow_requests/index.jsx +++ b/app/javascript/mastodon/features/follow_requests/index.jsx @@ -39,7 +39,7 @@ class FollowRequests extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchFollowRequests()); } diff --git a/app/javascript/mastodon/features/followers/index.jsx b/app/javascript/mastodon/features/followers/index.jsx index cdd65c9ef..1a1fdf578 100644 --- a/app/javascript/mastodon/features/followers/index.jsx +++ b/app/javascript/mastodon/features/followers/index.jsx @@ -17,7 +17,7 @@ import Column from '../ui/components/column'; import HeaderContainer from '../account_timeline/containers/header_container'; import ColumnBackButton from '../../components/column_back_button'; import ScrollableList from '../../components/scrollable_list'; -import TimelineHint from 'mastodon/components/timeline_hint'; +import { TimelineHint } from 'mastodon/components/timeline_hint'; import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; import { getAccountHidden } from 'mastodon/selectors'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; diff --git a/app/javascript/mastodon/features/following/index.jsx b/app/javascript/mastodon/features/following/index.jsx index 26dee213d..c024ff063 100644 --- a/app/javascript/mastodon/features/following/index.jsx +++ b/app/javascript/mastodon/features/following/index.jsx @@ -17,7 +17,7 @@ import Column from '../ui/components/column'; import HeaderContainer from '../account_timeline/containers/header_container'; import ColumnBackButton from '../../components/column_back_button'; import ScrollableList from '../../components/scrollable_list'; -import TimelineHint from 'mastodon/components/timeline_hint'; +import { TimelineHint } from 'mastodon/components/timeline_hint'; import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; import { getAccountHidden } from 'mastodon/selectors'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; diff --git a/app/javascript/mastodon/features/interaction_modal/index.jsx b/app/javascript/mastodon/features/interaction_modal/index.jsx index f3db2c78d..d9eff63cb 100644 --- a/app/javascript/mastodon/features/interaction_modal/index.jsx +++ b/app/javascript/mastodon/features/interaction_modal/index.jsx @@ -143,7 +143,7 @@ class InteractionModal extends React.PureComponent {

- + {signupButton}
diff --git a/app/javascript/mastodon/features/list_adder/components/account.jsx b/app/javascript/mastodon/features/list_adder/components/account.jsx index 410f1537a..5dc384aba 100644 --- a/app/javascript/mastodon/features/list_adder/components/account.jsx +++ b/app/javascript/mastodon/features/list_adder/components/account.jsx @@ -4,7 +4,7 @@ import { makeGetAccount } from '../../../selectors'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Avatar } from '../../../components/avatar'; -import DisplayName from '../../../components/display_name'; +import { DisplayName } from '../../../components/display_name'; import { injectIntl } from 'react-intl'; const makeMapStateToProps = () => { diff --git a/app/javascript/mastodon/features/list_editor/components/account.jsx b/app/javascript/mastodon/features/list_editor/components/account.jsx index b46d0504a..fc1d2d607 100644 --- a/app/javascript/mastodon/features/list_editor/components/account.jsx +++ b/app/javascript/mastodon/features/list_editor/components/account.jsx @@ -5,7 +5,7 @@ import { makeGetAccount } from '../../../selectors'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Avatar } from '../../../components/avatar'; -import DisplayName from '../../../components/display_name'; +import { DisplayName } from '../../../components/display_name'; import { IconButton } from '../../../components/icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import { removeFromListEditor, addToListEditor } from '../../../actions/lists'; diff --git a/app/javascript/mastodon/features/list_timeline/index.jsx b/app/javascript/mastodon/features/list_timeline/index.jsx index c93305341..e1408d8ef 100644 --- a/app/javascript/mastodon/features/list_timeline/index.jsx +++ b/app/javascript/mastodon/features/list_timeline/index.jsx @@ -76,7 +76,7 @@ class ListTimeline extends React.PureComponent { this.disconnect = dispatch(connectListStream(id)); } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { const { dispatch } = this.props; const { id } = nextProps.params; diff --git a/app/javascript/mastodon/features/lists/index.jsx b/app/javascript/mastodon/features/lists/index.jsx index afd645a30..232b0c2d5 100644 --- a/app/javascript/mastodon/features/lists/index.jsx +++ b/app/javascript/mastodon/features/lists/index.jsx @@ -42,7 +42,7 @@ class Lists extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchLists()); } diff --git a/app/javascript/mastodon/features/mutes/index.jsx b/app/javascript/mastodon/features/mutes/index.jsx index 5c05d2f70..078d8779e 100644 --- a/app/javascript/mastodon/features/mutes/index.jsx +++ b/app/javascript/mastodon/features/mutes/index.jsx @@ -35,7 +35,7 @@ class Mutes extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchMutes()); } diff --git a/app/javascript/mastodon/features/notifications/components/follow_request.jsx b/app/javascript/mastodon/features/notifications/components/follow_request.jsx index 0d930f0b1..d8b2ca1cc 100644 --- a/app/javascript/mastodon/features/notifications/components/follow_request.jsx +++ b/app/javascript/mastodon/features/notifications/components/follow_request.jsx @@ -2,7 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { Avatar } from 'mastodon/components/avatar'; -import DisplayName from 'mastodon/components/display_name'; +import { DisplayName } from 'mastodon/components/display_name'; import { Link } from 'react-router-dom'; import { IconButton } from 'mastodon/components/icon_button'; import { defineMessages, injectIntl } from 'react-intl'; diff --git a/app/javascript/mastodon/features/notifications/index.jsx b/app/javascript/mastodon/features/notifications/index.jsx index 8a31c5db6..8b77374c4 100644 --- a/app/javascript/mastodon/features/notifications/index.jsx +++ b/app/javascript/mastodon/features/notifications/index.jsx @@ -93,7 +93,7 @@ class Notifications extends React.PureComponent { trackScroll: true, }; - componentWillMount() { + UNSAFE_componentWillMount() { this.props.dispatch(mountNotifications()); } diff --git a/app/javascript/mastodon/features/onboarding/follows.jsx b/app/javascript/mastodon/features/onboarding/follows.jsx index 7cccdefb3..c96c69055 100644 --- a/app/javascript/mastodon/features/onboarding/follows.jsx +++ b/app/javascript/mastodon/features/onboarding/follows.jsx @@ -7,7 +7,7 @@ import { fetchSuggestions } from 'mastodon/actions/suggestions'; import { markAsPartial } from 'mastodon/actions/timelines'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Account from 'mastodon/containers/account_container'; -import EmptyAccount from 'mastodon/components/account'; +import { EmptyAccount } from 'mastodon/components/empty_account'; import { FormattedMessage, FormattedHTMLMessage } from 'react-intl'; import { makeGetAccount } from 'mastodon/selectors'; import { me } from 'mastodon/initial_state'; @@ -31,6 +31,7 @@ class Follows extends React.PureComponent { suggestions: ImmutablePropTypes.list, account: ImmutablePropTypes.map, isLoading: PropTypes.bool, + multiColumn: PropTypes.bool, }; componentDidMount () { @@ -44,7 +45,7 @@ class Follows extends React.PureComponent { } render () { - const { onBack, isLoading, suggestions, account } = this.props; + const { onBack, isLoading, suggestions, account, multiColumn } = this.props; let loadedContent; @@ -58,7 +59,7 @@ class Follows extends React.PureComponent { return ( - +
@@ -84,4 +85,4 @@ class Follows extends React.PureComponent { } -export default connect(mapStateToProps)(Follows); \ No newline at end of file +export default connect(mapStateToProps)(Follows); diff --git a/app/javascript/mastodon/features/onboarding/index.jsx b/app/javascript/mastodon/features/onboarding/index.jsx index 388734055..ca4a8bcf1 100644 --- a/app/javascript/mastodon/features/onboarding/index.jsx +++ b/app/javascript/mastodon/features/onboarding/index.jsx @@ -40,6 +40,7 @@ class Onboarding extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, account: ImmutablePropTypes.map, + multiColumn: PropTypes.bool, }; state = { @@ -93,14 +94,14 @@ class Onboarding extends ImmutablePureComponent { } render () { - const { account } = this.props; + const { account, multiColumn } = this.props; const { step, shareClicked } = this.state; switch(step) { case 'follows': - return ; + return ; case 'share': - return ; + return ; } return ( @@ -114,7 +115,7 @@ class Onboarding extends ImmutablePureComponent {
0 && account.get('note').length > 0)} icon='address-book-o' label={} description={} /> - = 7} icon='user-plus' label={} description={} /> + = 7} icon='user-plus' label={} description={} /> = 1} icon='pencil-square-o' label={} description={} /> } description={} />
diff --git a/app/javascript/mastodon/features/onboarding/share.jsx b/app/javascript/mastodon/features/onboarding/share.jsx index 5f7cfb8a6..82fdada41 100644 --- a/app/javascript/mastodon/features/onboarding/share.jsx +++ b/app/javascript/mastodon/features/onboarding/share.jsx @@ -140,17 +140,18 @@ class Share extends React.PureComponent { static propTypes = { onBack: PropTypes.func, account: ImmutablePropTypes.map, + multiColumn: PropTypes.bool, intl: PropTypes.object, }; render () { - const { onBack, account, intl } = this.props; + const { onBack, account, multiColumn, intl } = this.props; const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href; return ( - +
diff --git a/app/javascript/mastodon/features/picture_in_picture/components/header.jsx b/app/javascript/mastodon/features/picture_in_picture/components/header.jsx index c6d2a103d..c1c04da54 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/header.jsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/header.jsx @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import { IconButton } from 'mastodon/components/icon_button'; import { Link } from 'react-router-dom'; import { Avatar } from 'mastodon/components/avatar'; -import DisplayName from 'mastodon/components/display_name'; +import { DisplayName } from 'mastodon/components/display_name'; import { defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ diff --git a/app/javascript/mastodon/features/pinned_statuses/index.jsx b/app/javascript/mastodon/features/pinned_statuses/index.jsx index 24a0b6d2e..e58ce2bb8 100644 --- a/app/javascript/mastodon/features/pinned_statuses/index.jsx +++ b/app/javascript/mastodon/features/pinned_statuses/index.jsx @@ -29,7 +29,7 @@ class PinnedStatuses extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchPinnedStatuses()); } diff --git a/app/javascript/mastodon/features/privacy_policy/index.jsx b/app/javascript/mastodon/features/privacy_policy/index.jsx index d5bbda6a3..07a691498 100644 --- a/app/javascript/mastodon/features/privacy_policy/index.jsx +++ b/app/javascript/mastodon/features/privacy_policy/index.jsx @@ -4,7 +4,7 @@ import { Helmet } from 'react-helmet'; import { FormattedMessage, FormattedDate, injectIntl, defineMessages } from 'react-intl'; import Column from 'mastodon/components/column'; import api from 'mastodon/api'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; const messages = defineMessages({ title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' }, diff --git a/app/javascript/mastodon/features/reblogs/index.jsx b/app/javascript/mastodon/features/reblogs/index.jsx index fb503f40f..757ef0dd0 100644 --- a/app/javascript/mastodon/features/reblogs/index.jsx +++ b/app/javascript/mastodon/features/reblogs/index.jsx @@ -31,13 +31,13 @@ class Reblogs extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; - componentWillMount () { + UNSAFE_componentWillMount () { if (!this.props.accountIds) { this.props.dispatch(fetchReblogs(this.props.params.statusId)); } } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { this.props.dispatch(fetchReblogs(nextProps.params.statusId)); } diff --git a/app/javascript/mastodon/features/report/components/status_check_box.jsx b/app/javascript/mastodon/features/report/components/status_check_box.jsx index 003cdc8e3..8d6091f77 100644 --- a/app/javascript/mastodon/features/report/components/status_check_box.jsx +++ b/app/javascript/mastodon/features/report/components/status_check_box.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import StatusContent from 'mastodon/components/status_content'; import { Avatar } from 'mastodon/components/avatar'; -import DisplayName from 'mastodon/components/display_name'; +import { DisplayName } from 'mastodon/components/display_name'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import Option from './option'; import MediaAttachments from 'mastodon/components/media_attachments'; diff --git a/app/javascript/mastodon/features/status/components/card.jsx b/app/javascript/mastodon/features/status/components/card.jsx index 1d5edfc17..bc0266531 100644 --- a/app/javascript/mastodon/features/status/components/card.jsx +++ b/app/javascript/mastodon/features/status/components/card.jsx @@ -66,7 +66,7 @@ export default class Card extends React.PureComponent { revealed: !this.props.sensitive, }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!Immutable.is(this.props.card, nextProps.card)) { this.setState({ embedded: false, previewLoaded: false }); } diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index 72c902124..5019dfdb4 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Avatar } from '../../../components/avatar'; -import DisplayName from '../../../components/display_name'; +import { DisplayName } from '../../../components/display_name'; import StatusContent from '../../../components/status_content'; import MediaGallery from '../../../components/media_gallery'; import { Link } from 'react-router-dom'; diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js index bfed16620..835bb41b5 100644 --- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js +++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js @@ -128,12 +128,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(mentionCompose(account, router)); }, - onOpenMedia (media, index) { - dispatch(openModal('MEDIA', { media, index })); + onOpenMedia (media, index, lang) { + dispatch(openModal('MEDIA', { media, index, lang })); }, - onOpenVideo (media, options) { - dispatch(openModal('VIDEO', { media, options })); + onOpenVideo (media, lang, options) { + dispatch(openModal('VIDEO', { media, lang, options })); }, onBlock (status) { diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 0c0b55748..5f1715c27 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -207,7 +207,7 @@ class Status extends ImmutablePureComponent { loadedStatusId: undefined, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchStatus(this.props.params.statusId)); } @@ -215,7 +215,7 @@ class Status extends ImmutablePureComponent { attachFullscreenListener(this.onFullScreenChange); } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { this._scrolledIntoView = false; this.props.dispatch(fetchStatus(nextProps.params.statusId)); @@ -345,12 +345,12 @@ class Status extends ImmutablePureComponent { this.props.dispatch(mentionCompose(account, router)); }; - handleOpenMedia = (media, index) => { - this.props.dispatch(openModal('MEDIA', { statusId: this.props.status.get('id'), media, index })); + handleOpenMedia = (media, index, lang) => { + this.props.dispatch(openModal('MEDIA', { statusId: this.props.status.get('id'), media, index, lang })); }; - handleOpenVideo = (media, options) => { - this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, options })); + handleOpenVideo = (media, lang, options) => { + this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, lang, options })); }; handleHotkeyOpenMedia = e => { diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.jsx b/app/javascript/mastodon/features/ui/components/boost_modal.jsx index 1f28927d6..4eeb30be0 100644 --- a/app/javascript/mastodon/features/ui/components/boost_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/boost_modal.jsx @@ -7,7 +7,7 @@ import Button from '../../../components/button'; import StatusContent from '../../../components/status_content'; import { Avatar } from '../../../components/avatar'; import { RelativeTimestamp } from '../../../components/relative_timestamp'; -import DisplayName from '../../../components/display_name'; +import { DisplayName } from '../../../components/display_name'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { Icon } from 'mastodon/components/icon'; import AttachmentList from 'mastodon/components/attachment_list'; diff --git a/app/javascript/mastodon/features/ui/components/bundle.jsx b/app/javascript/mastodon/features/ui/components/bundle.jsx index 1b10a218b..c1e837b16 100644 --- a/app/javascript/mastodon/features/ui/components/bundle.jsx +++ b/app/javascript/mastodon/features/ui/components/bundle.jsx @@ -33,11 +33,11 @@ class Bundle extends React.PureComponent { forceRender: false, }; - componentWillMount() { + UNSAFE_componentWillMount() { this.load(this.props); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.fetchComponent !== this.props.fetchComponent) { this.load(nextProps); } diff --git a/app/javascript/mastodon/features/ui/components/columns_area.jsx b/app/javascript/mastodon/features/ui/components/columns_area.jsx index 1dd6e34e8..86911efa3 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.jsx +++ b/app/javascript/mastodon/features/ui/components/columns_area.jsx @@ -18,7 +18,7 @@ import { BookmarkedStatuses, ListTimeline, Directory, -} from '../../ui/util/async-components'; +} from '../util/async-components'; import ComposePanel from './compose_panel'; import NavigationPanel from './navigation_panel'; import { supportsPassiveEvents } from 'detect-passive-events'; @@ -76,7 +76,7 @@ export default class ColumnsArea extends ImmutablePureComponent { this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl'); } - componentWillUpdate(nextProps) { + UNSAFE_componentWillUpdate(nextProps) { if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) { this.node.removeEventListener('wheel', this.handleWheel); } diff --git a/app/javascript/mastodon/features/ui/components/embed_modal.jsx b/app/javascript/mastodon/features/ui/components/embed_modal.jsx index 3e0bcc93c..949c64042 100644 --- a/app/javascript/mastodon/features/ui/components/embed_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/embed_modal.jsx @@ -85,7 +85,7 @@ class EmbedModal extends ImmutablePureComponent { className='embed-modal__iframe' frameBorder='0' ref={this.setIframeRef} - sandbox='allow-same-origin' + sandbox='allow-scripts allow-same-origin' title='preview' />
diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx b/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx index eda550b04..d6f15e276 100644 --- a/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx @@ -5,11 +5,10 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; import classNames from 'classnames'; import { changeUploadCompose, uploadThumbnail, onChangeMediaDescription, onChangeMediaFocus } from '../../../actions/compose'; -import { getPointerPosition } from '../../video'; +import Video, { getPointerPosition } from '../../video'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { IconButton } from 'mastodon/components/icon_button'; import Button from 'mastodon/components/button'; -import Video from 'mastodon/features/video'; import Audio from 'mastodon/features/audio'; import Textarea from 'react-textarea-autosize'; import UploadProgress from 'mastodon/features/compose/components/upload_progress'; diff --git a/app/javascript/mastodon/features/ui/components/header.jsx b/app/javascript/mastodon/features/ui/components/header.jsx index af18ac331..6419dbc50 100644 --- a/app/javascript/mastodon/features/ui/components/header.jsx +++ b/app/javascript/mastodon/features/ui/components/header.jsx @@ -51,13 +51,13 @@ class Header extends React.PureComponent { if (registrationsOpen) { signupButton = ( - + ); } else { signupButton = ( - ); @@ -65,8 +65,8 @@ class Header extends React.PureComponent { content = ( <> - {signupButton} + ); } diff --git a/app/javascript/mastodon/features/ui/components/media_modal.jsx b/app/javascript/mastodon/features/ui/components/media_modal.jsx index e8293c2e7..594f5cf64 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/media_modal.jsx @@ -3,7 +3,6 @@ import ReactSwipeableViews from 'react-swipeable-views'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import Video from 'mastodon/features/video'; -import { connect } from 'react-redux'; import classNames from 'classnames'; import { defineMessages, injectIntl } from 'react-intl'; import { IconButton } from 'mastodon/components/icon_button'; @@ -21,15 +20,12 @@ const messages = defineMessages({ next: { id: 'lightbox.next', defaultMessage: 'Next' }, }); -const mapStateToProps = (state, { statusId }) => ({ - language: state.getIn(['statuses', statusId, 'language']), -}); - class MediaModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.list.isRequired, statusId: PropTypes.string, + lang: PropTypes.string, index: PropTypes.number.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -133,7 +129,7 @@ class MediaModal extends ImmutablePureComponent { }; render () { - const { media, language, statusId, intl, onClose } = this.props; + const { media, statusId, lang, intl, onClose } = this.props; const { navigationHidden } = this.state; const index = this.getIndex(); @@ -153,7 +149,7 @@ class MediaModal extends ImmutablePureComponent { width={width} height={height} alt={image.get('description')} - lang={language} + lang={lang} key={image.get('url')} onClick={this.toggleNavigation} zoomButtonHidden={this.state.zoomButtonHidden} @@ -176,7 +172,7 @@ class MediaModal extends ImmutablePureComponent { onCloseVideo={onClose} detailed alt={image.get('description')} - lang={language} + lang={lang} key={image.get('url')} /> ); @@ -188,7 +184,7 @@ class MediaModal extends ImmutablePureComponent { height={height} key={image.get('url')} alt={image.get('description')} - lang={language} + lang={lang} onClick={this.toggleNavigation} /> ); @@ -256,4 +252,4 @@ class MediaModal extends ImmutablePureComponent { } -export default connect(mapStateToProps, null, null, { forwardRef: true })(injectIntl(MediaModal)); +export default injectIntl(MediaModal); diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx index ee1a83cc6..b62d216ae 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import { Link } from 'react-router-dom'; -import Logo from 'mastodon/components/logo'; +import { WordmarkLogo } from 'mastodon/components/logo'; import { timelinePreview, showTrends } from 'mastodon/initial_state'; import ColumnLink from './column_link'; import DisabledAccountBanner from './disabled_account_banner'; @@ -46,7 +46,7 @@ class NavigationPanel extends React.Component { return (
- +
diff --git a/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx b/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx index 86fcc11b5..39f0c71c3 100644 --- a/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx +++ b/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx @@ -16,13 +16,13 @@ const SignInBanner = () => { if (registrationsOpen) { signupButton = ( - + ); } else { signupButton = ( - ); @@ -30,9 +30,9 @@ const SignInBanner = () => { return (
-

- +

{signupButton} +
); }; diff --git a/app/javascript/mastodon/features/ui/components/upload_area.jsx b/app/javascript/mastodon/features/ui/components/upload_area.jsx index 035fe7a26..cec4cf5b1 100644 --- a/app/javascript/mastodon/features/ui/components/upload_area.jsx +++ b/app/javascript/mastodon/features/ui/components/upload_area.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Motion from '../../ui/util/optional_motion'; +import Motion from '../util/optional_motion'; import spring from 'react-motion/lib/spring'; import { FormattedMessage } from 'react-intl'; diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index 4ce4ac6c8..8e97460c4 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -37,6 +37,7 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, { timelineId }) => ({ statusIds: getStatusIds(state, { type: timelineId }), + lastId: state.getIn(['timelines', timelineId, 'items'])?.last(), isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), hasMore: state.getIn(['timelines', timelineId, 'hasMore']), diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 6dc5177b5..26a777732 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -123,7 +123,7 @@ class SwitchingColumnsArea extends React.PureComponent { mobile: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { if (this.props.mobile) { document.body.classList.toggle('layout-single-column', true); document.body.classList.toggle('layout-multiple-columns', false); diff --git a/app/javascript/mastodon/features/video/index.jsx b/app/javascript/mastodon/features/video/index.jsx index 8b57cf3d1..560dbc6d6 100644 --- a/app/javascript/mastodon/features/video/index.jsx +++ b/app/javascript/mastodon/features/video/index.jsx @@ -370,7 +370,7 @@ class Video extends React.PureComponent { } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { this.setState({ revealed: nextProps.visible }); } @@ -469,7 +469,7 @@ class Video extends React.PureComponent { handleOpenVideo = () => { this.video.pause(); - this.props.onOpenVideo({ + this.props.onOpenVideo(this.props.lang, { startTime: this.video.currentTime, autoPlay: !this.state.paused, defaultVolume: this.state.volume, diff --git a/app/javascript/mastodon/is_mobile.ts b/app/javascript/mastodon/is_mobile.ts index b2918eb4b..36cde2133 100644 --- a/app/javascript/mastodon/is_mobile.ts +++ b/app/javascript/mastodon/is_mobile.ts @@ -1,4 +1,5 @@ import { supportsPassiveEvents } from 'detect-passive-events'; + import { forceSingleColumn } from './initial_state'; const LAYOUT_BREAKPOINT = 630; diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index a735d2ff3..c64a970ea 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -356,7 +356,7 @@ { "descriptors": [ { - "defaultMessage": "You need to sign in to access this resource.", + "defaultMessage": "You need to login to access this resource.", "id": "not_signed_in_indicator.not_signed_in" } ], @@ -2623,7 +2623,7 @@ "id": "interaction_modal.on_this_server" }, { - "defaultMessage": "Sign in", + "defaultMessage": "Login", "id": "sign_in_banner.sign_in" }, { @@ -3236,7 +3236,7 @@ "id": "onboarding.steps.follow_people.title" }, { - "defaultMessage": "You curate your own feed. Lets fill it with interesting people.", + "defaultMessage": "You curate your own feed. Let's fill it with interesting people.", "id": "onboarding.steps.follow_people.body" }, { @@ -4175,7 +4175,7 @@ "id": "sign_in_banner.create_account" }, { - "defaultMessage": "Sign in", + "defaultMessage": "Login", "id": "sign_in_banner.sign_in" } ], @@ -4374,11 +4374,11 @@ "id": "sign_in_banner.create_account" }, { - "defaultMessage": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.", + "defaultMessage": "Login to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.", "id": "sign_in_banner.text" }, { - "defaultMessage": "Sign in", + "defaultMessage": "Login", "id": "sign_in_banner.sign_in" } ], diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 85040bf5b..a9c9f534f 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -391,7 +391,7 @@ "navigation_bar.public_timeline": "Federated timeline", "navigation_bar.search": "Search", "navigation_bar.security": "Security", - "not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.", + "not_signed_in_indicator.not_signed_in": "You need to login to access this resource.", "notification.admin.report": "{name} reported {target}", "notification.admin.sign_up": "{name} signed up", "notification.favourite": "{name} favourited your post", @@ -573,8 +573,8 @@ "server_banner.learn_more": "Learn more", "server_banner.server_stats": "Server stats:", "sign_in_banner.create_account": "Create account", - "sign_in_banner.sign_in": "Sign in", - "sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.", + "sign_in_banner.sign_in": "Login", + "sign_in_banner.text": "Login to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.", "status.admin_account": "Open moderation interface for @{name}", "status.admin_domain": "Open moderation interface for {domain}", "status.admin_status": "Open this post in the moderation interface", diff --git a/app/javascript/mastodon/locales/locale-data/co.js b/app/javascript/mastodon/locales/locale-data/co.js index 2b071ecbd..dff8a54da 100644 --- a/app/javascript/mastodon/locales/locale-data/co.js +++ b/app/javascript/mastodon/locales/locale-data/co.js @@ -2,7 +2,7 @@ /*eslint no-nested-ternary: "off"*/ /*eslint quotes: "off"*/ -export default [{ +const rules = [{ locale: "co", pluralRuleFunction: function (e, a) { return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other"; @@ -106,3 +106,5 @@ export default [{ }, }, }]; + +export default rules; diff --git a/app/javascript/mastodon/locales/locale-data/oc.js b/app/javascript/mastodon/locales/locale-data/oc.js index d4adc42eb..6ab306b8c 100644 --- a/app/javascript/mastodon/locales/locale-data/oc.js +++ b/app/javascript/mastodon/locales/locale-data/oc.js @@ -2,7 +2,7 @@ /*eslint no-nested-ternary: "off"*/ /*eslint quotes: "off"*/ -export default [{ +const rules = [{ locale: "oc", pluralRuleFunction: function (e, a) { return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other"; @@ -106,3 +106,5 @@ export default [{ }, }, }]; + +export default rules; diff --git a/app/javascript/mastodon/locales/locale-data/sa.js b/app/javascript/mastodon/locales/locale-data/sa.js index 946dfde0f..65e09e97f 100644 --- a/app/javascript/mastodon/locales/locale-data/sa.js +++ b/app/javascript/mastodon/locales/locale-data/sa.js @@ -2,9 +2,8 @@ /*eslint no-nested-ternary: "off"*/ /*eslint quotes: "off"*/ /*eslint comma-dangle: "off"*/ -/*eslint semi: "off"*/ -export default [ +const rules = [ { locale: "sa", fields: { @@ -94,4 +93,6 @@ export default [ } } } -] +]; + +export default rules; diff --git a/app/javascript/mastodon/main.jsx b/app/javascript/mastodon/main.jsx index c5960477d..b31540cb5 100644 --- a/app/javascript/mastodon/main.jsx +++ b/app/javascript/mastodon/main.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { setupBrowserNotifications } from 'mastodon/actions/notifications'; import Mastodon from 'mastodon/containers/mastodon'; import { store } from 'mastodon/store'; @@ -17,7 +17,8 @@ function main() { const mountNode = document.getElementById('mastodon'); const props = JSON.parse(mountNode.getAttribute('data-props')); - ReactDOM.render(, mountNode); + const root = createRoot(mountNode); + root.render(); store.dispatch(setupBrowserNotifications()); if (process.env.NODE_ENV === 'production' && me && 'serviceWorker' in navigator) { diff --git a/app/javascript/mastodon/polyfills/base_polyfills.ts b/app/javascript/mastodon/polyfills/base_polyfills.ts index 64211c11e..e008d8f02 100644 --- a/app/javascript/mastodon/polyfills/base_polyfills.ts +++ b/app/javascript/mastodon/polyfills/base_polyfills.ts @@ -10,8 +10,13 @@ if (!HTMLCanvasElement.prototype.toBlob) { const BASE64_MARKER = ';base64,'; Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { - value(callback: BlobCallback, type = 'image/png', quality: any) { - const dataURL = this.toDataURL(type, quality); + value: function ( + this: HTMLCanvasElement, + callback: BlobCallback, + type = 'image/png', + quality: unknown + ) { + const dataURL: string = this.toDataURL(type, quality); let data; if (dataURL.indexOf(BASE64_MARKER) >= 0) { diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts index 518d8cd79..29c9abe68 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -1,46 +1,47 @@ -import { combineReducers } from 'redux-immutable'; -import dropdown_menu from './dropdown_menu'; -import timelines from './timelines'; -import meta from './meta'; -import alerts from './alerts'; import { loadingBarReducer } from 'react-redux-loading-bar'; -import modal from './modal'; -import user_lists from './user_lists'; -import domain_lists from './domain_lists'; +import { combineReducers } from 'redux-immutable'; + import accounts from './accounts'; import accounts_counters from './accounts_counters'; -import statuses from './statuses'; -import relationships from './relationships'; -import settings from './settings'; -import push_notifications from './push_notifications'; -import status_lists from './status_lists'; -import mutes from './mutes'; +import accounts_map from './accounts_map'; +import alerts from './alerts'; +import announcements from './announcements'; import blocks from './blocks'; import boosts from './boosts'; -import server from './server'; -import contexts from './contexts'; import compose from './compose'; -import search from './search'; -import media_attachments from './media_attachments'; -import notifications from './notifications'; -import height_cache from './height_cache'; -import custom_emojis from './custom_emojis'; -import lists from './lists'; -import listEditor from './list_editor'; -import listAdder from './list_adder'; -import filters from './filters'; +import contexts from './contexts'; import conversations from './conversations'; -import suggestions from './suggestions'; -import polls from './polls'; -import trends from './trends'; -import { missedUpdatesReducer } from './missed_updates'; -import announcements from './announcements'; -import markers from './markers'; -import picture_in_picture from './picture_in_picture'; -import accounts_map from './accounts_map'; -import history from './history'; -import tags from './tags'; +import custom_emojis from './custom_emojis'; +import domain_lists from './domain_lists'; +import dropdown_menu from './dropdown_menu'; +import filters from './filters'; import followed_tags from './followed_tags'; +import height_cache from './height_cache'; +import history from './history'; +import listAdder from './list_adder'; +import listEditor from './list_editor'; +import lists from './lists'; +import markers from './markers'; +import media_attachments from './media_attachments'; +import meta from './meta'; +import { missedUpdatesReducer } from './missed_updates'; +import modal from './modal'; +import mutes from './mutes'; +import notifications from './notifications'; +import picture_in_picture from './picture_in_picture'; +import polls from './polls'; +import push_notifications from './push_notifications'; +import relationships from './relationships'; +import search from './search'; +import server from './server'; +import settings from './settings'; +import status_lists from './status_lists'; +import statuses from './statuses'; +import suggestions from './suggestions'; +import tags from './tags'; +import timelines from './timelines'; +import trends from './trends'; +import user_lists from './user_lists'; const reducers = { announcements, diff --git a/app/javascript/mastodon/reducers/markers.js b/app/javascript/mastodon/reducers/markers.js index e3d1b1936..3e8b1780a 100644 --- a/app/javascript/mastodon/reducers/markers.js +++ b/app/javascript/mastodon/reducers/markers.js @@ -2,13 +2,13 @@ import { MARKERS_SUBMIT_SUCCESS, } from '../actions/markers'; +import { Map as ImmutableMap } from 'immutable'; + const initialState = ImmutableMap({ home: '0', notifications: '0', }); -import { Map as ImmutableMap } from 'immutable'; - export default function markers(state = initialState, action) { switch(action.type) { case MARKERS_SUBMIT_SUCCESS: diff --git a/app/javascript/mastodon/reducers/missed_updates.ts b/app/javascript/mastodon/reducers/missed_updates.ts index 9c1a5cbd2..a587fcb03 100644 --- a/app/javascript/mastodon/reducers/missed_updates.ts +++ b/app/javascript/mastodon/reducers/missed_updates.ts @@ -1,12 +1,14 @@ import { Record } from 'immutable'; -import type { Action } from 'redux'; -import { NOTIFICATIONS_UPDATE } from '../actions/notifications'; -import { focusApp, unfocusApp } from '../actions/app'; -type MissedUpdatesState = { +import type { Action } from 'redux'; + +import { focusApp, unfocusApp } from '../actions/app'; +import { NOTIFICATIONS_UPDATE } from '../actions/notifications'; + +interface MissedUpdatesState { focused: boolean; unread: number; -}; +} const initialState = Record({ focused: true, unread: 0, diff --git a/app/javascript/mastodon/store/index.ts b/app/javascript/mastodon/store/index.ts index 6c3e963d9..1ef9cb818 100644 --- a/app/javascript/mastodon/store/index.ts +++ b/app/javascript/mastodon/store/index.ts @@ -1,14 +1,32 @@ +import type { TypedUseSelectorHook } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; + import { configureStore } from '@reduxjs/toolkit'; + import { rootReducer } from '../reducers'; -import { loadingBarMiddleware } from './middlewares/loading_bar'; + import { errorsMiddleware } from './middlewares/errors'; +import { loadingBarMiddleware } from './middlewares/loading_bar'; import { soundsMiddleware } from './middlewares/sounds'; -import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; export const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => - getDefaultMiddleware() + getDefaultMiddleware({ + // In development, Redux Toolkit enables 2 default middlewares to detect + // common issues with states. Unfortunately, our use of ImmutableJS for state + // triggers both, so lets disable them until our state is fully refactored + + // https://redux-toolkit.js.org/api/serializabilityMiddleware + // This checks recursively that every values in the state are serializable in JSON + // Which is not the case, as we use ImmutableJS structures, but also File objects + serializableCheck: false, + + // https://redux-toolkit.js.org/api/immutabilityMiddleware + // This checks recursively if every value in the state is immutable (ie, a JS primitive type) + // But this is not the case, as our Root State is an ImmutableJS map, which is an object + immutableCheck: false, + }) .concat( loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'], diff --git a/app/javascript/mastodon/store/middlewares/errors.ts b/app/javascript/mastodon/store/middlewares/errors.ts index a5e99d04e..4e720bfed 100644 --- a/app/javascript/mastodon/store/middlewares/errors.ts +++ b/app/javascript/mastodon/store/middlewares/errors.ts @@ -1,17 +1,18 @@ -import { Middleware } from 'redux'; +import type { AnyAction, Middleware } from 'redux'; + +import type { RootState } from '..'; import { showAlertForError } from '../../actions/alerts'; -import { RootState } from '..'; const defaultFailSuffix = 'FAIL'; export const errorsMiddleware: Middleware, RootState> = ({ dispatch }) => (next) => - (action) => { + (action: AnyAction & { skipAlert?: boolean; skipNotFound?: boolean }) => { if (action.type && !action.skipAlert) { const isFail = new RegExp(`${defaultFailSuffix}$`, 'g'); - if (action.type.match(isFail)) { + if (typeof action.type === 'string' && action.type.match(isFail)) { dispatch(showAlertForError(action.error, action.skipNotFound)); } } diff --git a/app/javascript/mastodon/store/middlewares/loading_bar.ts b/app/javascript/mastodon/store/middlewares/loading_bar.ts index 183c0cf9d..0f997fd34 100644 --- a/app/javascript/mastodon/store/middlewares/loading_bar.ts +++ b/app/javascript/mastodon/store/middlewares/loading_bar.ts @@ -1,6 +1,7 @@ import { showLoading, hideLoading } from 'react-redux-loading-bar'; -import { Middleware } from 'redux'; -import { RootState } from '..'; +import type { AnyAction, Middleware } from 'redux'; + +import type { RootState } from '..'; interface Config { promiseTypeSuffixes?: string[]; @@ -19,7 +20,7 @@ export const loadingBarMiddleware = ( return ({ dispatch }) => (next) => - (action) => { + (action: AnyAction) => { if (action.type && !action.skipLoading) { const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes; @@ -27,13 +28,15 @@ export const loadingBarMiddleware = ( const isFulfilled = new RegExp(`${FULFILLED}$`, 'g'); const isRejected = new RegExp(`${REJECTED}$`, 'g'); - if (action.type.match(isPending)) { - dispatch(showLoading()); - } else if ( - action.type.match(isFulfilled) || - action.type.match(isRejected) - ) { - dispatch(hideLoading()); + if (typeof action.type === 'string') { + if (action.type.match(isPending)) { + dispatch(showLoading()); + } else if ( + action.type.match(isFulfilled) || + action.type.match(isRejected) + ) { + dispatch(hideLoading()); + } } } diff --git a/app/javascript/mastodon/store/middlewares/sounds.ts b/app/javascript/mastodon/store/middlewares/sounds.ts index e7c87df7e..6005d3649 100644 --- a/app/javascript/mastodon/store/middlewares/sounds.ts +++ b/app/javascript/mastodon/store/middlewares/sounds.ts @@ -1,5 +1,6 @@ -import { Middleware, AnyAction } from 'redux'; -import { RootState } from '..'; +import type { Middleware, AnyAction } from 'redux'; + +import type { RootState } from '..'; interface AudioSource { src: string; @@ -27,7 +28,7 @@ const play = (audio: HTMLAudioElement) => { } } - audio.play(); + void audio.play(); }; export const soundsMiddleware = (): Middleware< @@ -47,13 +48,15 @@ export const soundsMiddleware = (): Middleware< ]), }; - return () => (next) => (action: AnyAction) => { - const sound = action?.meta?.sound; + return () => + (next) => + (action: AnyAction & { meta?: { sound?: string } }) => { + const sound = action?.meta?.sound; - if (sound && soundCache[sound]) { - play(soundCache[sound]); - } + if (sound && soundCache[sound]) { + play(soundCache[sound]); + } - return next(action); - }; + return next(action); + }; }; diff --git a/app/javascript/mastodon/utils/__tests__/html-test.js b/app/javascript/mastodon/utils/__tests__/html-test.js index ef9296e6c..d948cf4c5 100644 --- a/app/javascript/mastodon/utils/__tests__/html-test.js +++ b/app/javascript/mastodon/utils/__tests__/html-test.js @@ -1,7 +1,7 @@ import * as html from '../html'; describe('html', () => { - describe('unsecapeHTML', () => { + describe('unescapeHTML', () => { it('returns unescaped HTML', () => { const output = html.unescapeHTML('

lorem

ipsum


<br>'); expect(output).toEqual('lorem\n\nipsum\n
'); diff --git a/app/javascript/mastodon/uuid.ts b/app/javascript/mastodon/uuid.ts index 6cadbd6bb..0b4d55beb 100644 --- a/app/javascript/mastodon/uuid.ts +++ b/app/javascript/mastodon/uuid.ts @@ -1,8 +1,9 @@ export function uuid(a?: string): string { return a ? ( - (a as any as number) ^ - ((Math.random() * 16) >> ((a as any as number) / 4)) + (a as unknown as number) ^ + ((Math.random() * 16) >> ((a as unknown as number) / 4)) ).toString(16) - : ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid); + : // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid); } diff --git a/app/javascript/packs/admin.jsx b/app/javascript/packs/admin.jsx index 52311af50..1e02a7b96 100644 --- a/app/javascript/packs/admin.jsx +++ b/app/javascript/packs/admin.jsx @@ -1,7 +1,7 @@ import './public-path'; import ready from '../mastodon/ready'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; ready(() => { [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => { @@ -10,11 +10,13 @@ ready(() => { import('../mastodon/containers/admin_component').then(({ default: AdminComponent }) => { return import('../mastodon/components/admin/' + componentName).then(({ default: Component }) => { - ReactDOM.render(( + const root = createRoot(element); + + root.render ( - - ), element); + , + ); }); }).catch(error => { console.error(error); diff --git a/app/javascript/packs/public.jsx b/app/javascript/packs/public.jsx index c8c3bd31e..26409d4f4 100644 --- a/app/javascript/packs/public.jsx +++ b/app/javascript/packs/public.jsx @@ -15,7 +15,7 @@ import { delegate } from '@rails/ujs'; import emojify from '../mastodon/features/emoji/emoji'; import { getLocale } from '../mastodon/locales'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { createBrowserHistory } from 'history'; start(); @@ -137,7 +137,8 @@ function loaded() { const content = document.createElement('div'); - ReactDOM.render(, content); + const root = createRoot(content); + root.render(); document.body.appendChild(content); scrollToDetailedStatus(); }) diff --git a/app/javascript/packs/share.jsx b/app/javascript/packs/share.jsx index 542a2f3ae..6a87eccda 100644 --- a/app/javascript/packs/share.jsx +++ b/app/javascript/packs/share.jsx @@ -4,7 +4,7 @@ import { start } from '../mastodon/common'; import ready from '../mastodon/ready'; import ComposeContainer from '../mastodon/containers/compose_container'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; start(); @@ -16,7 +16,8 @@ function loaded() { if(!attr) return; const props = JSON.parse(attr); - ReactDOM.render(, mountNode); + const root = createRoot(mountNode); + root.render(); } } diff --git a/app/javascript/packs/sign_up.js b/app/javascript/packs/sign_up.js new file mode 100644 index 000000000..260e404eb --- /dev/null +++ b/app/javascript/packs/sign_up.js @@ -0,0 +1,15 @@ +import './public-path'; +import ready from '../mastodon/ready'; +import axios from 'axios'; + +ready(() => { + setInterval(() => { + axios.get('/api/v1/emails/check_confirmation').then((response) => { + if (response.data) { + window.location = '/start'; + } + }).catch(error => { + console.error(error); + }); + }, 5000); +}); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 4c36411a3..0ce1c1eae 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3118,7 +3118,7 @@ $ui-header-height: 55px; &.active { transition: none; - box-shadow: 0 0 0 2px rgba(lighten($highlight-text-color, 8%), 0.7); + box-shadow: 0 0 0 6px rgba(lighten($highlight-text-color, 8%), 0.7); } } @@ -6447,13 +6447,6 @@ a.status-card.compact:hover { &--wide { grid-column: span 2; } - - &.standalone { - .media-gallery__item-gifv-thumbnail { - transform: none; - top: 0; - } - } } .media-gallery__item-thumbnail { @@ -6501,11 +6494,7 @@ a.status-card.compact:hover { cursor: zoom-in; height: 100%; object-fit: cover; - position: relative; - top: 50%; - transform: translateY(-50%); width: 100%; - z-index: 1; } .media-gallery__item-thumbnail-label { @@ -6604,6 +6593,8 @@ a.status-card.compact:hover { border-radius: 4px; box-sizing: border-box; color: $white; + display: flex; + align-items: center; &.editable { border-radius: 0; @@ -6638,9 +6629,6 @@ a.status-card.compact:hover { &.inline { video { object-fit: contain; - position: relative; - top: 50%; - transform: translateY(-50%); } } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 03e06c100..57f077c4e 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -136,6 +136,10 @@ code { line-height: 22px; color: $secondary-text-color; margin-bottom: 30px; + + a { + color: $highlight-text-color; + } } .rules-list { @@ -1039,6 +1043,10 @@ code { } } +.simple_form .h-captcha { + text-align: center; +} + .permissions-list { &__item { padding: 15px; diff --git a/app/javascript/types/image.d.ts b/app/javascript/types/image.d.ts index fae2ed701..15f0007af 100644 --- a/app/javascript/types/image.d.ts +++ b/app/javascript/types/image.d.ts @@ -14,11 +14,6 @@ declare module '*.jpg' { export default path; } -declare module '*.jpg' { - const path: string; - export default path; -} - declare module '*.png' { const path: string; export default path; diff --git a/app/javascript/types/resources.ts b/app/javascript/types/resources.ts index 090650415..63ec2993b 100644 --- a/app/javascript/types/resources.ts +++ b/app/javascript/types/resources.ts @@ -12,7 +12,7 @@ type AccountField = Record<{ verified_at: string | null; }>; -type AccountApiResponseValues = { +interface AccountApiResponseValues { acct: string; avatar: string; avatar_static: string; @@ -34,7 +34,7 @@ type AccountApiResponseValues = { statuses_count: number; url: string; username: string; -}; +} type NormalizedAccountField = Record<{ name_emojified: string; @@ -42,12 +42,12 @@ type NormalizedAccountField = Record<{ value_plain: string; }>; -type NormalizedAccountValues = { +interface NormalizedAccountValues { display_name_html: string; fields: NormalizedAccountField[]; note_emojified: string; note_plain: string; -}; +} export type Account = Record< AccountApiResponseValues & NormalizedAccountValues diff --git a/app/lib/account_reach_finder.rb b/app/lib/account_reach_finder.rb index 706ce8c1f..481e25439 100644 --- a/app/lib/account_reach_finder.rb +++ b/app/lib/account_reach_finder.rb @@ -6,7 +6,7 @@ class AccountReachFinder end def inboxes - (followers_inboxes + reporters_inboxes + relay_inboxes).uniq + (followers_inboxes + reporters_inboxes + recently_mentioned_inboxes + relay_inboxes).uniq end private @@ -19,6 +19,13 @@ class AccountReachFinder Account.where(id: @account.targeted_reports.select(:account_id)).inboxes end + def recently_mentioned_inboxes + cutoff_id = Mastodon::Snowflake.id_at(2.days.ago, with_random: false) + recent_statuses = @account.statuses.recent.where(id: cutoff_id...).limit(200) + + Account.joins(:mentions).where(mentions: { status: recent_statuses }).inboxes.take(2000) + end + def relay_inboxes Relay.enabled.pluck(:inbox_url) end diff --git a/app/lib/activitypub/activity/flag.rb b/app/lib/activitypub/activity/flag.rb index dc808ad36..dc1932f59 100644 --- a/app/lib/activitypub/activity/flag.rb +++ b/app/lib/activitypub/activity/flag.rb @@ -16,7 +16,7 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity @account, target_account, status_ids: target_statuses.nil? ? [] : target_statuses.map(&:id), - comment: @json['content'] || '', + comment: report_comment, uri: report_uri ) end @@ -35,4 +35,8 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity def report_uri @json['id'] unless @json['id'].nil? || non_matching_uri_hosts?(@account.uri, @json['id']) end + + def report_comment + (@json['content'] || '')[0...5000] + end end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index a65a9565a..864328631 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -28,6 +28,8 @@ class ActivityPub::TagManager return activity_account_status_url(target.account, target) if target.reblog? short_account_status_url(target.account, target) + when :flag + target.uri end end @@ -43,6 +45,8 @@ class ActivityPub::TagManager account_status_url(target.account, target) when :emoji emoji_url(target) + when :flag + target.uri end end diff --git a/app/lib/admin/metrics/dimension.rb b/app/lib/admin/metrics/dimension.rb index 81b89d9b3..e0122a65b 100644 --- a/app/lib/admin/metrics/dimension.rb +++ b/app/lib/admin/metrics/dimension.rb @@ -14,9 +14,9 @@ class Admin::Metrics::Dimension }.freeze def self.retrieve(dimension_keys, start_at, end_at, limit, params) - Array(dimension_keys).map do |key| + Array(dimension_keys).filter_map do |key| klass = DIMENSIONS[key.to_sym] klass&.new(start_at, end_at, limit, klass.with_params? ? params.require(key.to_sym) : nil) - end.compact + end end end diff --git a/app/lib/admin/metrics/measure.rb b/app/lib/admin/metrics/measure.rb index 0b510eb25..fe7e04929 100644 --- a/app/lib/admin/metrics/measure.rb +++ b/app/lib/admin/metrics/measure.rb @@ -19,9 +19,9 @@ class Admin::Metrics::Measure }.freeze def self.retrieve(measure_keys, start_at, end_at, params) - Array(measure_keys).map do |key| + Array(measure_keys).filter_map do |key| klass = MEASURES[key.to_sym] klass&.new(start_at, end_at, klass.with_params? ? params.require(key.to_sym) : nil) - end.compact + end end end diff --git a/app/lib/application_extension.rb b/app/lib/application_extension.rb index d61ec0e6e..4de69c1ea 100644 --- a/app/lib/application_extension.rb +++ b/app/lib/application_extension.rb @@ -9,10 +9,6 @@ module ApplicationExtension validates :redirect_uri, length: { maximum: 2_000 } end - def most_recently_used_access_token - @most_recently_used_access_token ||= access_tokens.where.not(last_used_at: nil).order(last_used_at: :desc).first - end - def confirmation_redirect_uri redirect_uri.lines.first.strip end diff --git a/app/lib/extractor.rb b/app/lib/extractor.rb index 540bbe1a9..9090773ae 100644 --- a/app/lib/extractor.rb +++ b/app/lib/extractor.rb @@ -64,7 +64,7 @@ module Extractor end_position = match_data.char_end(1) after = ::Regexp.last_match.post_match - if %r{\A://}.match?(after) + if after.start_with?('://') hash_text.match(/(.+)(https?\Z)/) do |matched| hash_text = matched[1] end_position -= matched[2].codepoint_length diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index e98ae2d70..3cf6e21c3 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -213,7 +213,7 @@ class FeedManager timeline_key = key(:home, account.id) timeline_status_ids = redis.zrange(timeline_key, 0, -1) statuses = Status.where(id: timeline_status_ids).select(:id, :reblog_of_id, :account_id).to_a - reblogged_ids = Status.where(id: statuses.map(&:reblog_of_id).compact, account: target_account).pluck(:id) + reblogged_ids = Status.where(id: statuses.filter_map(&:reblog_of_id), account: target_account).pluck(:id) with_mentions_ids = Mention.active.where(status_id: statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact, account: target_account).pluck(:status_id) target_statuses = statuses.select do |status| @@ -233,7 +233,7 @@ class FeedManager timeline_key = key(:list, list.id) timeline_status_ids = redis.zrange(timeline_key, 0, -1) statuses = Status.where(id: timeline_status_ids).select(:id, :reblog_of_id, :account_id).to_a - reblogged_ids = Status.where(id: statuses.map(&:reblog_of_id).compact, account: target_account).pluck(:id) + reblogged_ids = Status.where(id: statuses.filter_map(&:reblog_of_id), account: target_account).pluck(:id) with_mentions_ids = Mention.active.where(status_id: statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact, account: target_account).pluck(:status_id) target_statuses = statuses.select do |status| @@ -603,9 +603,9 @@ class FeedManager arr end - crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:in_reply_to_account_id).compact).pluck(:target_account_id).index_with(true) + crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:target_account_id).index_with(true) crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h - crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.map { |s| s.account_id if s.reblog? }.compact, show_reblogs: false).pluck(:target_account_id).index_with(true) + crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true) crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true) diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb index f8a0be636..dfed69285 100644 --- a/app/lib/link_details_extractor.rb +++ b/app/lib/link_details_extractor.rb @@ -140,7 +140,7 @@ class LinkDetailsExtractor end def html - player_url.present? ? content_tag(:iframe, nil, src: player_url, width: width, height: height, allowtransparency: 'true', scrolling: 'no', frameborder: '0') : nil + player_url.present? ? content_tag(:iframe, nil, src: player_url, width: width, height: height, allowfullscreen: 'true', allowtransparency: 'true', scrolling: 'no', frameborder: '0') : nil end def width diff --git a/app/lib/vacuum/access_tokens_vacuum.rb b/app/lib/vacuum/access_tokens_vacuum.rb index 7b91f74a5..a224f6d63 100644 --- a/app/lib/vacuum/access_tokens_vacuum.rb +++ b/app/lib/vacuum/access_tokens_vacuum.rb @@ -9,10 +9,12 @@ class Vacuum::AccessTokensVacuum private def vacuum_revoked_access_tokens! - Doorkeeper::AccessToken.where.not(revoked_at: nil).where('revoked_at < NOW()').delete_all + Doorkeeper::AccessToken.where.not(expires_in: nil).where('created_at + make_interval(secs => expires_in) < NOW()').in_batches.delete_all + Doorkeeper::AccessToken.where.not(revoked_at: nil).where('revoked_at < NOW()').in_batches.delete_all end def vacuum_revoked_access_grants! - Doorkeeper::AccessGrant.where.not(revoked_at: nil).where('revoked_at < NOW()').delete_all + Doorkeeper::AccessGrant.where.not(expires_in: nil).where('created_at + make_interval(secs => expires_in) < NOW()').in_batches.delete_all + Doorkeeper::AccessGrant.where.not(revoked_at: nil).where('revoked_at < NOW()').in_batches.delete_all end end diff --git a/app/models/account.rb b/app/models/account.rb index 30af67615..bbc9bda02 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -299,11 +299,11 @@ class Account < ApplicationRecord end def fields - (self[:fields] || []).map do |f| + (self[:fields] || []).filter_map do |f| Account::Field.new(self, f) rescue nil - end.compact + end end def fields_attributes=(attributes) diff --git a/app/models/account_statuses_cleanup_policy.rb b/app/models/account_statuses_cleanup_policy.rb index 63582b9f9..a10279544 100644 --- a/app/models/account_statuses_cleanup_policy.rb +++ b/app/models/account_statuses_cleanup_policy.rb @@ -117,12 +117,12 @@ class AccountStatusesCleanupPolicy < ApplicationRecord private def update_last_inspected - if EXCEPTION_BOOLS.map { |name| attribute_change_to_be_saved(name) }.compact.include?([true, false]) + if EXCEPTION_BOOLS.filter_map { |name| attribute_change_to_be_saved(name) }.include?([true, false]) # Policy has been widened in such a way that any previously-inspected status # may need to be deleted, so we'll have to start again. redis.del("account_cleanup:#{account_id}") end - redis.del("account_cleanup:#{account_id}") if EXCEPTION_THRESHOLDS.map { |name| attribute_change_to_be_saved(name) }.compact.any? { |old, new| old.present? && (new.nil? || new > old) } + redis.del("account_cleanup:#{account_id}") if EXCEPTION_THRESHOLDS.filter_map { |name| attribute_change_to_be_saved(name) }.any? { |old, new| old.present? && (new.nil? || new > old) } end def validate_local_account diff --git a/app/models/account_suggestions/setting_source.rb b/app/models/account_suggestions/setting_source.rb index 7b8873e0c..6185732b4 100644 --- a/app/models/account_suggestions/setting_source.rb +++ b/app/models/account_suggestions/setting_source.rb @@ -48,14 +48,14 @@ class AccountSuggestions::SettingSource < AccountSuggestions::Source end def setting_to_usernames_and_domains - setting.split(',').map do |str| + setting.split(',').filter_map do |str| username, domain = str.strip.gsub(/\A@/, '').split('@', 2) domain = nil if TagManager.instance.local_domain?(domain) next if username.blank? [username.downcase, domain&.downcase] - end.compact + end end def setting diff --git a/app/models/account_suggestions/source.rb b/app/models/account_suggestions/source.rb index be462cd0f..504d26a8b 100644 --- a/app/models/account_suggestions/source.rb +++ b/app/models/account_suggestions/source.rb @@ -20,7 +20,7 @@ class AccountSuggestions::Source map = scope.index_by { |account| to_ordered_list_key(account) } - ordered_list.map { |ordered_list_key| map[ordered_list_key] }.compact.map do |account| + ordered_list.filter_map { |ordered_list_key| map[ordered_list_key] }.map do |account| AccountSuggestions::Suggestion.new( account: account, source: key diff --git a/app/models/follow_recommendation_filter.rb b/app/models/follow_recommendation_filter.rb index 531332614..2fab97569 100644 --- a/app/models/follow_recommendation_filter.rb +++ b/app/models/follow_recommendation_filter.rb @@ -22,7 +22,7 @@ class FollowRecommendationFilter account_ids = redis.zrevrange("follow_recommendations:#{@language}", 0, -1).map(&:to_i) accounts = Account.where(id: account_ids).index_by(&:id) - account_ids.map { |id| accounts[id] }.compact + account_ids.filter_map { |id| accounts[id] } end end end diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index 6a05f8163..4665a5867 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -123,7 +123,18 @@ class Form::AccountBatch account: current_account, action: :suspend ) + Admin::SuspensionWorker.perform_async(account.id) + + # Suspending a single account closes their associated reports, so + # mass-suspending would be consistent. + Report.where(target_account: account).unresolved.find_each do |report| + authorize(report, :update?) + log_action(:resolve, report) + report.resolve!(current_account) + rescue Mastodon::NotPermittedError + # This should not happen, but just in case, do not fail early + end end def approve_account(account) diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index eaee142fa..8f1dc4954 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -41,6 +41,7 @@ class Form::AdminSettings content_cache_retention_period backups_retention_period status_page_url + captcha_enabled ).freeze INTEGER_KEYS = %i( diff --git a/app/models/notification.rb b/app/models/notification.rb index 8ba506fa1..5527953af 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -114,7 +114,7 @@ class Notification < ApplicationRecord ActiveRecord::Associations::Preloader.new.preload(grouped_notifications, associations) end - unique_target_statuses = notifications.map(&:target_status).compact.uniq + unique_target_statuses = notifications.filter_map(&:target_status).uniq # Call cache_collection in block cached_statuses_by_id = yield(unique_target_statuses).index_by(&:id) diff --git a/app/models/report.rb b/app/models/report.rb index c3a0c4c8b..e738281ad 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -40,7 +40,10 @@ class Report < ApplicationRecord scope :resolved, -> { where.not(action_taken_at: nil) } scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with({ user: [:invite_request, :invite] })) } - validates :comment, length: { maximum: 1_000 } + # A report is considered local if the reporter is local + delegate :local?, to: :account + + validates :comment, length: { maximum: 1_000 }, if: :local? validates :rule_ids, absence: true, unless: :violation? validate :validate_rule_ids @@ -51,10 +54,6 @@ class Report < ApplicationRecord violation: 2_000, } - def local? - false # Force uri_for to use uri attribute - end - before_validation :set_uri, only: :create after_create_commit :trigger_webhooks diff --git a/app/models/user_role.rb b/app/models/user_role.rb index a1b91dc0f..5472646c6 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -125,7 +125,7 @@ class UserRole < ApplicationRecord end def permissions_as_keys=(value) - self.permissions = value.map(&:presence).compact.reduce(Flags::NONE) { |bitmask, privilege| FLAGS.key?(privilege.to_sym) ? (bitmask | FLAGS[privilege.to_sym]) : bitmask } + self.permissions = value.filter_map(&:presence).reduce(Flags::NONE) { |bitmask, privilege| FLAGS.key?(privilege.to_sym) ? (bitmask | FLAGS[privilege.to_sym]) : bitmask } end def can?(*any_of_privileges) diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 9a056a386..c556bcc2b 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -53,7 +53,7 @@ class Webhook < ApplicationRecord end def strip_events - self.events = events.map { |str| str.strip.presence }.compact if events.present? + self.events = events.filter_map { |str| str.strip.presence } if events.present? end def generate_secret diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 89b8d06cd..5a99eb50c 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -101,8 +101,8 @@ class BackupService < BaseService actor[:likes] = 'likes.json' actor[:bookmarks] = 'bookmarks.json' - download_to_zip(tar, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists? - download_to_zip(tar, account.header, "header#{File.extname(account.header.path)}") if account.header.exists? + download_to_zip(zipfile, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists? + download_to_zip(zipfile, account.header, "header#{File.extname(account.header.path)}") if account.header.exists? json = Oj.dump(actor) diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index b3b279147..f3fbb8021 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -68,7 +68,7 @@ class ProcessMentionsService < BaseService def assign_mentions! # Make sure we never mention blocked accounts unless @current_mentions.empty? - mentioned_domains = @current_mentions.map { |m| m.account.domain }.compact.uniq + mentioned_domains = @current_mentions.filter_map { |m| m.account.domain }.uniq blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains)) mentioned_account_ids = @current_mentions.map(&:account_id) blocked_account_ids = Set.new(@status.account.block_relationships.where(target_account_id: mentioned_account_ids).pluck(:target_account_id)) diff --git a/app/validators/existing_username_validator.rb b/app/validators/existing_username_validator.rb index 45de4f4a4..037d92f39 100644 --- a/app/validators/existing_username_validator.rb +++ b/app/validators/existing_username_validator.rb @@ -4,14 +4,14 @@ class ExistingUsernameValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) return if value.blank? - usernames_and_domains = value.split(',').map do |str| + usernames_and_domains = value.split(',').filter_map do |str| username, domain = str.strip.gsub(/\A@/, '').split('@', 2) domain = nil if TagManager.instance.local_domain?(domain) next if username.blank? [str, username, domain] - end.compact + end usernames_with_no_accounts = usernames_and_domains.filter_map do |(str, username, domain)| str unless Account.find_remote(username, domain) diff --git a/app/validators/vote_validator.rb b/app/validators/vote_validator.rb index 9bd17fbe8..fa2bd223d 100644 --- a/app/validators/vote_validator.rb +++ b/app/validators/vote_validator.rb @@ -3,8 +3,8 @@ class VoteValidator < ActiveModel::Validator def validate(vote) vote.errors.add(:base, I18n.t('polls.errors.expired')) if vote.poll_expired? - vote.errors.add(:base, I18n.t('polls.errors.invalid_choice')) if invalid_choice?(vote) + vote.errors.add(:base, I18n.t('polls.errors.self_vote')) if self_vote?(vote) vote.errors.add(:base, I18n.t('polls.errors.already_voted')) if additional_voting_not_allowed?(vote) end @@ -27,6 +27,10 @@ class VoteValidator < ActiveModel::Validator vote.choice.negative? || vote.choice >= vote.poll.options.size end + def self_vote?(vote) + vote.account_id == vote.poll.account_id + end + def already_voted_for_same_choice_on_multiple_poll?(vote) if vote.persisted? account_votes_on_same_poll(vote).where(choice: vote.choice).where.not(poll_votes: { id: vote }).exists? diff --git a/app/views/admin/reports/_media_attachments.html.haml b/app/views/admin/reports/_media_attachments.html.haml index d0b7d52c3..2305805a7 100644 --- a/app/views/admin/reports/_media_attachments.html.haml +++ b/app/views/admin/reports/_media_attachments.html.haml @@ -1,8 +1,8 @@ - if status.ordered_media_attachments.first.video? - video = status.ordered_media_attachments.first - = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), frameRate: video.file.meta.dig('original', 'frame_rate'), blurhash: video.blurhash, sensitive: status.sensitive?, visible: false, width: 610, height: 343, inline: true, alt: video.description, media: [ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer)].as_json + = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), frameRate: video.file.meta.dig('original', 'frame_rate'), blurhash: video.blurhash, sensitive: status.sensitive?, visible: false, width: 610, height: 343, inline: true, alt: video.description, lang: status.language, media: [ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer)].as_json - elsif status.ordered_media_attachments.first.audio? - audio = status.ordered_media_attachments.first - = react_component :audio, src: audio.file.url(:original), height: 110, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) + = react_component :audio, src: audio.file.url(:original), height: 110, alt: audio.description, lang: status.language, duration: audio.file.meta.dig(:original, :duration) - else - = react_component :media_gallery, height: 343, sensitive: status.sensitive?, visible: false, media: status.ordered_media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } + = react_component :media_gallery, height: 343, sensitive: status.sensitive?, visible: false, lang: status.language, media: status.ordered_media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml index 455fa5eca..443a219fc 100644 --- a/app/views/admin/settings/registrations/show.html.haml +++ b/app/views/admin/settings/registrations/show.html.haml @@ -19,7 +19,7 @@ - if captcha_available? .fields-group - = f.input :captcha_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.captcha_enabled.title'), hint: t('admin.settings.captcha_enabled.desc_html'), glitch_only: true + = f.input :captcha_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.captcha_enabled.title'), hint: t('admin.settings.captcha_enabled.desc_html') .fields-group = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, input_html: { rows: 2 } diff --git a/app/views/auth/confirmations/captcha.html.haml b/app/views/auth/confirmations/captcha.html.haml index 1f577383e..77f4b35b4 100644 --- a/app/views/auth/confirmations/captcha.html.haml +++ b/app/views/auth/confirmations/captcha.html.haml @@ -5,6 +5,7 @@ = render 'auth/shared/progress', stage: 'confirm' = hidden_field_tag :confirmation_token, params[:confirmation_token] + = hidden_field_tag :redirect_to_app, params[:redirect_to_app] %p.lead= t('auth.captcha_confirmation.hint_html') diff --git a/app/views/oauth/authorized_applications/index.html.haml b/app/views/oauth/authorized_applications/index.html.haml index 0280d8aef..55d8524db 100644 --- a/app/views/oauth/authorized_applications/index.html.haml +++ b/app/views/oauth/authorized_applications/index.html.haml @@ -18,8 +18,8 @@ .announcements-list__item__action-bar .announcements-list__item__meta - - if application.most_recently_used_access_token - = t('doorkeeper.authorized_applications.index.last_used_at', date: l(application.most_recently_used_access_token.last_used_at.to_date)) + - if @last_used_at_by_app[application.id] + = t('doorkeeper.authorized_applications.index.last_used_at', date: l(@last_used_at_by_app[application.id].to_date)) - else = t('doorkeeper.authorized_applications.index.never_used') diff --git a/app/workers/post_process_media_worker.rb b/app/workers/post_process_media_worker.rb index 996d5def9..2d11b00c2 100644 --- a/app/workers/post_process_media_worker.rb +++ b/app/workers/post_process_media_worker.rb @@ -24,7 +24,7 @@ class PostProcessMediaWorker media_attachment.processing = :in_progress media_attachment.save - # Because paperclip-av-transcover overwrites this attribute + # Because paperclip-av-transcoder overwrites this attribute # we will save it here and restore it after reprocess is done previous_meta = media_attachment.file_meta diff --git a/config/initializers/ffmpeg.rb b/config/initializers/ffmpeg.rb index 4c0bf779d..cd5914eb5 100644 --- a/config/initializers/ffmpeg.rb +++ b/config/initializers/ffmpeg.rb @@ -1,3 +1,3 @@ if ENV['FFMPEG_BINARY'].present? - FFMPEG.ffmpeg_binary = ENV['FFMPEG_BINARY'] + FFMPEG.ffmpeg_binary = ENV['FFMPEG_BINARY'] end diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index f01670146..c2cd444f0 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -73,7 +73,7 @@ Devise.setup do |config| oidc_options[:display_name] = ENV['OIDC_DISPLAY_NAME'] #OPTIONAL oidc_options[:issuer] = ENV['OIDC_ISSUER'] if ENV['OIDC_ISSUER'] #NEED oidc_options[:discovery] = ENV['OIDC_DISCOVERY'] == 'true' if ENV['OIDC_DISCOVERY'] #OPTIONAL (default: false) - oidc_options[:client_auth_method] = ENV['OIDC_CLIENT_AUTH_METHOD'] if ENV['OIDC_CLIENT_AUTH_METHOD'] #OPTIONAL (default: basic) + oidc_options[:client_auth_method] = ENV['OIDC_CLIENT_AUTH_METHOD'] if ENV['OIDC_CLIENT_AUTH_METHOD'] #OPTIONAL (default: basic) scope_string = ENV['OIDC_SCOPE'] if ENV['OIDC_SCOPE'] #NEED scopes = scope_string.split(',') oidc_options[:scope] = scopes.map { |x| x.to_sym } diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 9f0ffc6dc..093d2ba9a 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -61,13 +61,13 @@ if ENV['S3_ENABLED'] == 'true' s3_options: { signature_version: ENV.fetch('S3_SIGNATURE_VERSION') { 'v4' }, - http_open_timeout: ENV.fetch('S3_OPEN_TIMEOUT'){ '5' }.to_i, - http_read_timeout: ENV.fetch('S3_READ_TIMEOUT'){ '5' }.to_i, + http_open_timeout: ENV.fetch('S3_OPEN_TIMEOUT') { '5' }.to_i, + http_read_timeout: ENV.fetch('S3_READ_TIMEOUT') { '5' }.to_i, http_idle_timeout: 5, retry_limit: 0, } ) - + Paperclip::Attachment.default_options[:s3_permissions] = ->(*) { nil } if ENV['S3_PERMISSION'] == '' if ENV.has_key?('S3_ENDPOINT') @@ -124,7 +124,7 @@ elsif ENV['SWIFT_ENABLED'] == 'true' openstack_cache_ttl: ENV.fetch('SWIFT_CACHE_TTL') { 60 }, openstack_temp_url_key: ENV['SWIFT_TEMP_URL_KEY'], }, - + fog_file: { 'Cache-Control' => 'public, max-age=315576000, immutable' }, fog_directory: ENV['SWIFT_CONTAINER'], diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 54b574666..5cc4e9380 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -145,7 +145,7 @@ class Rack::Attack 'Content-Type' => 'application/json', 'X-RateLimit-Limit' => match_data[:limit].to_s, 'X-RateLimit-Remaining' => '0', - 'X-RateLimit-Reset' => (now + (match_data[:period] - now.to_i % match_data[:period])).iso8601(6), + 'X-RateLimit-Reset' => (now + (match_data[:period] - (now.to_i % match_data[:period]))).iso8601(6), } [429, headers, [{ error: I18n.t('errors.429') }.to_json]] diff --git a/config/initializers/webauthn.rb b/config/initializers/webauthn.rb index a0a5b8153..a4f027947 100644 --- a/config/initializers/webauthn.rb +++ b/config/initializers/webauthn.rb @@ -1,7 +1,7 @@ WebAuthn.configure do |config| # This value needs to match `window.location.origin` evaluated by # the User Agent during registration and authentication ceremonies. - config.origin = "#{Rails.configuration.x.use_https ? 'https' : 'http' }://#{Rails.configuration.x.web_domain}" + config.origin = "#{Rails.configuration.x.use_https ? 'https' : 'http'}://#{Rails.configuration.x.web_domain}" # Relying Party name for display purposes config.rp_name = "Mastodon" diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 458fa6d75..eef821481 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -13,8 +13,8 @@ en: locked: Your account is locked. not_found_in_database: Invalid %{authentication_keys} or password. pending: Your account is still under review. - timeout: Your session expired. Please sign in again to continue. - unauthenticated: You need to sign in or sign up before continuing. + timeout: Your session expired. Please login again to continue. + unauthenticated: You need to login or sign up before continuing. unconfirmed: You have to confirm your email address before continuing. mailer: confirmation_instructions: @@ -102,7 +102,7 @@ en: unlocks: send_instructions: You will receive an email with instructions for how to unlock your account in a few minutes. Please check your spam folder if you didn't receive this email. send_paranoid_instructions: If your account exists, you will receive an email with instructions for how to unlock it in a few minutes. Please check your spam folder if you didn't receive this email. - unlocked: Your account has been unlocked successfully. Please sign in to continue. + unlocked: Your account has been unlocked successfully. Please login to continue. errors: messages: already_confirmed: was already confirmed, please try signing in diff --git a/config/locales/en.yml b/config/locales/en.yml index 0188519c2..76198763a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -731,6 +731,9 @@ en: branding: preamble: Your server's branding differentiates it from other servers in the network. This information may be displayed across a variety of environments, such as Mastodon's web interface, native applications, in link previews on other websites and within messaging apps, and so on. For this reason, it is best to keep this information clear, short and concise. title: Branding + captcha_enabled: + desc_html: This relies on external scripts from hCaptcha, which may be a security and privacy concern. In addition, this can make the registration process significantly less accessible to some (especially disabled) people. For these reasons, please consider alternative measures such as approval-based or invite-based registration. + title: Require new users to solve a CAPTCHA to confirm their account content_retention: preamble: Control how user-generated content is stored in Mastodon. title: Content retention @@ -979,6 +982,9 @@ en: your_token: Your access token auth: apply_for_account: Request an account + captcha_confirmation: + hint_html: Just one more step! To confirm your account, this server requires you to solve a CAPTCHA. You can contact the server administrator if you have questions or need assistance with confirming your account. + title: User verification change_password: Password confirmations: wrong_email_hint: If that e-mail address is not correct, you can change it in account settings. @@ -1027,8 +1033,8 @@ en: new_confirmation_instructions_sent: You will receive a new e-mail with the confirmation link in a few minutes! title: Check your inbox sign_in: - preamble_html: Sign in with your %{domain} credentials. If your account is hosted on a different server, you will not be able to log in here. - title: Sign in to %{domain} + preamble_html: Login with your %{domain} credentials. If your account is hosted on a different server, you will not be able to log in here. + title: Login to %{domain} sign_up: manual_review: Sign-ups on %{domain} go through manual review by our moderators. To help us process your registration, write a bit about yourself and why you want an account on %{domain}. preamble: With an account on this Mastodon server, you'll be able to follow any other person on the network, regardless of where their account is hosted. @@ -1440,6 +1446,7 @@ en: expired: The poll has already ended invalid_choice: The chosen vote option does not exist over_character_limit: cannot be longer than %{max} characters each + self_vote: You cannot vote in your own polls too_few_options: must have more than one item too_many_options: can't contain more than %{max} items preferences: @@ -1595,7 +1602,7 @@ en: show_newer: Show newer show_older: Show older show_thread: Show thread - sign_in_to_participate: Sign in to participate in the conversation + sign_in_to_participate: Login to participate in the conversation title: '%{name}: "%{quote}"' visibilities: direct: Direct diff --git a/config/routes/api.rb b/config/routes/api.rb index 1ca4d41be..8cc135c13 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -110,6 +110,7 @@ namespace :api, format: false do namespace :emails do resources :confirmations, only: [:create] + get :check_confirmation, to: 'confirmations#check' end resource :instance, only: [:show] do diff --git a/config/settings.yml b/config/settings.yml index 65eee7516..c9c37a6f7 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -43,8 +43,8 @@ defaults: &defaults show_domain_blocks_rationale: 'disabled' outgoing_spoilers: '' require_invite_text: false - captcha_enabled: false backups_retention_period: 7 + captcha_enabled: false development: <<: *defaults diff --git a/config/webpack/generateLocalePacks.js b/config/webpack/generateLocalePacks.js index fedf0c7a1..bb0b10a1c 100644 --- a/config/webpack/generateLocalePacks.js +++ b/config/webpack/generateLocalePacks.js @@ -12,7 +12,7 @@ const { existsSync, readdirSync, writeFileSync } = require('fs'); const { join, resolve } = require('path'); const rimraf = require('rimraf'); -const mkdirp = require('mkdirp'); +const { mkdirp } = require('mkdirp'); const { flavours } = require('./configuration'); module.exports = Object.keys(flavours).reduce(function (map, entry) { diff --git a/db/migrate/20200407202420_migrate_unavailable_inboxes.rb b/db/migrate/20200407202420_migrate_unavailable_inboxes.rb index 8f9c68794..05a01be28 100644 --- a/db/migrate/20200407202420_migrate_unavailable_inboxes.rb +++ b/db/migrate/20200407202420_migrate_unavailable_inboxes.rb @@ -5,9 +5,9 @@ class MigrateUnavailableInboxes < ActiveRecord::Migration[5.2] redis = RedisConfiguration.pool.checkout urls = redis.smembers('unavailable_inboxes') - hosts = urls.map do |url| + hosts = urls.filter_map do |url| Addressable::URI.parse(url).normalized_host - end.compact.uniq + end.uniq UnavailableDomain.delete_all diff --git a/jest.config.js b/jest.config.js index d6e7bc65f..922b0f550 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,7 +10,6 @@ const config = { '/tmp/', '/app/javascript/themes/', ], - setupFiles: ['raf/polyfill'], setupFilesAfterEnv: ['/app/javascript/mastodon/test_setup.js'], collectCoverageFrom: [ 'app/javascript/mastodon/**/*.{js,jsx,ts,tsx}', diff --git a/lib/mastodon/media_cli.rb b/lib/mastodon/media_cli.rb index 7dacd8d3d..1bedcd9be 100644 --- a/lib/mastodon/media_cli.rb +++ b/lib/mastodon/media_cli.rb @@ -24,7 +24,7 @@ module Mastodon desc 'remove', 'Remove remote media files, headers or avatars' long_desc <<-DESC Removes locally cached copies of media attachments (and optionally profile - headers and avatars) from other servers. By default, only media attachements + headers and avatars) from other servers. By default, only media attachments are removed. The --days option specifies how old media attachments have to be before they are removed. In case of avatars and headers, it specifies how old diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake index ceb421f4b..60399c9de 100644 --- a/lib/tasks/tests.rake +++ b/lib/tasks/tests.rake @@ -25,7 +25,7 @@ namespace :tests do end if Account.where(domain: Rails.configuration.x.local_domain).exists? - puts 'Faux remote accounts not properly claned up' + puts 'Faux remote accounts not properly cleaned up' exit(1) end diff --git a/package.json b/package.json index 08a5c7a64..7fd1273a2 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "file-loader": "^6.2.0", "font-awesome": "^4.7.0", "fuzzysort": "^2.0.4", - "glob": "^10.2.2", + "glob": "^10.2.6", "history": "^4.10.1", "http-link-header": "^1.1.1", "immutable": "^4.3.0", @@ -76,22 +76,22 @@ "intl-messageformat": "^2.2.0", "intl-relativeformat": "^6.4.3", "js-yaml": "^4.1.0", - "jsdom": "^21.1.2", + "jsdom": "^22.0.0", "lodash": "^4.17.21", "mark-loader": "^0.1.6", "marky": "^1.2.5", "mini-css-extract-plugin": "^1.6.2", - "mkdirp": "^2.1.6", + "mkdirp": "^3.0.1", "npmlog": "^7.0.1", "path-complete-extname": "^1.0.0", "pg": "^8.5.0", - "pg-connection-string": "^2.5.0", + "pg-connection-string": "^2.6.0", "postcss": "^8.4.23", "postcss-loader": "^4.3.0", "prop-types": "^15.8.1", "punycode": "^2.3.0", - "react": "^16.14.0", - "react-dom": "^16.14.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-helmet": "^6.1.0", "react-hotkeys": "^1.1.4", "react-immutable-proptypes": "^2.2.0", @@ -116,7 +116,7 @@ "regenerator-runtime": "^0.13.11", "requestidlecallback": "^0.3.0", "reselect": "^4.1.8", - "rimraf": "^5.0.0", + "rimraf": "^5.0.1", "sass": "^1.62.1", "sass-loader": "^10.2.0", "stacktrace-js": "^2.0.2", @@ -131,7 +131,7 @@ "webpack-assets-manifest": "^4.0.6", "webpack-bundle-analyzer": "^4.8.0", "webpack-cli": "^3.3.12", - "webpack-merge": "^5.8.0", + "webpack-merge": "^5.9.0", "wicg-inert": "^3.1.2", "workbox-expiration": "^6.5.4", "workbox-precaching": "^6.5.4", @@ -143,7 +143,7 @@ }, "devDependencies": { "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^12.1.5", + "@testing-library/react": "^14.0.0", "@types/babel__core": "^7.20.0", "@types/emoji-mart": "^3.0.9", "@types/escape-html": "^1.0.2", @@ -158,9 +158,8 @@ "@types/pg": "^8.6.6", "@types/prop-types": "^15.7.5", "@types/punycode": "^2.1.0", - "@types/raf": "^3.4.0", - "@types/react": "^16.14.38", - "@types/react-dom": "^16.9.18", + "@types/react": "^18.0.26", + "@types/react-dom": "^18.2.4", "@types/react-helmet": "^6.1.6", "@types/react-immutable-proptypes": "^2.1.0", "@types/react-intl": "2.3.18", @@ -179,14 +178,15 @@ "@types/uuid": "^9.0.0", "@types/webpack": "^4.41.33", "@types/yargs": "^17.0.24", - "@typescript-eslint/eslint-plugin": "^5.59.5", - "@typescript-eslint/parser": "^5.59.5", + "@typescript-eslint/eslint-plugin": "^5.59.7", + "@typescript-eslint/parser": "^5.59.7", "babel-jest": "^29.5.0", - "eslint": "^8.39.0", + "eslint": "^8.40.0", "eslint-config-prettier": "^8.8.0", + "eslint-import-resolver-typescript": "^3.5.5", "eslint-plugin-formatjs": "^4.10.1", "eslint-plugin-import": "~2.27.5", - "eslint-plugin-jsdoc": "^43.1.1", + "eslint-plugin-jsdoc": "^44.2.4", "eslint-plugin-jsx-a11y": "~6.7.1", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-promise": "~6.1.1", @@ -197,16 +197,16 @@ "jest-environment-jsdom": "^29.5.0", "lint-staged": "^13.2.2", "prettier": "^2.8.8", - "raf": "^3.4.1", "react-intl-translations-manager": "^5.0.3", - "react-test-renderer": "^16.14.0", - "stylelint": "^15.6.1", + "react-test-renderer": "^18.2.0", + "stylelint": "^15.6.2", "stylelint-config-standard-scss": "^9.0.0", "typescript": "^5.0.4", "webpack-dev-server": "^3.11.3", "yargs": "^17.7.2" }, "resolutions": { + "@types/react": "^18.0.26", "kind-of": "^6.0.3", "webpack/terser-webpack-plugin": "^4.2.3" }, @@ -216,7 +216,7 @@ }, "lint-staged": { "*": "prettier --ignore-unknown --write", - "Capfile|Gemfile|*.{rb,ruby,ru,rake}": "bundle exec rubocop -a", + "Capfile|Gemfile|*.{rb,ruby,ru,rake}": "bundle exec rubocop --force-exclusion -a", "*.{js,jsx,ts,tsx}": "eslint --fix", "*.{css,scss}": "stylelint --fix" } diff --git a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb index 8fcce165b..e544585ec 100644 --- a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb +++ b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb @@ -14,9 +14,7 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController do follower_2.follow!(account) follower_3.follow!(account) follower_4.follow!(account) - end - before do allow(controller).to receive(:signed_request_actor).and_return(remote_account) end diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb index 8823d9fe7..6946fdfcf 100644 --- a/spec/controllers/activitypub/outboxes_controller_spec.rb +++ b/spec/controllers/activitypub/outboxes_controller_spec.rb @@ -27,9 +27,7 @@ RSpec.describe ActivityPub::OutboxesController do Fabricate(:status, account: account, visibility: :private) Fabricate(:status, account: account, visibility: :direct) Fabricate(:status, account: account, visibility: :limited) - end - before do allow(controller).to receive(:signed_request_actor).and_return(remote_account) end diff --git a/spec/controllers/admin/announcements_controller_spec.rb b/spec/controllers/admin/announcements_controller_spec.rb index 288ac1d71..a8905160f 100644 --- a/spec/controllers/admin/announcements_controller_spec.rb +++ b/spec/controllers/admin/announcements_controller_spec.rb @@ -18,4 +18,59 @@ describe Admin::AnnouncementsController do expect(response).to have_http_status(:success) end end + + describe 'GET #new' do + it 'returns http success and renders new' do + get :new + + expect(response).to have_http_status(:success) + expect(response).to render_template(:new) + end + end + + describe 'GET #edit' do + let(:announcement) { Fabricate(:announcement) } + + it 'returns http success and renders edit' do + get :edit, params: { id: announcement.id } + + expect(response).to have_http_status(:success) + expect(response).to render_template(:edit) + end + end + + describe 'POST #create' do + it 'creates a new announcement and redirects' do + expect do + post :create, params: { announcement: { text: 'The announcement message.' } } + end.to change(Announcement, :count).by(1) + + expect(response).to redirect_to(admin_announcements_path) + expect(flash.notice).to match(I18n.t('admin.announcements.published_msg')) + end + end + + describe 'PUT #update' do + let(:announcement) { Fabricate(:announcement, text: 'Original text') } + + it 'updates an announcement and redirects' do + put :update, params: { id: announcement.id, announcement: { text: 'Updated text.' } } + + expect(response).to redirect_to(admin_announcements_path) + expect(flash.notice).to match(I18n.t('admin.announcements.updated_msg')) + end + end + + describe 'DELETE #destroy' do + let!(:announcement) { Fabricate(:announcement, text: 'Original text') } + + it 'destroys an announcement and redirects' do + expect do + delete :destroy, params: { id: announcement.id } + end.to change(Announcement, :count).by(-1) + + expect(response).to redirect_to(admin_announcements_path) + expect(flash.notice).to match(I18n.t('admin.announcements.destroyed_msg')) + end + end end diff --git a/spec/controllers/admin/confirmations_controller_spec.rb b/spec/controllers/admin/confirmations_controller_spec.rb index ffab56d9a..181616a66 100644 --- a/spec/controllers/admin/confirmations_controller_spec.rb +++ b/spec/controllers/admin/confirmations_controller_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Admin::ConfirmationsController do end end - describe 'POST #resernd' do + describe 'POST #resend' do subject { post :resend, params: { account_id: user.account.id } } let!(:user) { Fabricate(:user, confirmed_at: confirmed_at) } diff --git a/spec/controllers/admin/disputes/appeals_controller_spec.rb b/spec/controllers/admin/disputes/appeals_controller_spec.rb index 371c4f483..99b19298c 100644 --- a/spec/controllers/admin/disputes/appeals_controller_spec.rb +++ b/spec/controllers/admin/disputes/appeals_controller_spec.rb @@ -5,16 +5,16 @@ require 'rails_helper' RSpec.describe Admin::Disputes::AppealsController do render_views - before { sign_in current_user, scope: :user } + before do + sign_in current_user, scope: :user + + target_account.suspend! + end let(:target_account) { Fabricate(:account) } let(:strike) { Fabricate(:account_warning, target_account: target_account, action: :suspend) } let(:appeal) { Fabricate(:appeal, strike: strike, account: target_account) } - before do - target_account.suspend! - end - describe 'POST #approve' do let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } diff --git a/spec/controllers/admin/reports/actions_controller_spec.rb b/spec/controllers/admin/reports/actions_controller_spec.rb index a84f2324e..701855f92 100644 --- a/spec/controllers/admin/reports/actions_controller_spec.rb +++ b/spec/controllers/admin/reports/actions_controller_spec.rb @@ -146,13 +146,13 @@ describe Admin::Reports::ActionsController do end end - context 'with Action as submit button' do + context 'with action as submit button' do subject { post :create, params: common_params.merge({ action => '' }) } it_behaves_like 'all action types' end - context 'with Action as submit button' do + context 'with moderation action as an extra field' do subject { post :create, params: common_params.merge({ moderation_action: action }) } it_behaves_like 'all action types' diff --git a/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb index 3acae843a..fe39596df 100644 --- a/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb +++ b/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb @@ -5,19 +5,354 @@ require 'rails_helper' describe Api::V1::Admin::CanonicalEmailBlocksController do render_views - let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } - let(:account) { Fabricate(:account) } + let(:role) { UserRole.find_by(name: 'Admin') } + let(:user) { Fabricate(:user, role: role) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:scopes) { 'admin:read:canonical_email_blocks admin:write:canonical_email_blocks' } before do allow(controller).to receive(:doorkeeper_token) { token } end + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { UserRole.find_by(name: wrong_role) } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + describe 'GET #index' do + context 'with wrong scope' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'with wrong role' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'forbidden for wrong role', 'Moderator' + end + it 'returns http success' do - get :index, params: { account_id: account.id, limit: 2 } + get :index expect(response).to have_http_status(200) end + + context 'when there is no canonical email block' do + it 'returns an empty list' do + get :index + + body = body_as_json + + expect(body).to be_empty + end + end + + context 'when there are canonical email blocks' do + let!(:canonical_email_blocks) { Fabricate.times(5, :canonical_email_block) } + let(:expected_email_hashes) { canonical_email_blocks.pluck(:canonical_email_hash) } + + it 'returns the correct canonical email hashes' do + get :index + + json = body_as_json + + expect(json.pluck(:canonical_email_hash)).to match_array(expected_email_hashes) + end + + context 'with limit param' do + let(:params) { { limit: 2 } } + + it 'returns only the requested number of canonical email blocks' do + get :index, params: params + + json = body_as_json + + expect(json.size).to eq(params[:limit]) + end + end + + context 'with since_id param' do + let(:params) { { since_id: canonical_email_blocks[1].id } } + + it 'returns only the canonical email blocks after since_id' do + get :index, params: params + + canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s) + json = body_as_json + + expect(json.pluck(:id)).to match_array(canonical_email_blocks_ids[2..]) + end + end + + context 'with max_id param' do + let(:params) { { max_id: canonical_email_blocks[3].id } } + + it 'returns only the canonical email blocks before max_id' do + get :index, params: params + + canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s) + json = body_as_json + + expect(json.pluck(:id)).to match_array(canonical_email_blocks_ids[..2]) + end + end + end + end + + describe 'GET #show' do + let!(:canonical_email_block) { Fabricate(:canonical_email_block) } + let(:params) { { id: canonical_email_block.id } } + + context 'with wrong scope' do + before do + get :show, params: params + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'with wrong role' do + before do + get :show, params: params + end + + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'forbidden for wrong role', 'Moderator' + end + + context 'when canonical email block exists' do + it 'returns http success' do + get :show, params: params + + expect(response).to have_http_status(200) + end + + it 'returns canonical email block data correctly' do + get :show, params: params + + json = body_as_json + + expect(json[:id]).to eq(canonical_email_block.id.to_s) + expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash) + end + end + + context 'when canonical block does not exist' do + it 'returns http not found' do + get :show, params: { id: 0 } + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST #test' do + context 'with wrong scope' do + before do + post :test + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'with wrong role' do + before do + post :test, params: { email: 'whatever@email.com' } + end + + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'forbidden for wrong role', 'Moderator' + end + + context 'when required email is not provided' do + it 'returns http bad request' do + post :test + + expect(response).to have_http_status(400) + end + end + + context 'when required email is provided' do + let(:params) { { email: 'example@email.com' } } + + context 'when there is a matching canonical email block' do + let!(:canonical_email_block) { CanonicalEmailBlock.create(params) } + + it 'returns http success' do + post :test, params: params + + expect(response).to have_http_status(200) + end + + it 'returns expected canonical email hash' do + post :test, params: params + + json = body_as_json + + expect(json[0][:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash) + end + end + + context 'when there is no matching canonical email block' do + it 'returns http success' do + post :test, params: params + + expect(response).to have_http_status(200) + end + + it 'returns an empty list' do + post :test, params: params + + json = body_as_json + + expect(json).to be_empty + end + end + end + end + + describe 'POST #create' do + let(:params) { { email: 'example@email.com' } } + let(:canonical_email_block) { CanonicalEmailBlock.new(email: params[:email]) } + + context 'with wrong scope' do + before do + post :create, params: params + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'with wrong role' do + before do + post :create, params: params + end + + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'forbidden for wrong role', 'Moderator' + end + + it 'returns http success' do + post :create, params: params + + expect(response).to have_http_status(200) + end + + it 'returns canonical_email_hash correctly' do + post :create, params: params + + json = body_as_json + + expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash) + end + + context 'when required email param is not provided' do + it 'returns http unprocessable entity' do + post :create + + expect(response).to have_http_status(422) + end + end + + context 'when canonical_email_hash param is provided instead of email' do + let(:params) { { canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } } + + it 'returns http success' do + post :create, params: params + + expect(response).to have_http_status(200) + end + + it 'returns correct canonical_email_hash' do + post :create, params: params + + json = body_as_json + + expect(json[:canonical_email_hash]).to eq(params[:canonical_email_hash]) + end + end + + context 'when both email and canonical_email_hash params are provided' do + let(:params) { { email: 'example@email.com', canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } } + + it 'returns http success' do + post :create, params: params + + expect(response).to have_http_status(200) + end + + it 'ignores canonical_email_hash param' do + post :create, params: params + + json = body_as_json + + expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash) + end + end + + context 'when canonical email was already blocked' do + before do + canonical_email_block.save + end + + it 'returns http unprocessable entity' do + post :create, params: params + + expect(response).to have_http_status(422) + end + end + end + + describe 'DELETE #destroy' do + let!(:canonical_email_block) { Fabricate(:canonical_email_block) } + let(:params) { { id: canonical_email_block.id } } + + context 'with wrong scope' do + before do + delete :destroy, params: params + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'with wrong role' do + before do + delete :destroy, params: params + end + + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'forbidden for wrong role', 'Moderator' + end + + it 'returns http success' do + delete :destroy, params: params + + expect(response).to have_http_status(200) + end + + context 'when canonical email block is not found' do + it 'returns http not found' do + delete :destroy, params: { id: 0 } + + expect(response).to have_http_status(404) + end + end end end diff --git a/spec/controllers/api/v1/admin/domain_allows_controller_spec.rb b/spec/controllers/api/v1/admin/domain_allows_controller_spec.rb index 9db8a35b4..ca63ea5a7 100644 --- a/spec/controllers/api/v1/admin/domain_allows_controller_spec.rb +++ b/spec/controllers/api/v1/admin/domain_allows_controller_spec.rb @@ -128,5 +128,13 @@ RSpec.describe Api::V1::Admin::DomainAllowsController do expect(response).to have_http_status(422) end end + + context 'when domain name is not specified' do + it 'returns http unprocessable entity' do + post :create + + expect(response).to have_http_status(422) + end + end end end diff --git a/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb index a92a29869..3643eb0f3 100644 --- a/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb +++ b/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb @@ -5,19 +5,280 @@ require 'rails_helper' describe Api::V1::Admin::EmailDomainBlocksController do render_views - let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } + let(:role) { UserRole.find_by(name: 'Admin') } + let(:user) { Fabricate(:user, role: role) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } let(:account) { Fabricate(:account) } + let(:scopes) { 'admin:read:email_domain_blocks admin:write:email_domain_blocks' } before do allow(controller).to receive(:doorkeeper_token) { token } end + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { UserRole.find_by(name: wrong_role) } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + describe 'GET #index' do + context 'with wrong scope' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'with wrong role' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'forbidden for wrong role', 'Moderator' + end + it 'returns http success' do - get :index, params: { account_id: account.id, limit: 2 } + get :index expect(response).to have_http_status(200) end + + context 'when there is no email domain block' do + it 'returns an empty list' do + get :index + + json = body_as_json + + expect(json).to be_empty + end + end + + context 'when there are email domain blocks' do + let!(:email_domain_blocks) { Fabricate.times(5, :email_domain_block) } + let(:blocked_email_domains) { email_domain_blocks.pluck(:domain) } + + it 'return the correct blocked email domains' do + get :index + + json = body_as_json + + expect(json.pluck(:domain)).to match_array(blocked_email_domains) + end + + context 'with limit param' do + let(:params) { { limit: 2 } } + + it 'returns only the requested number of email domain blocks' do + get :index, params: params + + json = body_as_json + + expect(json.size).to eq(params[:limit]) + end + end + + context 'with since_id param' do + let(:params) { { since_id: email_domain_blocks[1].id } } + + it 'returns only the email domain blocks after since_id' do + get :index, params: params + + email_domain_blocks_ids = email_domain_blocks.pluck(:id).map(&:to_s) + json = body_as_json + + expect(json.pluck(:id)).to match_array(email_domain_blocks_ids[2..]) + end + end + + context 'with max_id param' do + let(:params) { { max_id: email_domain_blocks[3].id } } + + it 'returns only the email domain blocks before max_id' do + get :index, params: params + + email_domain_blocks_ids = email_domain_blocks.pluck(:id).map(&:to_s) + json = body_as_json + + expect(json.pluck(:id)).to match_array(email_domain_blocks_ids[..2]) + end + end + end + end + + describe 'GET #show' do + let!(:email_domain_block) { Fabricate(:email_domain_block) } + let(:params) { { id: email_domain_block.id } } + + context 'with wrong scope' do + before do + get :show, params: params + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'with wrong role' do + before do + get :show, params: params + end + + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'forbidden for wrong role', 'Moderator' + end + + context 'when email domain block exists' do + it 'returns http success' do + get :show, params: params + + expect(response).to have_http_status(200) + end + + it 'returns the correct blocked domain' do + get :show, params: params + + json = body_as_json + + expect(json[:domain]).to eq(email_domain_block.domain) + end + end + + context 'when email domain block does not exist' do + it 'returns http not found' do + get :show, params: { id: 0 } + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST #create' do + let(:params) { { domain: 'example.com' } } + + context 'with wrong scope' do + before do + post :create, params: params + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'with wrong role' do + before do + post :create, params: params + end + + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'forbidden for wrong role', 'Moderator' + end + + it 'returns http success' do + post :create, params: params + + expect(response).to have_http_status(200) + end + + it 'returns the correct blocked email domain' do + post :create, params: params + + json = body_as_json + + expect(json[:domain]).to eq(params[:domain]) + end + + context 'when domain param is not provided' do + let(:params) { { domain: '' } } + + it 'returns http unprocessable entity' do + post :create, params: params + + expect(response).to have_http_status(422) + end + end + + context 'when provided domain name has an invalid character' do + let(:params) { { domain: 'do\uD800.com' } } + + it 'returns http unprocessable entity' do + post :create, params: params + + expect(response).to have_http_status(422) + end + end + + context 'when provided domain is already blocked' do + before do + EmailDomainBlock.create(params) + end + + it 'returns http unprocessable entity' do + post :create, params: params + + expect(response).to have_http_status(422) + end + end + end + + describe 'DELETE #destroy' do + let!(:email_domain_block) { Fabricate(:email_domain_block) } + let(:params) { { id: email_domain_block.id } } + + context 'with wrong scope' do + before do + delete :destroy, params: params + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'with wrong role' do + before do + delete :destroy, params: params + end + + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'forbidden for wrong role', 'Moderator' + end + + it 'returns http success' do + delete :destroy, params: params + + expect(response).to have_http_status(200) + end + + it 'returns an empty body' do + delete :destroy, params: params + + json = body_as_json + + expect(json).to be_empty + end + + it 'deletes email domain block' do + delete :destroy, params: params + + email_domain_block = EmailDomainBlock.find_by(id: params[:id]) + + expect(email_domain_block).to be_nil + end + + context 'when email domain block does not exist' do + it 'returns http not found' do + delete :destroy, params: { id: 0 } + + expect(response).to have_http_status(404) + end + end end end diff --git a/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb index 50e2ae968..a5787883e 100644 --- a/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb +++ b/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb @@ -5,19 +5,305 @@ require 'rails_helper' describe Api::V1::Admin::IpBlocksController do render_views - let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } - let(:account) { Fabricate(:account) } + let(:role) { UserRole.find_by(name: 'Admin') } + let(:user) { Fabricate(:user, role: role) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:scopes) { 'admin:read:ip_blocks admin:write:ip_blocks' } before do allow(controller).to receive(:doorkeeper_token) { token } end + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { UserRole.find_by(name: wrong_role) } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + describe 'GET #index' do + context 'with wrong scope' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks' + end + + context 'with wrong role' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'forbidden for wrong role', 'Moderator' + end + it 'returns http success' do - get :index, params: { account_id: account.id, limit: 2 } + get :index expect(response).to have_http_status(200) end + + context 'when there is no ip block' do + it 'returns an empty body' do + get :index + + json = body_as_json + + expect(json).to be_empty + end + end + + context 'when there are ip blocks' do + let!(:ip_blocks) do + [ + IpBlock.create(ip: '192.0.2.0/24', severity: :no_access), + IpBlock.create(ip: '172.16.0.1', severity: :sign_up_requires_approval, comment: 'Spam'), + IpBlock.create(ip: '2001:0db8::/32', severity: :sign_up_block, expires_in: 10.days), + ] + end + let(:expected_response) do + ip_blocks.map do |ip_block| + { + id: ip_block.id.to_s, + ip: ip_block.ip, + severity: ip_block.severity.to_s, + comment: ip_block.comment, + created_at: ip_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), + expires_at: ip_block.expires_at&.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), + } + end + end + + it 'returns the correct blocked ips' do + get :index + + json = body_as_json + + expect(json).to match_array(expected_response) + end + + context 'with limit param' do + let(:params) { { limit: 2 } } + + it 'returns only the requested number of ip blocks' do + get :index, params: params + + json = body_as_json + + expect(json.size).to eq(params[:limit]) + end + end + end + end + + describe 'GET #show' do + let!(:ip_block) { IpBlock.create(ip: '192.0.2.0/24', severity: :no_access) } + let(:params) { { id: ip_block.id } } + + context 'with wrong scope' do + before do + get :show, params: params + end + + it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks' + end + + context 'with wrong role' do + before do + get :show, params: params + end + + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'forbidden for wrong role', 'Moderator' + end + + it 'returns http success' do + get :show, params: params + + expect(response).to have_http_status(200) + end + + it 'returns the correct ip block' do + get :show, params: params + + json = body_as_json + + expect(json[:ip]).to eq("#{ip_block.ip}/#{ip_block.ip.prefix}") + expect(json[:severity]).to eq(ip_block.severity.to_s) + end + + context 'when ip block does not exist' do + it 'returns http not found' do + get :show, params: { id: 0 } + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST #create' do + let(:params) { { ip: '151.0.32.55', severity: 'no_access', comment: 'Spam' } } + + context 'with wrong scope' do + before do + post :create, params: params + end + + it_behaves_like 'forbidden for wrong scope', 'admin:read:ip_blocks' + end + + context 'with wrong role' do + before do + post :create, params: params + end + + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'forbidden for wrong role', 'Moderator' + end + + it 'returns http success' do + post :create, params: params + + expect(response).to have_http_status(200) + end + + it 'returns the correct ip block' do + post :create, params: params + + json = body_as_json + + expect(json[:ip]).to eq("#{params[:ip]}/32") + expect(json[:severity]).to eq(params[:severity]) + expect(json[:comment]).to eq(params[:comment]) + end + + context 'when ip is not provided' do + let(:params) { { ip: '', severity: 'no_access' } } + + it 'returns http unprocessable entity' do + post :create, params: params + + expect(response).to have_http_status(422) + end + end + + context 'when severity is not provided' do + let(:params) { { ip: '173.65.23.1', severity: '' } } + + it 'returns http unprocessable entity' do + post :create, params: params + + expect(response).to have_http_status(422) + end + end + + context 'when provided ip is already blocked' do + before do + IpBlock.create(params) + end + + it 'returns http unprocessable entity' do + post :create, params: params + + expect(response).to have_http_status(422) + end + end + + context 'when provided ip address is invalid' do + let(:params) { { ip: '520.13.54.120', severity: 'no_access' } } + + it 'returns http unprocessable entity' do + post :create, params: params + + expect(response).to have_http_status(422) + end + end + end + + describe 'PUT #update' do + context 'when ip block exists' do + let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access', comment: 'Spam', expires_in: 48.hours) } + let(:params) { { id: ip_block.id, severity: 'sign_up_requires_approval', comment: 'Decreasing severity' } } + + it 'returns http success' do + put :update, params: params + + expect(response).to have_http_status(200) + end + + it 'returns the correct ip block' do + put :update, params: params + + json = body_as_json + + expect(json).to match(hash_including({ + ip: "#{ip_block.ip}/#{ip_block.ip.prefix}", + severity: 'sign_up_requires_approval', + comment: 'Decreasing severity', + })) + end + + it 'updates the severity correctly' do + expect { put :update, params: params }.to change { ip_block.reload.severity }.from('no_access').to('sign_up_requires_approval') + end + + it 'updates the comment correctly' do + expect { put :update, params: params }.to change { ip_block.reload.comment }.from('Spam').to('Decreasing severity') + end + end + + context 'when ip block does not exist' do + it 'returns http not found' do + put :update, params: { id: 0 } + + expect(response).to have_http_status(404) + end + end + end + + describe 'DELETE #destroy' do + context 'when ip block exists' do + let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access') } + let(:params) { { id: ip_block.id } } + + it 'returns http success' do + delete :destroy, params: params + + expect(response).to have_http_status(200) + end + + it 'returns an empty body' do + delete :destroy, params: params + + json = body_as_json + + expect(json).to be_empty + end + + it 'deletes the ip block' do + delete :destroy, params: params + + expect(IpBlock.find_by(id: ip_block.id)).to be_nil + end + end + + context 'when ip block does not exist' do + it 'returns http not found' do + delete :destroy, params: { id: 0 } + + expect(response).to have_http_status(404) + end + end end end diff --git a/spec/controllers/api/v1/emails/confirmations_controller_spec.rb b/spec/controllers/api/v1/emails/confirmations_controller_spec.rb index 2a0703ed9..219b5075d 100644 --- a/spec/controllers/api/v1/emails/confirmations_controller_spec.rb +++ b/spec/controllers/api/v1/emails/confirmations_controller_spec.rb @@ -63,4 +63,72 @@ RSpec.describe Api::V1::Emails::ConfirmationsController do end end end + + describe '#check' do + let(:scopes) { 'read' } + + context 'with an oauth token' do + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + context 'when the account is not confirmed' do + it 'returns http success' do + get :check + expect(response).to have_http_status(200) + end + + it 'returns false' do + get :check + expect(body_as_json).to be false + end + end + + context 'when the account is confirmed' do + let(:confirmed_at) { Time.now.utc } + + it 'returns http success' do + get :check + expect(response).to have_http_status(200) + end + + it 'returns true' do + get :check + expect(body_as_json).to be true + end + end + end + + context 'with an authentication cookie' do + before do + sign_in user, scope: :user + end + + context 'when the account is not confirmed' do + it 'returns http success' do + get :check + expect(response).to have_http_status(200) + end + + it 'returns false' do + get :check + expect(body_as_json).to be false + end + end + + context 'when the account is confirmed' do + let(:confirmed_at) { Time.now.utc } + + it 'returns http success' do + get :check + expect(response).to have_http_status(200) + end + + it 'returns true' do + get :check + expect(body_as_json).to be true + end + end + end + end end diff --git a/spec/controllers/api/v1/featured_tags_controller_spec.rb b/spec/controllers/api/v1/featured_tags_controller_spec.rb deleted file mode 100644 index aac942901..000000000 --- a/spec/controllers/api/v1/featured_tags_controller_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Api::V1::FeaturedTagsController do - render_views - - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } - let(:account) { Fabricate(:account) } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'GET #index' do - it 'returns http success' do - get :index, params: { account_id: account.id, limit: 2 } - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index ad8465e2a..a80273635 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -97,10 +97,12 @@ RSpec.describe Auth::RegistrationsController do end describe 'POST #create' do - let(:accept_language) { Rails.application.config.i18n.available_locales.sample.to_s } + let(:accept_language) { 'de' } before do session[:registration_form_time] = 5.seconds.ago + + request.env['devise.mapping'] = Devise.mappings[:user] end around do |example| @@ -109,8 +111,6 @@ RSpec.describe Auth::RegistrationsController do end end - before { request.env['devise.mapping'] = Devise.mappings[:user] } - context do subject do Setting.registrations_mode = 'open' diff --git a/spec/controllers/concerns/signature_verification_spec.rb b/spec/controllers/concerns/signature_verification_spec.rb index df20a5d7e..e6b7f1849 100644 --- a/spec/controllers/concerns/signature_verification_spec.rb +++ b/spec/controllers/concerns/signature_verification_spec.rb @@ -129,7 +129,7 @@ describe ApplicationController do end end - context 'with request with unparseable Date header' do + context 'with request with unparsable Date header' do before do get :success diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb index c846dd1d6..1885814cd 100644 --- a/spec/controllers/statuses_controller_spec.rb +++ b/spec/controllers/statuses_controller_spec.rb @@ -719,65 +719,180 @@ describe StatusesController do end context 'when status is public' do - pending + before do + status.update(visibility: :public) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end end context 'when status is private' do - pending + before do + status.update(visibility: :private) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http not_found' do + expect(response).to have_http_status(404) + end end context 'when status is direct' do - pending + before do + status.update(visibility: :direct) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http not_found' do + expect(response).to have_http_status(404) + end end context 'when signed-in' do + let(:user) { Fabricate(:user) } + + before do + sign_in(user) + end + context 'when status is public' do - pending + before do + status.update(visibility: :public) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end end context 'when status is private' do + before do + status.update(visibility: :private) + end + context 'when user is authorized to see it' do - pending + before do + user.account.follow!(account) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end end context 'when user is not authorized to see it' do - pending + before do + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http not_found' do + expect(response).to have_http_status(404) + end end end context 'when status is direct' do + before do + status.update(visibility: :direct) + end + context 'when user is authorized to see it' do - pending + before do + Fabricate(:mention, account: user.account, status: status) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end end context 'when user is not authorized to see it' do - pending + before do + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http not_found' do + expect(response).to have_http_status(404) + end end end end context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com') } + + before do + allow(controller).to receive(:signed_request_actor).and_return(remote_account) + end + context 'when status is public' do - pending + before do + status.update(visibility: :public) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end end context 'when status is private' do + before do + status.update(visibility: :private) + end + context 'when user is authorized to see it' do - pending + before do + remote_account.follow!(account) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end end context 'when user is not authorized to see it' do - pending + before do + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http not_found' do + expect(response).to have_http_status(404) + end end end context 'when status is direct' do + before do + status.update(visibility: :direct) + end + context 'when user is authorized to see it' do - pending + before do + Fabricate(:mention, account: remote_account, status: status) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end end context 'when user is not authorized to see it' do - pending + before do + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http not_found' do + expect(response).to have_http_status(404) + end end end end diff --git a/spec/fabricators/canonical_email_block_fabricator.rb b/spec/fabricators/canonical_email_block_fabricator.rb index 21d7c2402..3a018059f 100644 --- a/spec/fabricators/canonical_email_block_fabricator.rb +++ b/spec/fabricators/canonical_email_block_fabricator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true Fabricator(:canonical_email_block) do - email 'test@example.com' + email { sequence(:email) { |i| "#{i}#{Faker::Internet.email}" } } reference_account { Fabricate(:account) } end diff --git a/spec/fabricators/featured_tag_fabricator.rb b/spec/fabricators/featured_tag_fabricator.rb index 747d8e36a..838364056 100644 --- a/spec/fabricators/featured_tag_fabricator.rb +++ b/spec/fabricators/featured_tag_fabricator.rb @@ -3,5 +3,5 @@ Fabricator(:featured_tag) do account tag - name 'Tag' + name { sequence(:name) { |i| "Tag#{i}" } } end diff --git a/spec/fabricators/notification_fabricator.rb b/spec/fabricators/notification_fabricator.rb index 959fda913..1e0c80987 100644 --- a/spec/fabricators/notification_fabricator.rb +++ b/spec/fabricators/notification_fabricator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true Fabricator(:notification) do - activity fabricator: [:mention, :status, :follow, :follow_request, :favourite].sample + activity fabricator: :status account end diff --git a/spec/features/captcha_spec.rb b/spec/features/captcha_spec.rb new file mode 100644 index 000000000..db89ff3e6 --- /dev/null +++ b/spec/features/captcha_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'email confirmation flow when captcha is enabled' do + let(:user) { Fabricate(:user, confirmed_at: nil, confirmation_token: 'foobar', created_by_application: client_app) } + let(:client_app) { nil } + + before do + # rubocop:disable RSpec/AnyInstance -- easiest way to deal with that that I know of + allow_any_instance_of(Auth::ConfirmationsController).to receive(:captcha_enabled?).and_return(true) + allow_any_instance_of(Auth::ConfirmationsController).to receive(:check_captcha!).and_return(true) + allow_any_instance_of(Auth::ConfirmationsController).to receive(:render_captcha).and_return(nil) + # rubocop:enable RSpec/AnyInstance + end + + context 'when the user signed up through an app' do + let(:client_app) { Fabricate(:application) } + + it 'logs in' do + visit "/auth/confirmation?confirmation_token=#{user.confirmation_token}&redirect_to_app=true" + + # It presents the user with a captcha form + expect(page).to have_title(I18n.t('auth.captcha_confirmation.title')) + + # It does not confirm the user just yet + expect(user.reload.confirmed?).to be false + + # It redirects to app and confirms user + click_on I18n.t('challenge.confirm') + expect(user.reload.confirmed?).to be true + expect(page).to have_current_path(/\A#{client_app.confirmation_redirect_uri}/, url: true) + end + end +end diff --git a/spec/lib/account_reach_finder_spec.rb b/spec/lib/account_reach_finder_spec.rb new file mode 100644 index 000000000..1da95ba6b --- /dev/null +++ b/spec/lib/account_reach_finder_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe AccountReachFinder do + let(:account) { Fabricate(:account) } + + let(:follower1) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-1') } + let(:follower2) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-2') } + let(:follower3) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://foo.bar/users/a/inbox', shared_inbox_url: 'https://foo.bar/inbox') } + + let(:mentioned1) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://foo.bar/users/b/inbox', shared_inbox_url: 'https://foo.bar/inbox') } + let(:mentioned2) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-3') } + let(:mentioned3) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-4') } + + let(:unrelated_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/unrelated-inbox') } + + before do + follower1.follow!(account) + follower2.follow!(account) + follower3.follow!(account) + + Fabricate(:status, account: account).tap do |status| + status.mentions << Mention.new(account: follower1) + status.mentions << Mention.new(account: mentioned1) + end + + Fabricate(:status, account: account) + + Fabricate(:status, account: account).tap do |status| + status.mentions << Mention.new(account: mentioned2) + status.mentions << Mention.new(account: mentioned3) + end + + Fabricate(:status).tap do |status| + status.mentions << Mention.new(account: unrelated_account) + end + end + + describe '#inboxes' do + it 'includes the preferred inbox URL of followers' do + expect(described_class.new(account).inboxes).to include(*[follower1, follower2, follower3].map(&:preferred_inbox_url)) + end + + it 'includes the preferred inbox URL of recently-mentioned accounts' do + expect(described_class.new(account).inboxes).to include(*[mentioned1, mentioned2, mentioned3].map(&:preferred_inbox_url)) + end + + it 'does not include the inbox of unrelated users' do + expect(described_class.new(account).inboxes).to_not include(unrelated_account.preferred_inbox_url) + end + end +end diff --git a/spec/lib/activitypub/activity/flag_spec.rb b/spec/lib/activitypub/activity/flag_spec.rb index 005e185e6..601473069 100644 --- a/spec/lib/activitypub/activity/flag_spec.rb +++ b/spec/lib/activitypub/activity/flag_spec.rb @@ -39,6 +39,37 @@ RSpec.describe ActivityPub::Activity::Flag do end end + context 'when the report comment is excessively long' do + subject do + described_class.new({ + '@context': 'https://www.w3.org/ns/activitystreams', + id: flag_id, + type: 'Flag', + content: long_comment, + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: [ + ActivityPub::TagManager.instance.uri_for(flagged), + ActivityPub::TagManager.instance.uri_for(status), + ], + }.with_indifferent_access, sender) + end + + let(:long_comment) { Faker::Lorem.characters(number: 6000) } + + before do + subject.perform + end + + it 'creates a report but with a truncated comment' do + report = Report.find_by(account: sender, target_account: flagged) + + expect(report).to_not be_nil + expect(report.comment.length).to eq 5000 + expect(report.comment).to eq long_comment[0...5000] + expect(report.status_ids).to eq [status.id] + end + end + context 'when the reported status is private and should not be visible to the remote server' do let(:status) { Fabricate(:status, account: flagged, uri: 'foobar', visibility: :private) } diff --git a/spec/lib/mastodon/ip_blocks_cli_spec.rb b/spec/lib/mastodon/ip_blocks_cli_spec.rb new file mode 100644 index 000000000..27c005772 --- /dev/null +++ b/spec/lib/mastodon/ip_blocks_cli_spec.rb @@ -0,0 +1,292 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'mastodon/ip_blocks_cli' + +RSpec.describe Mastodon::IpBlocksCLI do + let(:cli) { described_class.new } + + describe '#add' do + let(:ip_list) do + [ + '192.0.2.1', + '172.16.0.1', + '192.0.2.0/24', + '172.16.0.0/16', + '10.0.0.0/8', + '2001:0db8:85a3:0000:0000:8a2e:0370:7334', + 'fe80::1', + '::1', + '2001:0db8::/32', + 'fe80::/10', + '::/128', + ] + end + let(:options) { { severity: 'no_access' } } + + shared_examples 'ip address blocking' do + it 'blocks all specified IP addresses' do + cli.invoke(:add, ip_list, options) + + blocked_ip_addresses = IpBlock.where(ip: ip_list).pluck(:ip) + expected_ip_addresses = ip_list.map { |ip| IPAddr.new(ip) } + + expect(blocked_ip_addresses).to match_array(expected_ip_addresses) + end + + it 'sets the severity for all blocked IP addresses' do + cli.invoke(:add, ip_list, options) + + blocked_ips_severity = IpBlock.where(ip: ip_list).pluck(:severity).all?(options[:severity]) + + expect(blocked_ips_severity).to be(true) + end + + it 'displays a success message with a summary' do + expect { cli.invoke(:add, ip_list, options) }.to output( + a_string_including("Added #{ip_list.size}, skipped 0, failed 0") + ).to_stdout + end + end + + context 'with valid IP addresses' do + include_examples 'ip address blocking' + end + + context 'when a specified IP address is already blocked' do + let!(:blocked_ip) { IpBlock.create(ip: ip_list.last, severity: options[:severity]) } + + it 'skips the already blocked IP address' do + allow(IpBlock).to receive(:new).and_call_original + + cli.invoke(:add, ip_list, options) + + expect(IpBlock).to_not have_received(:new).with(ip: ip_list.last) + end + + it 'displays the correct summary' do + expect { cli.invoke(:add, ip_list, options) }.to output( + a_string_including("#{ip_list.last} is already blocked\nAdded #{ip_list.size - 1}, skipped 1, failed 0") + ).to_stdout + end + + context 'with --force option' do + let!(:blocked_ip) { IpBlock.create(ip: ip_list.last, severity: 'no_access') } + let(:options) { { severity: 'sign_up_requires_approval', force: true } } + + it 'overwrites the existing IP block record' do + expect { cli.invoke(:add, ip_list, options) } + .to change { blocked_ip.reload.severity } + .from('no_access') + .to('sign_up_requires_approval') + end + + include_examples 'ip address blocking' + end + end + + context 'when a specified IP address is invalid' do + let(:ip_list) { ['320.15.175.0', '9.5.105.255', '0.0.0.0'] } + + it 'displays the correct summary' do + expect { cli.invoke(:add, ip_list, options) }.to output( + a_string_including("#{ip_list.first} is invalid\nAdded #{ip_list.size - 1}, skipped 0, failed 1") + ).to_stdout + end + end + + context 'with --comment option' do + let(:options) { { severity: 'no_access', comment: 'Spam' } } + + include_examples 'ip address blocking' + end + + context 'with --duration option' do + let(:options) { { severity: 'no_access', duration: 10.days } } + + include_examples 'ip address blocking' + end + + context 'with "sign_up_requires_approval" severity' do + let(:options) { { severity: 'sign_up_requires_approval' } } + + include_examples 'ip address blocking' + end + + context 'with "sign_up_block" severity' do + let(:options) { { severity: 'sign_up_block' } } + + include_examples 'ip address blocking' + end + + context 'when a specified IP address fails to be blocked' do + let(:ip_address) { '127.0.0.1' } + let(:ip_block) { instance_double(IpBlock, ip: ip_address, save: false) } + + before do + allow(IpBlock).to receive(:new).and_return(ip_block) + allow(ip_block).to receive(:severity=) + allow(ip_block).to receive(:expires_in=) + end + + it 'displays an error message' do + expect { cli.invoke(:add, [ip_address], options) } + .to output( + a_string_including("#{ip_address} could not be saved") + ).to_stdout + end + end + + context 'when no IP address is provided' do + it 'exits with an error message' do + expect { cli.add }.to output( + a_string_including('No IP(s) given') + ).to_stdout + .and raise_error(SystemExit) + end + end + end + + describe '#remove' do + context 'when removing exact matches' do + let(:ip_list) do + [ + '192.0.2.1', + '172.16.0.1', + '192.0.2.0/24', + '172.16.0.0/16', + '10.0.0.0/8', + '2001:0db8:85a3:0000:0000:8a2e:0370:7334', + 'fe80::1', + '::1', + '2001:0db8::/32', + 'fe80::/10', + '::/128', + ] + end + + before do + ip_list.each { |ip| IpBlock.create(ip: ip, severity: :no_access) } + end + + it 'removes exact IP blocks' do + cli.invoke(:remove, ip_list) + + expect(IpBlock.where(ip: ip_list)).to_not exist + end + + it 'displays success message with a summary' do + expect { cli.invoke(:remove, ip_list) }.to output( + a_string_including("Removed #{ip_list.size}, skipped 0") + ).to_stdout + end + end + + context 'with --force option' do + let!(:block1) { IpBlock.create(ip: '192.168.0.0/24', severity: :no_access) } + let!(:block2) { IpBlock.create(ip: '10.0.0.0/16', severity: :no_access) } + let!(:block3) { IpBlock.create(ip: '172.16.0.0/20', severity: :no_access) } + let(:arguments) { ['192.168.0.5', '10.0.1.50'] } + let(:options) { { force: true } } + + it 'removes blocks for IP ranges that cover given IP(s)' do + cli.invoke(:remove, arguments, options) + + expect(IpBlock.where(id: [block1.id, block2.id])).to_not exist + end + + it 'does not remove other IP ranges' do + cli.invoke(:remove, arguments, options) + + expect(IpBlock.where(id: block3.id)).to exist + end + end + + context 'when a specified IP address is not blocked' do + let(:unblocked_ip) { '192.0.2.1' } + + it 'skips the IP address' do + expect { cli.invoke(:remove, [unblocked_ip]) }.to output( + a_string_including("#{unblocked_ip} is not yet blocked") + ).to_stdout + end + + it 'displays the summary correctly' do + expect { cli.invoke(:remove, [unblocked_ip]) }.to output( + a_string_including('Removed 0, skipped 1') + ).to_stdout + end + end + + context 'when a specified IP address is invalid' do + let(:invalid_ip) { '320.15.175.0' } + + it 'skips the invalid IP address' do + expect { cli.invoke(:remove, [invalid_ip]) }.to output( + a_string_including("#{invalid_ip} is invalid") + ).to_stdout + end + + it 'displays the summary correctly' do + expect { cli.invoke(:remove, [invalid_ip]) }.to output( + a_string_including('Removed 0, skipped 1') + ).to_stdout + end + end + + context 'when no IP address is provided' do + it 'exits with an error message' do + expect { cli.remove }.to output( + a_string_including('No IP(s) given') + ).to_stdout + .and raise_error(SystemExit) + end + end + end + + describe '#export' do + let(:block1) { IpBlock.create(ip: '192.168.0.0/24', severity: :no_access) } + let(:block2) { IpBlock.create(ip: '10.0.0.0/16', severity: :no_access) } + let(:block3) { IpBlock.create(ip: '127.0.0.1', severity: :sign_up_block) } + + context 'when --format option is set to "plain"' do + let(:options) { { format: 'plain' } } + + it 'exports blocked IPs with "no_access" severity in plain format' do + expect { cli.invoke(:export, nil, options) }.to output( + a_string_including("#{block1.ip}/#{block1.ip.prefix}\n#{block2.ip}/#{block2.ip.prefix}") + ).to_stdout + end + + it 'does not export bloked IPs with different severities' do + expect { cli.invoke(:export, nil, options) }.to_not output( + a_string_including("#{block3.ip}/#{block1.ip.prefix}") + ).to_stdout + end + end + + context 'when --format option is set to "nginx"' do + let(:options) { { format: 'nginx' } } + + it 'exports blocked IPs with "no_access" severity in plain format' do + expect { cli.invoke(:export, nil, options) }.to output( + a_string_including("deny #{block1.ip}/#{block1.ip.prefix};\ndeny #{block2.ip}/#{block2.ip.prefix};") + ).to_stdout + end + + it 'does not export bloked IPs with different severities' do + expect { cli.invoke(:export, nil, options) }.to_not output( + a_string_including("deny #{block3.ip}/#{block1.ip.prefix};") + ).to_stdout + end + end + + context 'when --format option is not provided' do + it 'exports blocked IPs in plain format by default' do + expect { cli.export }.to output( + a_string_including("#{block1.ip}/#{block1.ip.prefix}\n#{block2.ip}/#{block2.ip.prefix}") + ).to_stdout + end + end + end +end diff --git a/spec/lib/mastodon/migration_warning_spec.rb b/spec/lib/mastodon/migration_warning_spec.rb new file mode 100644 index 000000000..4adf0837a --- /dev/null +++ b/spec/lib/mastodon/migration_warning_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'mastodon/migration_warning' + +describe Mastodon::MigrationWarning do + describe 'migration_duration_warning' do + before do + allow(migration).to receive(:valid_environment?).and_return(true) + allow(migration).to receive(:sleep).with(1) + end + + let(:migration) { Class.new(ActiveRecord::Migration[6.1]).extend(described_class) } + + context 'with the default message' do + it 'warns about long migrations' do + expectation = expect { migration.migration_duration_warning } + + expectation.to output(/interrupt this migration/).to_stdout + expectation.to output(/Continuing in 5/).to_stdout + end + end + + context 'with an additional message' do + it 'warns about long migrations' do + expectation = expect { migration.migration_duration_warning('Get ready for it') } + + expectation.to output(/interrupt this migration/).to_stdout + expectation.to output(/Get ready for it/).to_stdout + expectation.to output(/Continuing in 5/).to_stdout + end + end + end +end diff --git a/spec/lib/vacuum/access_tokens_vacuum_spec.rb b/spec/lib/vacuum/access_tokens_vacuum_spec.rb index 6b7234065..54760c41b 100644 --- a/spec/lib/vacuum/access_tokens_vacuum_spec.rb +++ b/spec/lib/vacuum/access_tokens_vacuum_spec.rb @@ -7,9 +7,11 @@ RSpec.describe Vacuum::AccessTokensVacuum do describe '#perform' do let!(:revoked_access_token) { Fabricate(:access_token, revoked_at: 1.minute.ago) } + let!(:expired_access_token) { Fabricate(:access_token, expires_in: 59.minutes.to_i, created_at: 1.hour.ago) } let!(:active_access_token) { Fabricate(:access_token) } let!(:revoked_access_grant) { Fabricate(:access_grant, revoked_at: 1.minute.ago) } + let!(:expired_access_grant) { Fabricate(:access_grant, expires_in: 59.minutes.to_i, created_at: 1.hour.ago) } let!(:active_access_grant) { Fabricate(:access_grant) } before do @@ -20,10 +22,18 @@ RSpec.describe Vacuum::AccessTokensVacuum do expect { revoked_access_token.reload }.to raise_error ActiveRecord::RecordNotFound end + it 'deletes expired access tokens' do + expect { expired_access_token.reload }.to raise_error ActiveRecord::RecordNotFound + end + it 'deletes revoked access grants' do expect { revoked_access_grant.reload }.to raise_error ActiveRecord::RecordNotFound end + it 'deletes expired access grants' do + expect { expired_access_grant.reload }.to raise_error ActiveRecord::RecordNotFound + end + it 'does not delete active access tokens' do expect { active_access_token.reload }.to_not raise_error end diff --git a/spec/locales/i18n_spec.rb b/spec/locales/i18n_spec.rb new file mode 100644 index 000000000..cfce8e223 --- /dev/null +++ b/spec/locales/i18n_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'I18n' do + describe 'Pluralizing locale translations' do + subject { I18n.t('generic.validation_errors', count: 1) } + + context 'with the `en` locale which has `one` and `other` plural values' do + around do |example| + I18n.with_locale(:en) do + example.run + end + end + + it 'translates to `en` correctly and without error' do + expect { subject }.to_not raise_error + expect(subject).to match(/the error below/) + end + end + + context 'with the `my` locale which has only `other` plural value' do + around do |example| + I18n.with_locale(:my) do + example.run + end + end + + it 'translates to `my` correctly and without error' do + expect { subject }.to_not raise_error + expect(subject).to match(/1/) + end + end + end +end diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb index 3113d05c3..73c751def 100644 --- a/spec/mailers/notification_mailer_spec.rb +++ b/spec/mailers/notification_mailer_spec.rb @@ -10,7 +10,7 @@ RSpec.describe NotificationMailer do shared_examples 'localized subject' do |*args, **kwrest| it 'renders subject localized for the locale of the receiver' do - locale = %i(de en).sample + locale = :de receiver.update!(locale: locale) expect(mail.subject).to eq I18n.t(*args, **kwrest.merge(locale: locale)) end diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 6144b2bbb..a4f6c145a 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -7,7 +7,7 @@ describe UserMailer do shared_examples 'localized subject' do |*args, **kwrest| it 'renders subject localized for the locale of the receiver' do - locale = I18n.available_locales.sample + locale = :de receiver.update!(locale: locale) expect(mail.subject).to eq I18n.t(*args, **kwrest.merge(locale: locale)) end diff --git a/spec/models/account_migration_spec.rb b/spec/models/account_migration_spec.rb index 0d97ea7e7..519b9a97a 100644 --- a/spec/models/account_migration_spec.rb +++ b/spec/models/account_migration_spec.rb @@ -25,7 +25,7 @@ RSpec.describe AccountMigration do end end - context 'with unresolveable account' do + context 'with unresolvable account' do let(:target_acct) { 'target@remote' } before do diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index bbe35f579..6e9c608ab 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -698,7 +698,7 @@ RSpec.describe Account do expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil end - xit 'does not match URL querystring' do + xit 'does not match URL query string' do expect(subject.match('https://example.com/?x=@alice')).to be_nil end end diff --git a/spec/models/form/account_batch_spec.rb b/spec/models/form/account_batch_spec.rb new file mode 100644 index 000000000..fd8e90901 --- /dev/null +++ b/spec/models/form/account_batch_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Form::AccountBatch do + let(:account_batch) { described_class.new } + + describe '#save' do + subject { account_batch.save } + + let(:account) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } + let(:account_ids) { [] } + let(:query) { Account.none } + + before do + account_batch.assign_attributes( + action: action, + current_account: account, + account_ids: account_ids, + query: query, + select_all_matching: select_all_matching + ) + end + + context 'when action is "suspend"' do + let(:action) { 'suspend' } + + let(:target_account) { Fabricate(:account) } + let(:target_account2) { Fabricate(:account) } + + before do + Fabricate(:report, target_account: target_account) + Fabricate(:report, target_account: target_account2) + end + + context 'when accounts are passed as account_ids' do + let(:select_all_matching) { '0' } + let(:account_ids) { [target_account.id, target_account2.id] } + + it 'suspends the expected users' do + expect { subject }.to change { [target_account.reload.suspended?, target_account2.reload.suspended?] }.from([false, false]).to([true, true]) + end + + it 'closes open reports targeting the suspended users' do + expect { subject }.to change { Report.unresolved.where(target_account: [target_account, target_account2]).count }.from(2).to(0) + end + end + + context 'when accounts are passed as a query' do + let(:select_all_matching) { '1' } + let(:query) { Account.where(id: [target_account.id, target_account2.id]) } + + it 'suspends the expected users' do + expect { subject }.to change { [target_account.reload.suspended?, target_account2.reload.suspended?] }.from([false, false]).to([true, true]) + end + + it 'closes open reports targeting the suspended users' do + expect { subject }.to change { Report.unresolved.where(target_account: [target_account, target_account2]).count }.from(2).to(0) + end + end + end + end +end diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index b006f60bb..0093dcd8d 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -121,10 +121,17 @@ describe Report do end describe 'validations' do - it 'is invalid if comment is longer than 1000 characters' do + let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } + + it 'is invalid if comment is longer than 1000 characters only if reporter is local' do report = Fabricate.build(:report, comment: Faker::Lorem.characters(number: 1001)) - report.valid? + expect(report.valid?).to be false expect(report).to model_have_error_on_field(:comment) end + + it 'is valid if comment is longer than 1000 characters and reporter is not local' do + report = Fabricate.build(:report, account: remote_account, comment: Faker::Lorem.characters(number: 1001)) + expect(report.valid?).to be true + end end end diff --git a/spec/models/user_settings/setting_spec.rb b/spec/models/user_settings/setting_spec.rb index 9884ae4f8..8c8d31ec5 100644 --- a/spec/models/user_settings/setting_spec.rb +++ b/spec/models/user_settings/setting_spec.rb @@ -90,7 +90,7 @@ RSpec.describe UserSettings::Setting do describe '#key' do context 'when there is no namespace' do - it 'returnsn a symbol' do + it 'returns a symbol' do expect(subject.key).to eq :foo end end diff --git a/spec/policies/report_note_policy_spec.rb b/spec/policies/report_note_policy_spec.rb index ea2a62ada..a657fce4b 100644 --- a/spec/policies/report_note_policy_spec.rb +++ b/spec/policies/report_note_policy_spec.rb @@ -30,19 +30,17 @@ RSpec.describe ReportNotePolicy do end end - context 'when admin?' do - context 'when owner?' do - it 'permit' do - report_note = Fabricate(:report_note, account: john) - expect(subject).to permit(john, report_note) - end + context 'when owner?' do + it 'permit' do + report_note = Fabricate(:report_note, account: john) + expect(subject).to permit(john, report_note) end + end - context 'with !owner?' do - it 'denies' do - report_note = Fabricate(:report_note) - expect(subject).to_not permit(john, report_note) - end + context 'with !owner?' do + it 'denies' do + report_note = Fabricate(:report_note) + expect(subject).to_not permit(john, report_note) end end end diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb index 38b9c4fdb..725bd0bbb 100644 --- a/spec/policies/status_policy_spec.rb +++ b/spec/policies/status_policy_spec.rb @@ -11,139 +11,151 @@ RSpec.describe StatusPolicy, type: :model do let(:bob) { Fabricate(:account, username: 'bob') } let(:status) { Fabricate(:status, account: alice) } - permissions :show?, :reblog? do - it 'grants access when no viewer' do - expect(subject).to permit(nil, status) - end + context 'with the permissions of show? and reblog?' do + permissions :show?, :reblog? do + it 'grants access when no viewer' do + expect(subject).to permit(nil, status) + end - it 'denies access when viewer is blocked' do - block = Fabricate(:block) - status.visibility = :private - status.account = block.target_account + it 'denies access when viewer is blocked' do + block = Fabricate(:block) + status.visibility = :private + status.account = block.target_account - expect(subject).to_not permit(block.account, status) + expect(subject).to_not permit(block.account, status) + end end end - permissions :show? do - it 'grants access when direct and account is viewer' do - status.visibility = :direct + context 'with the permission of show?' do + permissions :show? do + it 'grants access when direct and account is viewer' do + status.visibility = :direct - expect(subject).to permit(status.account, status) - end + expect(subject).to permit(status.account, status) + end - it 'grants access when direct and viewer is mentioned' do - status.visibility = :direct - status.mentions = [Fabricate(:mention, account: alice)] + it 'grants access when direct and viewer is mentioned' do + status.visibility = :direct + status.mentions = [Fabricate(:mention, account: alice)] - expect(subject).to permit(alice, status) - end + expect(subject).to permit(alice, status) + end - it 'grants access when direct and non-owner viewer is mentioned and mentions are loaded' do - status.visibility = :direct - status.mentions = [Fabricate(:mention, account: bob)] - status.mentions.load + it 'grants access when direct and non-owner viewer is mentioned and mentions are loaded' do + status.visibility = :direct + status.mentions = [Fabricate(:mention, account: bob)] + status.mentions.load - expect(subject).to permit(bob, status) - end + expect(subject).to permit(bob, status) + end - it 'denies access when direct and viewer is not mentioned' do - viewer = Fabricate(:account) - status.visibility = :direct + it 'denies access when direct and viewer is not mentioned' do + viewer = Fabricate(:account) + status.visibility = :direct - expect(subject).to_not permit(viewer, status) - end + expect(subject).to_not permit(viewer, status) + end - it 'grants access when private and account is viewer' do - status.visibility = :private + it 'grants access when private and account is viewer' do + status.visibility = :private - expect(subject).to permit(status.account, status) - end + expect(subject).to permit(status.account, status) + end - it 'grants access when private and account is following viewer' do - follow = Fabricate(:follow) - status.visibility = :private - status.account = follow.target_account + it 'grants access when private and account is following viewer' do + follow = Fabricate(:follow) + status.visibility = :private + status.account = follow.target_account - expect(subject).to permit(follow.account, status) - end + expect(subject).to permit(follow.account, status) + end - it 'grants access when private and viewer is mentioned' do - status.visibility = :private - status.mentions = [Fabricate(:mention, account: alice)] + it 'grants access when private and viewer is mentioned' do + status.visibility = :private + status.mentions = [Fabricate(:mention, account: alice)] - expect(subject).to permit(alice, status) - end + expect(subject).to permit(alice, status) + end - it 'denies access when private and viewer is not mentioned or followed' do - viewer = Fabricate(:account) - status.visibility = :private + it 'denies access when private and viewer is not mentioned or followed' do + viewer = Fabricate(:account) + status.visibility = :private - expect(subject).to_not permit(viewer, status) - end + expect(subject).to_not permit(viewer, status) + end - it 'denies access when local-only and the viewer is not logged in' do - allow(status).to receive(:local_only?).and_return(true) + it 'denies access when local-only and the viewer is not logged in' do + allow(status).to receive(:local_only?).and_return(true) - expect(subject).to_not permit(nil, status) - end + expect(subject).to_not permit(nil, status) + end - it 'denies access when local-only and the viewer is from another domain' do - viewer = Fabricate(:account, domain: 'remote-domain') - allow(status).to receive(:local_only?).and_return(true) - expect(subject).to_not permit(viewer, status) + it 'denies access when local-only and the viewer is from another domain' do + viewer = Fabricate(:account, domain: 'remote-domain') + allow(status).to receive(:local_only?).and_return(true) + expect(subject).to_not permit(viewer, status) + end end end - permissions :reblog? do - it 'denies access when private' do - viewer = Fabricate(:account) - status.visibility = :private + context 'with the permission of reblog?' do + permissions :reblog? do + it 'denies access when private' do + viewer = Fabricate(:account) + status.visibility = :private - expect(subject).to_not permit(viewer, status) - end + expect(subject).to_not permit(viewer, status) + end - it 'denies access when direct' do - viewer = Fabricate(:account) - status.visibility = :direct + it 'denies access when direct' do + viewer = Fabricate(:account) + status.visibility = :direct - expect(subject).to_not permit(viewer, status) + expect(subject).to_not permit(viewer, status) + end end end - permissions :destroy?, :unreblog? do - it 'grants access when account is deleter' do - expect(subject).to permit(status.account, status) - end + context 'with the permissions of destroy? and unreblog?' do + permissions :destroy?, :unreblog? do + it 'grants access when account is deleter' do + expect(subject).to permit(status.account, status) + end - it 'denies access when account is not deleter' do - expect(subject).to_not permit(bob, status) - end + it 'denies access when account is not deleter' do + expect(subject).to_not permit(bob, status) + end - it 'denies access when no deleter' do - expect(subject).to_not permit(nil, status) + it 'denies access when no deleter' do + expect(subject).to_not permit(nil, status) + end end end - permissions :favourite? do - it 'grants access when viewer is not blocked' do - follow = Fabricate(:follow) - status.account = follow.target_account + context 'with the permission of favourite?' do + permissions :favourite? do + it 'grants access when viewer is not blocked' do + follow = Fabricate(:follow) + status.account = follow.target_account - expect(subject).to permit(follow.account, status) - end + expect(subject).to permit(follow.account, status) + end - it 'denies when viewer is blocked' do - block = Fabricate(:block) - status.account = block.target_account + it 'denies when viewer is blocked' do + block = Fabricate(:block) + status.account = block.target_account - expect(subject).to_not permit(block.account, status) + expect(subject).to_not permit(block.account, status) + end end end - permissions :update? do - it 'grants access if owner' do - expect(subject).to permit(status.account, status) + context 'with the permission of update?' do + permissions :update? do + it 'grants access if owner' do + expect(subject).to permit(status.account, status) + end end end end diff --git a/spec/presenters/status_relationships_presenter_spec.rb b/spec/presenters/status_relationships_presenter_spec.rb index 11116cabd..a62fa004a 100644 --- a/spec/presenters/status_relationships_presenter_spec.rb +++ b/spec/presenters/status_relationships_presenter_spec.rb @@ -15,7 +15,7 @@ RSpec.describe StatusRelationshipsPresenter do let(:presenter) { StatusRelationshipsPresenter.new(statuses, current_account_id, **options) } let(:current_account_id) { Fabricate(:account).id } let(:statuses) { [Fabricate(:status)] } - let(:status_ids) { statuses.map(&:id) + statuses.map(&:reblog_of_id).compact } + let(:status_ids) { statuses.map(&:id) + statuses.filter_map(&:reblog_of_id) } let(:default_map) { { 1 => true } } context 'when options are not set' do diff --git a/spec/requests/api/v1/featured_tags_spec.rb b/spec/requests/api/v1/featured_tags_spec.rb new file mode 100644 index 000000000..8a552c1d4 --- /dev/null +++ b/spec/requests/api/v1/featured_tags_spec.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'FeaturedTags' do + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:scopes) { 'read:accounts write:accounts' } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + describe 'GET /api/v1/featured_tags' do + context 'with wrong scope' do + before do + get '/api/v1/featured_tags', headers: headers + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'when Authorization header is missing' do + it 'returns http unauthorized' do + get '/api/v1/featured_tags' + + expect(response).to have_http_status(401) + end + end + + it 'returns http success' do + get '/api/v1/featured_tags', headers: headers + + expect(response).to have_http_status(200) + end + + context 'when the requesting user has no featured tag' do + before { Fabricate.times(3, :featured_tag) } + + it 'returns an empty body' do + get '/api/v1/featured_tags', headers: headers + + body = body_as_json + + expect(body).to be_empty + end + end + + context 'when the requesting user has featured tags' do + let!(:user_featured_tags) { Fabricate.times(5, :featured_tag, account: user.account) } + + it 'returns only the featured tags belonging to the requesting user' do + get '/api/v1/featured_tags', headers: headers + + body = body_as_json + expected_ids = user_featured_tags.pluck(:id).map(&:to_s) + + expect(body.pluck(:id)).to match_array(expected_ids) + end + end + end + + describe 'POST /api/v1/featured_tags' do + let(:params) { { name: 'tag' } } + + it 'returns http success' do + post '/api/v1/featured_tags', headers: headers, params: params + + expect(response).to have_http_status(200) + end + + it 'returns the correct tag name' do + post '/api/v1/featured_tags', headers: headers, params: params + + body = body_as_json + + expect(body[:name]).to eq(params[:name]) + end + + it 'creates a new featured tag for the requesting user' do + post '/api/v1/featured_tags', headers: headers, params: params + + featured_tag = FeaturedTag.find_by(name: params[:name], account: user.account) + + expect(featured_tag).to be_present + end + + context 'with wrong scope' do + before do + post '/api/v1/featured_tags', headers: headers, params: params + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'when Authorization header is missing' do + it 'returns http unauthorized' do + post '/api/v1/featured_tags', params: params + + expect(response).to have_http_status(401) + end + end + + context 'when required param "name" is not provided' do + it 'returns http bad request' do + post '/api/v1/featured_tags', headers: headers + + expect(response).to have_http_status(400) + end + end + + context 'when provided tag name is invalid' do + let(:params) { { name: 'asj&*!' } } + + it 'returns http unprocessable entity' do + post '/api/v1/featured_tags', headers: headers, params: params + + expect(response).to have_http_status(422) + end + end + + context 'when tag name is already taken' do + before do + FeaturedTag.create(name: params[:name], account: user.account) + end + + it 'returns http unprocessable entity' do + post '/api/v1/featured_tags', headers: headers, params: params + + expect(response).to have_http_status(422) + end + end + end + + describe 'DELETE /api/v1/featured_tags' do + let!(:featured_tag) { FeaturedTag.create(name: 'tag', account: user.account) } + let(:id) { featured_tag.id } + + it 'returns http success' do + delete "/api/v1/featured_tags/#{id}", headers: headers + + expect(response).to have_http_status(200) + end + + it 'returns an empty body' do + delete "/api/v1/featured_tags/#{id}", headers: headers + + body = body_as_json + + expect(body).to be_empty + end + + it 'deletes the featured tag' do + delete "/api/v1/featured_tags/#{id}", headers: headers + + featured_tag = FeaturedTag.find_by(id: id) + + expect(featured_tag).to be_nil + end + + context 'with wrong scope' do + before do + delete "/api/v1/featured_tags/#{id}", headers: headers + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'when Authorization header is missing' do + it 'returns http unauthorized' do + delete "/api/v1/featured_tags/#{id}" + + expect(response).to have_http_status(401) + end + end + + context 'when featured tag with given id does not exist' do + it 'returns http not found' do + delete '/api/v1/featured_tags/0', headers: headers + + expect(response).to have_http_status(404) + end + end + + context 'when deleting a featured tag of another user' do + let!(:other_user_featured_tag) { Fabricate(:featured_tag) } + let(:id) { other_user_featured_tag.id } + + it 'returns http not found' do + delete "/api/v1/featured_tags/#{id}", headers: headers + + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb index ffbc51b64..db454d7ad 100644 --- a/spec/services/activitypub/process_account_service_spec.rb +++ b/spec/services/activitypub/process_account_service_spec.rb @@ -139,10 +139,6 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do end context 'when Accounts referencing other accounts' do - before do - stub_const 'ActivityPub::ProcessAccountService::DISCOVERIES_PER_REQUEST', 5 - end - let(:payload) do { '@context': ['https://www.w3.org/ns/activitystreams'], @@ -155,6 +151,8 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do end before do + stub_const 'ActivityPub::ProcessAccountService::DISCOVERIES_PER_REQUEST', 5 + 8.times do |i| actor_json = { '@context': ['https://www.w3.org/ns/activitystreams'], @@ -183,7 +181,7 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do '@context': ['https://www.w3.org/ns/activitystreams'], id: "https://foo.test/users/#{i}/featured", type: 'OrderedCollection', - totelItems: 1, + totalItems: 1, orderedItems: [status_json], }.with_indifferent_access webfinger = { diff --git a/spec/services/backup_service_spec.rb b/spec/services/backup_service_spec.rb index b961b7f67..73e0b42ad 100644 --- a/spec/services/backup_service_spec.rb +++ b/spec/services/backup_service_spec.rb @@ -21,6 +21,27 @@ RSpec.describe BackupService, type: :service do end end + context 'when the user has an avatar and header' do + before do + user.account.update!(avatar: attachment_fixture('avatar.gif')) + user.account.update!(header: attachment_fixture('emojo.png')) + end + + it 'stores them as expected' do + service_call + + json = Oj.load(read_zip_file(backup, 'actor.json')) + avatar_path = json.dig('icon', 'url') + header_path = json.dig('image', 'url') + + expect(avatar_path).to_not be_nil + expect(header_path).to_not be_nil + + expect(read_zip_file(backup, avatar_path)).to be_present + expect(read_zip_file(backup, header_path)).to be_present + end + end + it 'marks the backup as processed' do expect { service_call }.to change(backup, :processed).from(false).to(true) end diff --git a/spec/services/report_service_spec.rb b/spec/services/report_service_spec.rb index 29207462a..b8ceedb85 100644 --- a/spec/services/report_service_spec.rb +++ b/spec/services/report_service_spec.rb @@ -6,6 +6,14 @@ RSpec.describe ReportService, type: :service do subject { described_class.new } let(:source_account) { Fabricate(:account) } + let(:target_account) { Fabricate(:account) } + + context 'with a local account' do + it 'has a uri' do + report = subject.call(source_account, target_account) + expect(report.uri).to_not be_nil + end + end context 'with a remote account' do let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } @@ -35,7 +43,6 @@ RSpec.describe ReportService, type: :service do -> { described_class.new.call(source_account, target_account, status_ids: [status.id]) } end - let(:target_account) { Fabricate(:account) } let(:status) { Fabricate(:status, account: target_account, visibility: :direct) } context 'when it is addressed to the reporter' do @@ -91,8 +98,7 @@ RSpec.describe ReportService, type: :service do -> { described_class.new.call(source_account, target_account) } end - let!(:target_account) { Fabricate(:account) } - let!(:other_report) { Fabricate(:report, target_account: target_account) } + let!(:other_report) { Fabricate(:report, target_account: target_account) } before do ActionMailer::Base.deliveries.clear diff --git a/spec/services/unsuspend_account_service_spec.rb b/spec/services/unsuspend_account_service_spec.rb index 5d7012093..80285cc12 100644 --- a/spec/services/unsuspend_account_service_spec.rb +++ b/spec/services/unsuspend_account_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe UnsuspendAccountService, type: :service do - shared_examples 'common behavior' do + shared_context 'with common context' do subject { described_class.new.call(account) } let!(:local_follower) { Fabricate(:user, current_sign_in_at: 1.hour.ago).account } @@ -36,7 +36,7 @@ RSpec.describe UnsuspendAccountService, type: :service do expect { subject }.to_not change { account.suspended? } end - include_examples 'common behavior' do + include_examples 'with common context' do let!(:account) { Fabricate(:account) } let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub) } let!(:remote_reporter) { Fabricate(:account, uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) } @@ -61,7 +61,7 @@ RSpec.describe UnsuspendAccountService, type: :service do end describe 'unsuspending a remote account' do - include_examples 'common behavior' do + include_examples 'with common context' do let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) } let!(:resolve_account_service) { double } diff --git a/tsconfig.json b/tsconfig.json index 022ab1f71..d8f425f16 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,8 @@ "baseUrl": "./", "paths": { "locales": ["app/javascript/locales"], + "styles/*": ["app/javascript/styles/*"], + "packs/public-path": ["app/javascript/packs/public-path"], "flavours/glitch": ["app/javascript/flavours/glitch"], "flavours/glitch/*": ["app/javascript/flavours/glitch/*"], "mastodon": ["app/javascript/mastodon"], diff --git a/yarn.lock b/yarn.lock index 9c51955da..5cd2b4148 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1055,14 +1055,6 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime-corejs3@^7.10.2": - version "7.10.3" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.3.tgz#931ed6941d3954924a7aa967ee440e60c507b91a" - integrity sha512-HA7RPj5xvJxQl429r5Cxr2trJwOfPjKiqhCXcdQPSqO2G0RHPZpXu4fkYmBaTKCp2c/jRaMK9GB/lN+7zvvFPw== - dependencies: - core-js-pure "^3.0.0" - regenerator-runtime "^0.13.4" - "@babel/runtime@7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0.tgz#adeb78fedfc855aa05bc041640f3f6f98e85424c" @@ -1070,7 +1062,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200" integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q== @@ -1224,10 +1216,10 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== -"@es-joy/jsdoccomment@~0.37.1": - version "0.37.1" - resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.37.1.tgz#fa32a41ba12097452693343e09ad4d26d157aedd" - integrity sha512-5vxWJ1gEkEF0yRd0O+uK6dHJf7adrxwQSX8PuRiPfFSAbNLnY0ZJfXaZucoz14Jj2N11xn2DnlEPwWRpYpvRjg== +"@es-joy/jsdoccomment@~0.39.3": + version "0.39.3" + resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.39.3.tgz#76b55203bf447d608e4e299ecb62d7ef14db72bb" + integrity sha512-q6pObzaS+aTA96kl4DF91QILNpSiDE8S89cQdJnhIc7hWzwIHPnfBnsiBVa0Z/R9pLHdZTnXEMnggGMmCq7HmA== dependencies: comment-parser "1.3.1" esquery "^1.5.0" @@ -1245,14 +1237,14 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.4.0.tgz#3e61c564fcd6b921cb789838631c5ee44df09403" integrity sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ== -"@eslint/eslintrc@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.2.tgz#01575e38707add677cf73ca1589abba8da899a02" - integrity sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ== +"@eslint/eslintrc@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331" + integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.5.1" + espree "^9.5.2" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -1260,10 +1252,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.39.0": - version "8.39.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b" - integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng== +"@eslint/js@8.40.0": + version "8.40.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.40.0.tgz#3ba73359e11f5a7bd3e407f70b3528abfae69cec" + integrity sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA== "@floating-ui/core@^1.0.1": version "1.0.1" @@ -1678,6 +1670,18 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@pkgr/utils@^2.3.1": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.4.0.tgz#b6373d2504aedaf2fc7cdf2d13ab1f48fa5f12d5" + integrity sha512-2OCURAmRtdlL8iUDTypMrrxfwe8frXTeXaxGsVOaYtc/wrUyk8Z/0OBetM7cdlsy7ZFWlMX72VogKeh+A4Xcjw== + dependencies: + cross-spawn "^7.0.3" + fast-glob "^3.2.12" + is-glob "^4.0.3" + open "^9.1.0" + picocolors "^1.0.0" + tslib "^2.5.0" + "@polka/url@^1.0.0-next.9": version "1.0.0-next.11" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71" @@ -1810,18 +1814,18 @@ magic-string "^0.25.0" string.prototype.matchall "^4.0.6" -"@testing-library/dom@^8.0.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.1.0.tgz#f8358b1883844ea569ba76b7e94582168df5370d" - integrity sha512-kmW9alndr19qd6DABzQ978zKQ+J65gU2Rzkl8hriIetPnwpesRaK4//jEQyYh8fEALmGhomD/LBQqt+o+DL95Q== +"@testing-library/dom@^9.0.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.2.0.tgz#0e1f45e956f2a16f471559c06edd8827c4832f04" + integrity sha512-xTEnpUKiV/bMyEsE5bT4oYA0x0Z/colMtxzUY8bKyPXBNLn/e0V4ZjBZkEhms0xE4pv9QsPfSRu9AWS4y5wGvA== dependencies: "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.12.5" - "@types/aria-query" "^4.2.0" - aria-query "^4.2.2" + "@types/aria-query" "^5.0.1" + aria-query "^5.0.0" chalk "^4.1.0" - dom-accessibility-api "^0.5.6" - lz-string "^1.4.4" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" pretty-format "^27.0.2" "@testing-library/jest-dom@^5.16.5": @@ -1839,14 +1843,14 @@ lodash "^4.17.15" redent "^3.0.0" -"@testing-library/react@^12.1.5": - version "12.1.5" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b" - integrity sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg== +"@testing-library/react@^14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.0.0.tgz#59030392a6792450b9ab8e67aea5f3cc18d6347c" + integrity sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg== dependencies: "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^8.0.0" - "@types/react-dom" "<18.0.0" + "@testing-library/dom" "^9.0.0" + "@types/react-dom" "^18.0.0" "@tootallnate/once@2": version "2.0.0" @@ -1858,10 +1862,10 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@types/aria-query@^4.2.0": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0" - integrity sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A== +"@types/aria-query@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" + integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q== "@types/babel__core@^7.1.12", "@types/babel__core@^7.1.14", "@types/babel__core@^7.1.3": version "7.1.18" @@ -2172,29 +2176,17 @@ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== -"@types/raf@^3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2" - integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw== - "@types/range-parser@*": version "1.2.4" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-dom@<18.0.0": - version "17.0.15" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.15.tgz#f2c8efde11521a4b7991e076cb9c70ba3bb0d156" - integrity sha512-Tr9VU9DvNoHDWlmecmcsE5ZZiUkYx+nKBzum4Oxe1K0yJVyBlfbq7H3eXjxXqJczBKqPGq3EgfTru4MgKb9+Yw== +"@types/react-dom@^18.0.0", "@types/react-dom@^18.2.4": + version "18.2.4" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.4.tgz#13f25bfbf4e404d26f62ac6e406591451acba9e0" + integrity sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw== dependencies: - "@types/react" "^17" - -"@types/react-dom@^16.9.18": - version "16.9.18" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.18.tgz#1fda8b84370b1339d639a797a84c16d5a195b419" - integrity sha512-lmNARUX3+rNF/nmoAFqasG0jAA7q6MeGZK/fdeLwY3kAA4NPgHHrG5bNQe2B5xmD4B+x6Z6h0rEJQ7MEEgQxsw== - dependencies: - "@types/react" "^16" + "@types/react" "*" "@types/react-helmet@^6.1.6": version "6.1.6" @@ -2316,28 +2308,10 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^17": - version "17.0.44" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.44.tgz#c3714bd34dd551ab20b8015d9d0dbec812a51ec7" - integrity sha512-Ye0nlw09GeMp2Suh8qoOv0odfgCoowfM/9MG6WeRD60Gq9wS90bdkdRtYbRkNhXOpG4H+YXGvj4wOWhAC0LJ1g== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@>=16.9.11": - version "18.0.26" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.26.tgz#8ad59fc01fef8eaf5c74f4ea392621749f0b7917" - integrity sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@^16", "@types/react@^16.14.38": - version "16.14.38" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.38.tgz#b814d157ca8906603593d5106f6d733af9b79df4" - integrity sha512-PbEjuhwkdH6IB5Sak6BFAqpVMHY/wJxa0EG3bKkr0vWA2hSDIq3iEMhHyqjXrDFMqRzkiQkdyNXOnoELrh/9aQ== +"@types/react@*", "@types/react@>=16.9.11", "@types/react@^18.0.26": + version "18.2.6" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.6.tgz#5cd53ee0d30ffc193b159d3516c8c8ad2f19d571" + integrity sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -2475,15 +2449,15 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.5.tgz#f156827610a3f8cefc56baeaa93cd4a5f32966b4" - integrity sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg== +"@typescript-eslint/eslint-plugin@^5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.7.tgz#e470af414f05ecfdc05a23e9ce6ec8f91db56fe2" + integrity sha512-BL+jYxUFIbuYwy+4fF86k5vdT9lT0CNJ6HtwrIvGh0PhH8s0yy5rjaKH2fDCrz5ITHy07WCzVGNvAmjJh4IJFA== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.5" - "@typescript-eslint/type-utils" "5.59.5" - "@typescript-eslint/utils" "5.59.5" + "@typescript-eslint/scope-manager" "5.59.7" + "@typescript-eslint/type-utils" "5.59.7" + "@typescript-eslint/utils" "5.59.7" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" @@ -2491,31 +2465,31 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.5.tgz#63064f5eafbdbfb5f9dfbf5c4503cdf949852981" - integrity sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw== +"@typescript-eslint/parser@^5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.7.tgz#02682554d7c1028b89aa44a48bf598db33048caa" + integrity sha512-VhpsIEuq/8i5SF+mPg9jSdIwgMBBp0z9XqjiEay+81PYLJuroN+ET1hM5IhkiYMJd9MkTz8iJLt7aaGAgzWUbQ== dependencies: - "@typescript-eslint/scope-manager" "5.59.5" - "@typescript-eslint/types" "5.59.5" - "@typescript-eslint/typescript-estree" "5.59.5" + "@typescript-eslint/scope-manager" "5.59.7" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/typescript-estree" "5.59.7" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz#33ffc7e8663f42cfaac873de65ebf65d2bce674d" - integrity sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A== +"@typescript-eslint/scope-manager@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz#0243f41f9066f3339d2f06d7f72d6c16a16769e2" + integrity sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ== dependencies: - "@typescript-eslint/types" "5.59.5" - "@typescript-eslint/visitor-keys" "5.59.5" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/visitor-keys" "5.59.7" -"@typescript-eslint/type-utils@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.5.tgz#485b0e2c5b923460bc2ea6b338c595343f06fc9b" - integrity sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg== +"@typescript-eslint/type-utils@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.7.tgz#89c97291371b59eb18a68039857c829776f1426d" + integrity sha512-ozuz/GILuYG7osdY5O5yg0QxXUAEoI4Go3Do5xeu+ERH9PorHBPSdvD3Tjp2NN2bNLh1NJQSsQu2TPu/Ly+HaQ== dependencies: - "@typescript-eslint/typescript-estree" "5.59.5" - "@typescript-eslint/utils" "5.59.5" + "@typescript-eslint/typescript-estree" "5.59.7" + "@typescript-eslint/utils" "5.59.7" debug "^4.3.4" tsutils "^3.21.0" @@ -2524,10 +2498,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.0.tgz#3fcdac7dbf923ec5251545acdd9f1d42d7c4fe32" integrity sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA== -"@typescript-eslint/types@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.5.tgz#e63c5952532306d97c6ea432cee0981f6d2258c7" - integrity sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w== +"@typescript-eslint/types@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.7.tgz#6f4857203fceee91d0034ccc30512d2939000742" + integrity sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A== "@typescript-eslint/typescript-estree@5.59.0": version "5.59.0" @@ -2542,30 +2516,30 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz#9b252ce55dd765e972a7a2f99233c439c5101e42" - integrity sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg== +"@typescript-eslint/typescript-estree@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz#b887acbd4b58e654829c94860dbff4ac55c5cff8" + integrity sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ== dependencies: - "@typescript-eslint/types" "5.59.5" - "@typescript-eslint/visitor-keys" "5.59.5" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/visitor-keys" "5.59.7" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.5.tgz#15b3eb619bb223302e60413adb0accd29c32bcae" - integrity sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA== +"@typescript-eslint/utils@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.7.tgz#7adf068b136deae54abd9a66ba5a8780d2d0f898" + integrity sha512-yCX9WpdQKaLufz5luG4aJbOpdXf/fjwGMcLFXZVPUz3QqLirG5QcwwnIHNf8cjLjxK4qtzTO8udUtMQSAToQnQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.59.5" - "@typescript-eslint/types" "5.59.5" - "@typescript-eslint/typescript-estree" "5.59.5" + "@typescript-eslint/scope-manager" "5.59.7" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/typescript-estree" "5.59.7" eslint-scope "^5.1.1" semver "^7.3.7" @@ -2577,12 +2551,12 @@ "@typescript-eslint/types" "5.59.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz#ba5b8d6791a13cf9fea6716af1e7626434b29b9b" - integrity sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA== +"@typescript-eslint/visitor-keys@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz#09c36eaf268086b4fbb5eb9dc5199391b6485fc5" + integrity sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ== dependencies: - "@typescript-eslint/types" "5.59.5" + "@typescript-eslint/types" "5.59.7" eslint-visitor-keys "^3.3.0" "@webassemblyjs/ast@1.9.0": @@ -2788,7 +2762,7 @@ acorn@^6.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== -acorn@^8.0.4, acorn@^8.1.0, acorn@^8.5.0, acorn@^8.8.0, acorn@^8.8.1, acorn@^8.8.2: +acorn@^8.0.4, acorn@^8.1.0, acorn@^8.5.0, acorn@^8.8.0, acorn@^8.8.1: version "8.8.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== @@ -2968,14 +2942,6 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" - integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== - dependencies: - "@babel/runtime" "^7.10.2" - "@babel/runtime-corejs3" "^7.10.2" - aria-query@^5.0.0, aria-query@^5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" @@ -3379,6 +3345,11 @@ batch@0.6.1: resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= +big-integer@^1.6.44: + version "1.6.51" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" + integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -3461,6 +3432,13 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= +bplist-parser@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.2.0.tgz#43a9d183e5bf9d545200ceac3e712f79ebbe8d0e" + integrity sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw== + dependencies: + big-integer "^1.6.44" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -3636,6 +3614,13 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +bundle-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-3.0.0.tgz#ba59bcc9ac785fb67ccdbf104a2bf60c099f0e1a" + integrity sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw== + dependencies: + run-applescript "^5.0.0" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -4130,11 +4115,6 @@ core-js-compat@^3.25.1: dependencies: browserslist "^4.21.4" -core-js-pure@^3.0.0: - version "3.6.5" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" - integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== - core-js@^2.5.0: version "2.6.12" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" @@ -4536,6 +4516,24 @@ deepmerge@^4.0, deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +default-browser-id@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-3.0.0.tgz#bee7bbbef1f4e75d31f98f4d3f1556a14cea790c" + integrity sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA== + dependencies: + bplist-parser "^0.2.0" + untildify "^4.0.0" + +default-browser@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-4.0.0.tgz#53c9894f8810bf86696de117a6ce9085a3cbc7da" + integrity sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA== + dependencies: + bundle-name "^3.0.0" + default-browser-id "^3.0.0" + execa "^7.1.1" + titleize "^3.0.0" + default-gateway@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" @@ -4544,6 +4542,11 @@ default-gateway@^4.2.0: execa "^1.0.0" ip-regex "^2.1.0" +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== + define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" @@ -4712,6 +4715,11 @@ dom-accessibility-api@^0.5.6: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz#3f5d43b52c7a3bd68b5fb63fa47b4e4c1fdf65a9" integrity sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw== +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + dom-helpers@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" @@ -4882,6 +4890,14 @@ enhanced-resolve@^4.1.1, enhanced-resolve@^4.5.0: memory-fs "^0.5.0" tapable "^1.0.0" +enhanced-resolve@^5.12.0: + version "5.13.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz#26d1ecc448c02de997133217b5c1053f34a0a275" + integrity sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + entities@^4.2.0, entities@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" @@ -5020,6 +5036,20 @@ eslint-import-resolver-node@^0.3.7: is-core-module "^2.11.0" resolve "^1.22.1" +eslint-import-resolver-typescript@^3.5.5: + version "3.5.5" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.5.tgz#0a9034ae7ed94b254a360fbea89187b60ea7456d" + integrity sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw== + dependencies: + debug "^4.3.4" + enhanced-resolve "^5.12.0" + eslint-module-utils "^2.7.4" + get-tsconfig "^4.5.0" + globby "^13.1.3" + is-core-module "^2.11.0" + is-glob "^4.0.3" + synckit "^0.8.5" + eslint-module-utils@^2.7.4: version "2.7.4" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" @@ -5065,18 +5095,18 @@ eslint-plugin-import@~2.27.5: semver "^6.3.0" tsconfig-paths "^3.14.1" -eslint-plugin-jsdoc@^43.1.1: - version "43.1.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-43.1.1.tgz#fc72ba21597cc99b1a0dc988aebb9bb57d0ec492" - integrity sha512-J2kjjsJ5vBXSyNzqJhceeSGTAgVgZHcPSJKo3vD4tNjUdfky98rR2VfZUDsS1GKL6isyVa8GWvr+Az7Vyg2HXA== +eslint-plugin-jsdoc@^44.2.4: + version "44.2.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-44.2.4.tgz#0bdc163771504ec7330414eda6a7dbae67156ddb" + integrity sha512-/EMMxCyRh1SywhCb66gAqoGX4Yv6Xzc4bsSkF1AiY2o2+bQmGMQ05QZ5+JjHbdFTPDZY9pfn+DsSNP0a5yQpIg== dependencies: - "@es-joy/jsdoccomment" "~0.37.1" + "@es-joy/jsdoccomment" "~0.39.3" are-docs-informative "^0.0.2" comment-parser "1.3.1" debug "^4.3.4" escape-string-regexp "^4.0.0" esquery "^1.5.0" - semver "^7.5.0" + semver "^7.5.1" spdx-expression-parse "^3.0.1" eslint-plugin-jsx-a11y@~6.7.1: @@ -5163,20 +5193,20 @@ eslint-scope@^7.2.0: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc" - integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ== +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" + integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint@^8.39.0: - version "8.39.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.39.0.tgz#7fd20a295ef92d43809e914b70c39fd5a23cf3f1" - integrity sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og== +eslint@^8.40.0: + version "8.40.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.40.0.tgz#a564cd0099f38542c4e9a2f630fa45bf33bc42a4" + integrity sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.0.2" - "@eslint/js" "8.39.0" + "@eslint/eslintrc" "^2.0.3" + "@eslint/js" "8.40.0" "@humanwhocodes/config-array" "^0.11.8" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -5187,8 +5217,8 @@ eslint@^8.39.0: doctrine "^3.0.0" escape-string-regexp "^4.0.0" eslint-scope "^7.2.0" - eslint-visitor-keys "^3.4.0" - espree "^9.5.1" + eslint-visitor-keys "^3.4.1" + espree "^9.5.2" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -5214,14 +5244,14 @@ eslint@^8.39.0: strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.5.1: - version "9.5.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.1.tgz#4f26a4d5f18905bf4f2e0bd99002aab807e96dd4" - integrity sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg== +espree@^9.5.2: + version "9.5.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" + integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== dependencies: acorn "^8.8.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.0" + eslint-visitor-keys "^3.4.1" esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" @@ -5325,7 +5355,7 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -execa@^7.0.0: +execa@^7.0.0, execa@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-7.1.1.tgz#3eb3c83d239488e7b409d48e8813b76bb55c9c43" integrity sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q== @@ -5457,7 +5487,7 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== -fast-glob@^3.2.12, fast-glob@^3.2.9: +fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== @@ -5829,6 +5859,11 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +get-tsconfig@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.5.0.tgz#6d52d1c7b299bd3ee9cd7638561653399ac77b0f" + integrity sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ== + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -5856,15 +5891,15 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^10.0.0, glob@^10.2.2: - version "10.2.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.2.tgz#ce2468727de7e035e8ecf684669dc74d0526ab75" - integrity sha512-Xsa0BcxIC6th9UwNjZkhrMtNo/MnyRL8jGCP+uEwhA5oFOCY1f2s1/oNKY47xQ0Bg5nkjsfAEIej1VeH62bDDQ== +glob@^10.2.5, glob@^10.2.6: + version "10.2.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.6.tgz#1e27edbb3bbac055cb97113e27a066c100a4e5e1" + integrity sha512-U/rnDpXJGF414QQQZv5uVsabTVxMSwzS5CH0p3DRCIV6ownl4f7PzGnkGmvlum2wB+9RlJWJZ6ACU1INnBqiPA== dependencies: foreground-child "^3.1.0" jackspeak "^2.0.3" - minimatch "^9.0.0" - minipass "^5.0.0" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2" path-scurry "^1.7.0" glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: @@ -5939,6 +5974,17 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +globby@^13.1.3: + version "13.1.4" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.4.tgz#2f91c116066bcec152465ba36e5caa4a13c01317" + integrity sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.2.11" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^4.0.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -5967,6 +6013,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== +graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + grapheme-splitter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" @@ -6622,6 +6673,16 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + is-electron@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.0.tgz#8943084f09e8b731b3a7a0298a7b5d56f6b7eef0" @@ -6678,6 +6739,13 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + is-map@^2.0.1, is-map@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" @@ -6861,6 +6929,13 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -7416,19 +7491,16 @@ jsdom@^20.0.0: ws "^8.11.0" xml-name-validator "^4.0.0" -jsdom@^21.1.2: - version "21.1.2" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.1.2.tgz#6433f751b8718248d646af1cdf6662dc8a1ca7f9" - integrity sha512-sCpFmK2jv+1sjff4u7fzft+pUh2KSUbUrEHYHyfSIbGTIcmnjyp83qg6qLwdJ/I3LpTXx33ACxeRL7Lsyc6lGQ== +jsdom@^22.0.0: + version "22.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-22.0.0.tgz#3295c6992c70089c4b8f5cf060489fddf7ee9816" + integrity sha512-p5ZTEb5h+O+iU02t0GfEjAnkdYPrQSkfuTSMkMYyIoMvUNEHsbG0bHHbfXIcfTqD2UfvjQX7mmgiFsyRwGscVw== dependencies: abab "^2.0.6" - acorn "^8.8.2" - acorn-globals "^7.0.0" cssstyle "^3.0.0" data-urls "^4.0.0" decimal.js "^10.4.3" domexception "^4.0.0" - escodegen "^2.0.0" form-data "^4.0.0" html-encoding-sniffer "^3.0.0" http-proxy-agent "^5.0.0" @@ -7805,10 +7877,10 @@ lru-cache@^9.0.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.0.1.tgz#ac061ed291f8b9adaca2b085534bb1d3b61bef83" integrity sha512-C8QsKIN1UIXeOs3iWmiZ1lQY+EnKDojWd37fXy1aSbJvH4iSma1uy2OWuoB3m4SYRli5+CUjDv3Dij5DVoetmg== -lz-string@^1.4.4: - version "1.4.4" - resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" - integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.9" @@ -8063,10 +8135,10 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.0.tgz#bfc8e88a1c40ffd40c172ddac3decb8451503b56" - integrity sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w== +minimatch@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253" + integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== dependencies: brace-expansion "^2.0.1" @@ -8117,6 +8189,11 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== +"minipass@^5.0.0 || ^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-6.0.2.tgz#542844b6c4ce95b202c0995b0a471f1229de4c81" + integrity sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w== + minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -8145,10 +8222,10 @@ mkdirp@^1.0, mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19" - integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A== +mkdirp@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== mousetrap@^1.5.2: version "1.6.5" @@ -8508,6 +8585,16 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +open@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/open/-/open-9.1.0.tgz#684934359c90ad25742f5a26151970ff8c6c80b6" + integrity sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg== + dependencies: + default-browser "^4.0.0" + define-lazy-prop "^3.0.0" + is-inside-container "^1.0.0" + is-wsl "^2.2.0" + opencollective-postinstall@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" @@ -8786,15 +8873,10 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -pg-connection-string@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.4.0.tgz#c979922eb47832999a204da5dbe1ebf2341b6a10" - integrity sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ== - -pg-connection-string@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" - integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== +pg-connection-string@^2.4.0, pg-connection-string@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.0.tgz#12a36cc4627df19c25cc1b9b736cc39ee1f73ae8" + integrity sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg== pg-int8@1.0.1: version "1.0.1" @@ -9394,7 +9476,7 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== -raf@^3.1.0, raf@^3.4.1: +raf@^3.1.0: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== @@ -9431,15 +9513,13 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -react-dom@^16.14.0: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" - integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.19.1" + scheduler "^0.23.0" react-event-listener@^0.6.0: version "0.6.6" @@ -9509,7 +9589,12 @@ react-intl@^2.9.0: intl-relativeformat "^2.1.0" invariant "^2.1.1" -react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.6: +"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -9627,6 +9712,14 @@ react-select@*, react-select@^5.7.3: react-transition-group "^4.3.0" use-isomorphic-layout-effect "^1.1.2" +react-shallow-renderer@^16.15.0: + version "16.15.0" + resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457" + integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA== + dependencies: + object-assign "^4.1.1" + react-is "^16.12.0 || ^17.0.0 || ^18.0.0" + react-side-effect@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a" @@ -9670,15 +9763,14 @@ react-swipeable-views@^0.14.0: react-swipeable-views-utils "^0.14.0" warning "^4.0.1" -react-test-renderer@^16.14.0: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae" - integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg== +react-test-renderer@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.2.0.tgz#1dd912bd908ff26da5b9fca4fd1c489b9523d37e" + integrity sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA== dependencies: - object-assign "^4.1.1" - prop-types "^15.6.2" - react-is "^16.8.6" - scheduler "^0.19.1" + react-is "^18.2.0" + react-shallow-renderer "^16.15.0" + scheduler "^0.23.0" react-textarea-autosize@*, react-textarea-autosize@^8.4.1: version "8.4.1" @@ -9706,14 +9798,12 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" -react@^16.14.0: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" - integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" read-pkg-up@^7.0.1: version "7.0.1" @@ -9843,7 +9933,7 @@ regenerator-runtime@^0.12.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== -regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4: +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.3: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== @@ -10077,12 +10167,12 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -rimraf@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.0.tgz#5bda14e410d7e4dd522154891395802ce032c2cb" - integrity sha512-Jf9llaP+RvaEVS5nPShYFhtXIrb3LRKP281ib3So0KkeZKo2wIKyq0Re7TOSwanasA423PSr6CCIL4bP6T040g== +rimraf@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.1.tgz#0881323ab94ad45fec7c0221f27ea1a142f3f0d0" + integrity sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg== dependencies: - glob "^10.0.0" + glob "^10.2.5" ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" @@ -10114,6 +10204,13 @@ rrweb-cssom@^0.6.0: resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw== +run-applescript@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-5.0.0.tgz#e11e1c932e055d5c6b40d98374e0268d9b11899c" + integrity sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg== + dependencies: + execa "^5.0.0" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -10186,13 +10283,12 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" - integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" schema-utils@^1.0.0: version "1.0.0" @@ -10251,10 +10347,10 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.0: - version "7.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" - integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== +semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.1: + version "7.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec" + integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw== dependencies: lru-cache "^6.0.0" @@ -10426,6 +10522,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + slice-ansi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" @@ -10949,10 +11050,10 @@ stylelint-scss@^4.6.0: postcss-selector-parser "^6.0.11" postcss-value-parser "^4.2.0" -stylelint@^15.6.1: - version "15.6.1" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.6.1.tgz#e4cd33a3af88587b99a5d1328aedd8c298b6dc81" - integrity sha512-d8icFBlVl93Elf3Z5ABQNOCe4nx69is3D/NZhDLAie1eyYnpxfeKe7pCfqzT5W4F8vxHCLSDfV8nKNJzogvV2Q== +stylelint@^15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.6.2.tgz#06d9005b62a83b72887eed623520e9b472af8c15" + integrity sha512-fjQWwcdUye4DU+0oIxNGwawIPC5DvG5kdObY5Sg4rc87untze3gC/5g/ikePqVjrAsBUZjwMN+pZsAYbDO6ArQ== dependencies: "@csstools/css-parser-algorithms" "^2.1.1" "@csstools/css-tokenizer" "^2.1.1" @@ -11070,6 +11171,14 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +synckit@^0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.5.tgz#b7f4358f9bb559437f9f167eb6bc46b3c9818fa3" + integrity sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q== + dependencies: + "@pkgr/utils" "^2.3.1" + tslib "^2.5.0" + table@^6.8.1: version "6.8.1" resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" @@ -11086,6 +11195,11 @@ tapable@^1.0, tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + tar@^6.0.2: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" @@ -11208,6 +11322,11 @@ tiny-warning@^1.0.0: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +titleize@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53" + integrity sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ== + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -11316,7 +11435,7 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.5.0, tslib@^2.1.0, tslib@^2.4.0: +tslib@2.5.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== @@ -11534,6 +11653,11 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + upath@^1.1.1, upath@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" @@ -11849,10 +11973,10 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" -webpack-merge@^5.8.0: - version "5.8.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" - integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== +webpack-merge@^5.9.0: + version "5.9.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826" + integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg== dependencies: clone-deep "^4.0.1" wildcard "^2.0.0"